diff --git a/Makefile b/Makefile
index 25edc0f05d5333c3c5bc541fd4a3cec93aebf485..360d733fa356d89f6cf4e12ce681a235a6e9baa3 100644
--- a/Makefile
+++ b/Makefile
@@ -34,4 +34,4 @@ update_master:
 
 master: update_master clean build version
 
-release: update_release clean build version
+release: update_release clean build
diff --git a/README.md b/README.md
index a5041e876b23c787616d2f260830f2aca19de0d3..cb6d69e09ad7aab6c7676b2aae8c310539d286b4 100644
--- a/README.md
+++ b/README.md
@@ -145,59 +145,49 @@ Usage:
   client [command]
 
 Available Commands:
-  generate    Generates version and dependency information for the
-              Elixxir binary
+  generate    Generates version and dependency information for the Elixxir binary
+  getndf      Download the network definition file from the network and print it.
+  group       Group commands for cMix client
   help        Help about any command
-  version     Print the version and dependency information for the
-              Elixxir binary
+  init        Initialize a user ID but do not connect to the network
+  proto       Load client with a proto client JSON file.
+  single      Send and respond to single-use messages.
+  ud          Register for and search users using the xx network user discovery service.
+  version     Print the version and dependency information for the Elixxir binary
 
 Flags:
-      --accept-channel            Accept the channel request for the
-                                  corresponding recipient ID
-      --delete-channel            Delete the channel information for the
-                                  corresponding recipient ID                            
+      --accept-channel            Accept the channel request for the corresponding recipient ID
+      --auth-timeout uint         The number of seconds to wait for an authentication channelto confirm (default 120)
+      --delete-channel            Delete the channel information for the corresponding recipient ID
       --destfile string           Read this contact file for the destination id
-  -d, --destid string             ID to send message to (if below 40, will be
-                                  precanned. Use '0x' or 'b64:' for hex and
-                                  base64 representations) (default "0")
-      --forceHistoricalRounds     Force all rounds to be sent to historical
-                                  round retrieval
-      --forceMessagePickupRetry   Enable a mechanism which forces a 50% chance 
-                                  of no message pickup, instead triggering the 
-                                  message pickup retry mechanism
+  -d, --destid string             ID to send message to (if below 40, will be precanned. Use '0x' or 'b64:' for hex and base64 representations) (default "0")
+      --e2eMaxKeys uint           Max keys used before blocking until a rekey completes (default 800)
+      --e2eMinKeys uint           Minimum number of keys used before requesting rekey (default 500)
+      --e2eNumReKeys uint         Number of rekeys reserved for rekey operations (default 16)
+      --forceHistoricalRounds     Force all rounds to be sent to historical round retrieval
+      --forceMessagePickupRetry   Enable a mechanism which forces a 50% chance of no message pickup, instead triggering the message pickup retry mechanism
   -h, --help                      help for client
-  -l, --log string                Path to the log output path (- is stdout)
-                                  (default "-")
+  -l, --log string                Path to the log output path (- is stdout) (default "-")
+  -v, --logLevel uint             Verbose mode for debugging
   -m, --message string            Message to send
-  -n, --ndf string                Path to the network definition JSON file
-                                  (default "ndf.json")
+  -n, --ndf string                Path to the network definition JSON file (default "ndf.json")
   -p, --password string           Password to the session file
-      --receiveCount uint         How many messages we should wait for before
-                                  quitting (default 1)
-      --regcode string            Registration code (optional)
-      --sendCount uint            The number of times to send the message
-                                  (default 1)
-      --sendDelay uint            The delay between sending the messages in ms
-                                  (default 500)
-      --sendid uint               Use precanned user id (must be between 1 and
-                                  40, inclusive)
-      --slowPolling bool          Enables polling for all network updates and RSA signed rounds.
-                                  Defaults to true (filtered updates with ECC signed rounds) if not set
-
-  -s, --session string            Sets the initial directory for client storage
-      --unsafe                    Send raw, unsafe messages without e2e
-                                  encryption.
-      --unsafe-channel-creation   Turns off the user identity authenticated
-                                  channel check, automatically approving
-                                  authenticated channels
-      --verboseRoundTracking      Verbose round tracking, keeps track and prints 
-                                  all rounds the client was aware of while running. 
-                                  Defaults to false if not set.
-  -v, --logLevel uint             Level of debugging to print (0 = info, 
-                                  1 = debug, >1 = trace). (Default info)
-      --waitTimeout uint          The number of seconds to wait for messages to
-                                  arrive (default 15)
-  -w, --writeContact string       Write the contact file for this user to this
+      --profile-cpu string        Enable cpu profiling to this file
+      --protoUserOut string       Path to which a normally constructed client will write proto user JSON file (default "protoUser.json")
+      --protoUserPath string      Path to proto user JSON file containing cryptographic primitives the client will load (default "protoUser.json")
+      --receiveCount uint         How many messages we should wait for before quitting (default 1)
+      --regcode string            Identity code (optional)
+      --send-auth-request         Send an auth request to the specified destination and waitfor confirmation
+      --sendCount uint            The number of times to send the message (default 1)
+      --sendDelay uint            The delay between sending the messages in ms (default 500)
+      --sendid uint               Use precanned user id (must be between 1 and 40, inclusive)
+  -s, --session string            Sets the initial storage directory for client session data
+      --slowPolling               Enables polling for unfiltered network updates with RSA signatures
+      --unsafe                    Send raw, unsafe messages without e2e encryption.
+      --unsafe-channel-creation   Turns off the user identity authenticated channel check, automatically approving authenticated channels
+      --verboseRoundTracking      Verbose round tracking, keeps track and prints all rounds the client was aware of while running. Defaults to false if not set.
+      --waitTimeout uint          The number of seconds to wait for messages to arrive (default 15)
+  -w, --writeContact string       Write contact information, if any, to this file,  defaults to stdout (default "-")
                                   file
 
 Use "client [command] --help" for more information about a command.
diff --git a/api/authenticatedChannel.go b/api/authenticatedChannel.go
index 746572e94e73eb85c16dc60851adb04d901297a1..b302f78d46b81b9ebdb20d9f3e7e08f9ce2abb47 100644
--- a/api/authenticatedChannel.go
+++ b/api/authenticatedChannel.go
@@ -12,6 +12,8 @@ import (
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/auth"
 	"gitlab.com/elixxir/client/interfaces"
+	"gitlab.com/elixxir/client/interfaces/preimage"
+	"gitlab.com/elixxir/client/storage/edge"
 	"gitlab.com/elixxir/crypto/contact"
 	"gitlab.com/elixxir/primitives/fact"
 	"gitlab.com/xx_network/primitives/id"
@@ -103,6 +105,27 @@ func (c *Client) MakePrecannedAuthenticatedChannel(precannedID uint) (contact.Co
 	// the channel
 	c.network.CheckGarbledMessages()
 
+	//add the e2e and rekey firngeprints
+	//e2e
+	sessionPartner, err := c.storage.E2e().GetPartner(precan.ID)
+	if err != nil {
+		jww.FATAL.Panicf("Cannot find %s right after creating: %+v", precan.ID, err)
+	}
+	me := c.storage.GetUser().ReceptionID
+
+	c.storage.GetEdge().Add(edge.Preimage{
+		Data:   sessionPartner.GetE2EPreimage(),
+		Type:   preimage.E2e,
+		Source: precan.ID[:],
+	}, me)
+
+	//rekey
+	c.storage.GetEdge().Add(edge.Preimage{
+		Data:   sessionPartner.GetRekeyPreimage(),
+		Type:   preimage.Rekey,
+		Source: precan.ID[:],
+	}, me)
+
 	return precan, err
 }
 
diff --git a/api/client.go b/api/client.go
index 012e980a5d946c5dd632b896a9d26037d9b308bd..ef162c13555cdba73c64ce38cc970c343c263eca 100644
--- a/api/client.go
+++ b/api/client.go
@@ -8,17 +8,20 @@
 package api
 
 import (
+	"encoding/json"
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/auth"
 	"gitlab.com/elixxir/client/interfaces"
 	"gitlab.com/elixxir/client/interfaces/params"
+	"gitlab.com/elixxir/client/interfaces/preimage"
 	"gitlab.com/elixxir/client/interfaces/user"
 	"gitlab.com/elixxir/client/keyExchange"
 	"gitlab.com/elixxir/client/network"
 	"gitlab.com/elixxir/client/registration"
 	"gitlab.com/elixxir/client/stoppable"
 	"gitlab.com/elixxir/client/storage"
+	"gitlab.com/elixxir/client/storage/edge"
 	"gitlab.com/elixxir/client/switchboard"
 	"gitlab.com/elixxir/comms/client"
 	"gitlab.com/elixxir/crypto/cyclic"
@@ -72,7 +75,8 @@ type Client struct {
 // with the network. Note that this does not register a username/identity, but
 // merely creates a new cryptographic identity for adding such information
 // at a later date.
-func NewClient(ndfJSON, storageDir string, password []byte, registrationCode string) error {
+func NewClient(ndfJSON, storageDir string, password []byte,
+	registrationCode string) error {
 	jww.INFO.Printf("NewClient(dir: %s)", storageDir)
 	// Use fastRNG for RNG ops (AES fortuna based RNG using system RNG)
 	rngStreamGen := fastRNG.NewStreamGenerator(12, 1024, csprng.NewSystemRNG)
@@ -88,7 +92,7 @@ func NewClient(ndfJSON, storageDir string, password []byte, registrationCode str
 	protoUser := createNewUser(rngStreamGen, cmixGrp, e2eGrp)
 	jww.DEBUG.Printf("User generation took: %s", time.Now().Sub(start))
 
-	err = checkVersionAndSetupStorage(def, storageDir, password, protoUser,
+	_, err = checkVersionAndSetupStorage(def, storageDir, password, protoUser,
 		cmixGrp, e2eGrp, rngStreamGen, false, registrationCode)
 	if err != nil {
 		return err
@@ -103,7 +107,8 @@ func NewClient(ndfJSON, storageDir string, password []byte, registrationCode str
 // with the network. Note that this does not register a username/identity, but
 // merely creates a new cryptographic identity for adding such information
 // at a later date.
-func NewPrecannedClient(precannedID uint, defJSON, storageDir string, password []byte) error {
+func NewPrecannedClient(precannedID uint, defJSON, storageDir string,
+	password []byte) error {
 	jww.INFO.Printf("NewPrecannedClient()")
 	// Use fastRNG for RNG ops (AES fortuna based RNG using system RNG)
 	rngStreamGen := fastRNG.NewStreamGenerator(12, 1024, csprng.NewSystemRNG)
@@ -118,7 +123,7 @@ func NewPrecannedClient(precannedID uint, defJSON, storageDir string, password [
 
 	protoUser := createPrecannedUser(precannedID, rngStream, cmixGrp, e2eGrp)
 
-	err = checkVersionAndSetupStorage(def, storageDir, password, protoUser,
+	_, err = checkVersionAndSetupStorage(def, storageDir, password, protoUser,
 		cmixGrp, e2eGrp, rngStreamGen, true, "")
 	if err != nil {
 		return err
@@ -132,7 +137,8 @@ func NewPrecannedClient(precannedID uint, defJSON, storageDir string, password [
 // with the network. Note that this does not register a username/identity, but
 // merely creates a new cryptographic identity for adding such information
 // at a later date.
-func NewVanityClient(ndfJSON, storageDir string, password []byte, registrationCode string, userIdPrefix string) error {
+func NewVanityClient(ndfJSON, storageDir string, password []byte,
+	registrationCode string, userIdPrefix string) error {
 	jww.INFO.Printf("NewVanityClient()")
 	// Use fastRNG for RNG ops (AES fortuna based RNG using system RNG)
 	rngStreamGen := fastRNG.NewStreamGenerator(12, 1024, csprng.NewSystemRNG)
@@ -147,7 +153,7 @@ func NewVanityClient(ndfJSON, storageDir string, password []byte, registrationCo
 
 	protoUser := createNewVanityUser(rngStream, cmixGrp, e2eGrp, userIdPrefix)
 
-	err = checkVersionAndSetupStorage(def, storageDir, password, protoUser,
+	_, err = checkVersionAndSetupStorage(def, storageDir, password, protoUser,
 		cmixGrp, e2eGrp, rngStreamGen, false, registrationCode)
 	if err != nil {
 		return err
@@ -193,6 +199,55 @@ func OpenClient(storageDir string, password []byte, parameters params.Network) (
 	return c, nil
 }
 
+// NewProtoClient_Unsafe initializes a client object from a JSON containing
+// predefined cryptographic which defines a user. This is designed for some
+// specific deployment procedures and is generally unsafe.
+func NewProtoClient_Unsafe(ndfJSON, storageDir string, password,
+	protoClientJSON []byte) error {
+	jww.INFO.Printf("NewProtoClient_Unsafe")
+
+	// Use fastRNG for RNG ops (AES fortuna based RNG using system RNG)
+	rngStreamGen := fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG)
+
+	// Parse the NDF
+	def, err := parseNDF(ndfJSON)
+	if err != nil {
+		return err
+	}
+
+	cmixGrp, e2eGrp := decodeGroups(def)
+
+	// Pull the proto user from the JSON
+	protoUser := &user.Proto{}
+	err = json.Unmarshal(protoClientJSON, protoUser)
+	if err != nil {
+		return err
+	}
+
+	// Initialize a user object for storage set up
+	usr := user.NewUserFromProto(protoUser)
+
+	// Set up storage
+	storageSess, err := checkVersionAndSetupStorage(def, storageDir, password, usr,
+		cmixGrp, e2eGrp, rngStreamGen, false, protoUser.RegCode)
+	if err != nil {
+		return err
+	}
+
+	// Set registration values in storage
+	storageSess.User().SetReceptionRegistrationValidationSignature(protoUser.ReceptionRegValidationSig)
+	storageSess.User().SetTransmissionRegistrationValidationSignature(protoUser.TransmissionRegValidationSig)
+	storageSess.User().SetRegistrationTimestamp(protoUser.RegistrationTimestamp)
+
+	//move the registration state to indicate registered with registration on proto client
+	err = storageSess.ForwardRegistrationStatus(storage.PermissioningComplete)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
 // Login initializes a client object from existing storage.
 func Login(storageDir string, password []byte, parameters params.Network) (*Client, error) {
 	jww.INFO.Printf("Login()")
@@ -234,7 +289,8 @@ func Login(storageDir string, password []byte, parameters params.Network) (*Clie
 		hp.KaClientOpts.Time = time.Duration(math.MaxInt64)
 		hp.AuthEnabled = false
 		hp.MaxRetries = 5
-		_, err = c.comms.AddHost(&id.NotificationBot, def.Notification.Address, []byte(def.Notification.TlsCertificate), hp)
+		_, err = c.comms.AddHost(&id.NotificationBot, def.Notification.Address,
+			[]byte(def.Notification.TlsCertificate), hp)
 		if err != nil {
 			jww.WARN.Printf("Failed adding host for notifications: %+v", err)
 		}
@@ -380,7 +436,9 @@ func (c *Client) registerFollower() error {
 	}
 
 	//register the core follower service
-	err = c.followerServices.add(func() (stoppable.Stoppable, error) { return c.network.Follow(cer) })
+	err = c.followerServices.add(func() (stoppable.Stoppable, error) {
+		return c.network.Follow(cer)
+	})
 	if err != nil {
 		return errors.WithMessage(err, "Failed to start following "+
 			"the network")
@@ -537,31 +595,68 @@ func (c *Client) GetNodeRegistrationStatus() (int, int, error) {
 	cmixStore := c.storage.Cmix()
 
 	var numRegistered int
+	var numStale = 0
 	for i, n := range nodes {
 		nid, err := id.Unmarshal(n.ID)
 		if err != nil {
 			return 0, 0, errors.Errorf("Failed to unmarshal node ID %v "+
 				"(#%d): %s", n.ID, i, err.Error())
 		}
+		if n.Status == ndf.Stale {
+			numStale += 1
+			continue
+		}
 		if cmixStore.Has(nid) {
 			numRegistered++
 		}
 	}
 
 	// Get the number of in progress node registrations
-	return numRegistered, len(nodes), nil
+	return numRegistered, len(nodes) - numStale, nil
 }
 
 // DeleteContact is a function which removes a partner from Client's storage
 func (c *Client) DeleteContact(partnerId *id.ID) error {
 	jww.DEBUG.Printf("Deleting contact with ID %s", partnerId)
-	if err := c.storage.E2e().DeletePartner(partnerId); err != nil {
+	//get the partner so they can be removed from preiamge store
+	partner, err := c.storage.E2e().GetPartner(partnerId)
+	if err != nil {
+		return errors.WithMessagef(err, "Could not delete %s because "+
+			"they could not be found", partnerId)
+	}
+	e2ePreimage := partner.GetE2EPreimage()
+	rekeyPreimage := partner.GetRekeyPreimage()
+
+	//delete the partner
+	if err = c.storage.E2e().DeletePartner(partnerId); err != nil {
 		return err
 	}
-	if err := c.storage.Auth().Delete(partnerId); err != nil {
+	//delete the preimages
+	if err = c.storage.GetEdge().Remove(edge.Preimage{
+		Data:   e2ePreimage,
+		Type:   preimage.E2e,
+		Source: partnerId[:],
+	}, c.storage.GetUser().ReceptionID); err != nil {
+		jww.WARN.Printf("Failed delete the preimage for e2e "+
+			"from %s on contact deletion: %+v", partnerId, err)
+	}
+
+	if err = c.storage.GetEdge().Remove(edge.Preimage{
+		Data:   rekeyPreimage,
+		Type:   preimage.Rekey,
+		Source: partnerId[:],
+	}, c.storage.GetUser().ReceptionID); err != nil {
+		jww.WARN.Printf("Failed delete the preimage for rekey "+
+			"from %s on contact deletion: %+v", partnerId, err)
+	}
+
+	if err = c.storage.Auth().Delete(partnerId); err != nil {
 		return err
 	}
+
+	//delete conversations
 	c.storage.Conversations().Delete(partnerId)
+
 	return nil
 }
 
@@ -664,13 +759,15 @@ func decodeGroups(ndf *ndf.NetworkDefinition) (cmixGrp, e2eGrp *cyclic.Group) {
 
 // checkVersionAndSetupStorage is common code shared by NewClient, NewPrecannedClient and NewVanityClient
 // it checks client version and creates a new storage for user data
-func checkVersionAndSetupStorage(def *ndf.NetworkDefinition, storageDir string, password []byte,
-	protoUser user.User, cmixGrp, e2eGrp *cyclic.Group, rngStreamGen *fastRNG.StreamGenerator,
-	isPrecanned bool, registrationCode string) error {
+func checkVersionAndSetupStorage(def *ndf.NetworkDefinition,
+	storageDir string, password []byte,
+	protoUser user.User,
+	cmixGrp, e2eGrp *cyclic.Group, rngStreamGen *fastRNG.StreamGenerator,
+	isPrecanned bool, registrationCode string) (*storage.Session, error) {
 	// Get current client version
 	currentVersion, err := version.ParseVersion(SEMVER)
 	if err != nil {
-		return errors.WithMessage(err, "Could not parse version string.")
+		return nil, errors.WithMessage(err, "Could not parse version string.")
 	}
 
 	// Create Storage
@@ -678,7 +775,7 @@ func checkVersionAndSetupStorage(def *ndf.NetworkDefinition, storageDir string,
 	storageSess, err := storage.New(storageDir, passwordStr, protoUser,
 		currentVersion, cmixGrp, e2eGrp, rngStreamGen)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	// Save NDF to be used in the future
@@ -694,10 +791,17 @@ func checkVersionAndSetupStorage(def *ndf.NetworkDefinition, storageDir string,
 		err = storageSess.ForwardRegistrationStatus(storage.PermissioningComplete)
 	}
 
+	//add the request preiamge
+	storageSess.GetEdge().Add(edge.Preimage{
+		Data:   preimage.GenerateRequest(protoUser.ReceptionID),
+		Type:   preimage.Request,
+		Source: protoUser.ReceptionID[:],
+	}, protoUser.ReceptionID)
+
 	if err != nil {
-		return errors.WithMessage(err, "Failed to denote state "+
+		return nil, errors.WithMessage(err, "Failed to denote state "+
 			"change in session")
 	}
 
-	return nil
+	return storageSess, nil
 }
diff --git a/api/notifications.go b/api/notifications.go
index 2a7d1b7c107128360f89930c8bf0bcb16dfc54de..58e97190e469a7d21d9175b9b84ddaff89b75e2c 100644
--- a/api/notifications.go
+++ b/api/notifications.go
@@ -43,7 +43,7 @@ func (c *Client) RegisterForNotifications(token string) error {
 			TransmissionSalt:      c.GetUser().TransmissionSalt,
 			TransmissionRsaSig:    c.GetStorage().User().GetTransmissionRegistrationValidationSignature(),
 			IIDTransmissionRsaSig: sig,
-			RegistrationTimestamp: c.GetUser().RegistrationTimestamp.UnixNano(),
+			RegistrationTimestamp: c.GetUser().RegistrationTimestamp,
 		})
 	if err != nil {
 		err := errors.Errorf(
diff --git a/api/permissioning.go b/api/permissioning.go
index ef94e61daf315472ecde87eddc360335c08b445d..85aac34b93f333f4994c49b1dac1ec868965807a 100644
--- a/api/permissioning.go
+++ b/api/permissioning.go
@@ -8,7 +8,9 @@
 package api
 
 import (
+	"encoding/json"
 	"github.com/pkg/errors"
+	"gitlab.com/elixxir/client/interfaces/user"
 	"gitlab.com/elixxir/client/storage"
 )
 
@@ -47,3 +49,41 @@ func (c *Client) registerWithPermissioning() error {
 	}
 	return nil
 }
+
+// ConstructProtoUerFile is a helper function which is used for proto client testing.
+// This is used for development testing.
+func (c *Client) ConstructProtoUerFile() ([]byte, error) {
+
+	//load the registration code
+	regCode, err := c.storage.GetRegCode()
+	if err != nil {
+		return nil, errors.WithMessage(err, "failed to register with "+
+			"permissioning")
+	}
+
+	Usr := user.Proto{
+		TransmissionID:               c.GetUser().TransmissionID,
+		TransmissionSalt:             c.GetUser().TransmissionSalt,
+		TransmissionRSA:              c.GetUser().TransmissionRSA,
+		ReceptionID:                  c.GetUser().ReceptionID,
+		ReceptionSalt:                c.GetUser().ReceptionSalt,
+		ReceptionRSA:                 c.GetUser().ReceptionRSA,
+		Precanned:                    c.GetUser().Precanned,
+		RegistrationTimestamp:        c.GetUser().RegistrationTimestamp,
+		RegCode:                      regCode,
+		TransmissionRegValidationSig: c.storage.User().GetTransmissionRegistrationValidationSignature(),
+		ReceptionRegValidationSig:    c.storage.User().GetReceptionRegistrationValidationSignature(),
+		CmixDhPrivateKey:             c.GetStorage().Cmix().GetDHPrivateKey(),
+		CmixDhPublicKey:              c.GetStorage().Cmix().GetDHPublicKey(),
+		E2eDhPrivateKey:              c.GetStorage().E2e().GetDHPrivateKey(),
+		E2eDhPublicKey:               c.GetStorage().E2e().GetDHPublicKey(),
+	}
+
+	jsonBytes, err := json.Marshal(Usr)
+	if err != nil {
+		return nil, errors.WithMessage(err, "failed to register with "+
+			"permissioning")
+	}
+
+	return jsonBytes, nil
+}
diff --git a/api/user.go b/api/user.go
index ef8eb1092ee61e8e766c00d2979cb4c20d108d82..1b22a0877235636c55bbd2be922cddfcf4fe2b39 100644
--- a/api/user.go
+++ b/api/user.go
@@ -36,6 +36,62 @@ func createNewUser(rng *fastRNG.StreamGenerator, cmix, e2e *cyclic.Group) user.U
 
 	var cMixKeyBytes, e2eKeyBytes, transmissionSalt, receptionSalt []byte
 
+	cMixKeyBytes, e2eKeyBytes, transmissionSalt, receptionSalt,
+		transmissionRsaKey, receptionRsaKey = createDhKeys(rng, cmix, e2e)
+
+	// Salt, UID, etc gen
+	stream := rng.GetStream()
+	transmissionSalt = make([]byte, SaltSize)
+
+	n, err := stream.Read(transmissionSalt)
+
+	if err != nil {
+		jww.FATAL.Panicf(err.Error())
+	}
+	if n != SaltSize {
+		jww.FATAL.Panicf("transmissionSalt size too small: %d", n)
+	}
+
+	receptionSalt = make([]byte, SaltSize)
+
+	n, err = stream.Read(receptionSalt)
+
+	if err != nil {
+		jww.FATAL.Panicf(err.Error())
+	}
+	if n != SaltSize {
+		jww.FATAL.Panicf("transmissionSalt size too small: %d", n)
+	}
+
+	stream.Close()
+
+	transmissionID, err := xx.NewID(transmissionRsaKey.GetPublic(), transmissionSalt, id.User)
+	if err != nil {
+		jww.FATAL.Panicf(err.Error())
+	}
+
+	receptionID, err := xx.NewID(receptionRsaKey.GetPublic(), receptionSalt, id.User)
+	if err != nil {
+		jww.FATAL.Panicf(err.Error())
+	}
+
+	return user.User{
+		TransmissionID:   transmissionID.DeepCopy(),
+		TransmissionSalt: transmissionSalt,
+		TransmissionRSA:  transmissionRsaKey,
+		ReceptionID:      receptionID.DeepCopy(),
+		ReceptionSalt:    receptionSalt,
+		ReceptionRSA:     receptionRsaKey,
+		Precanned:        false,
+		CmixDhPrivateKey: cmix.NewIntFromBytes(cMixKeyBytes),
+		E2eDhPrivateKey:  e2e.NewIntFromBytes(e2eKeyBytes),
+	}
+}
+
+func createDhKeys(rng *fastRNG.StreamGenerator,
+	cmix, e2e *cyclic.Group) (cMixKeyBytes, e2eKeyBytes,
+	transmissionSalt, receptionSalt []byte,
+	transmissionRsaKey, receptionRsaKey *rsa.PrivateKey) {
 	wg := sync.WaitGroup{}
 
 	wg.Add(4)
@@ -93,53 +149,8 @@ func createNewUser(rng *fastRNG.StreamGenerator, cmix, e2e *cyclic.Group) user.U
 	}()
 	wg.Wait()
 
-	// Salt, UID, etc gen
-	stream := rng.GetStream()
-	transmissionSalt = make([]byte, SaltSize)
-
-	n, err := stream.Read(transmissionSalt)
-
-	if err != nil {
-		jww.FATAL.Panicf(err.Error())
-	}
-	if n != SaltSize {
-		jww.FATAL.Panicf("transmissionSalt size too small: %d", n)
-	}
-
-	receptionSalt = make([]byte, SaltSize)
-
-	n, err = stream.Read(receptionSalt)
-
-	if err != nil {
-		jww.FATAL.Panicf(err.Error())
-	}
-	if n != SaltSize {
-		jww.FATAL.Panicf("transmissionSalt size too small: %d", n)
-	}
-
-	stream.Close()
-
-	transmissionID, err := xx.NewID(transmissionRsaKey.GetPublic(), transmissionSalt, id.User)
-	if err != nil {
-		jww.FATAL.Panicf(err.Error())
-	}
-
-	receptionID, err := xx.NewID(receptionRsaKey.GetPublic(), receptionSalt, id.User)
-	if err != nil {
-		jww.FATAL.Panicf(err.Error())
-	}
+	return
 
-	return user.User{
-		TransmissionID:   transmissionID.DeepCopy(),
-		TransmissionSalt: transmissionSalt,
-		TransmissionRSA:  transmissionRsaKey,
-		ReceptionID:      receptionID.DeepCopy(),
-		ReceptionSalt:    receptionSalt,
-		ReceptionRSA:     receptionRsaKey,
-		Precanned:        false,
-		CmixDhPrivateKey: cmix.NewIntFromBytes(cMixKeyBytes),
-		E2eDhPrivateKey:  e2e.NewIntFromBytes(e2eKeyBytes),
-	}
 }
 
 // TODO: Add precanned user code structures here.
diff --git a/api/version_vars.go b/api/version_vars.go
index fbf2295851c49dc762e377b4b311e45e270d984d..694d6b1eea937ed4bc6c4a154ec3d22d71dcac20 100644
--- a/api/version_vars.go
+++ b/api/version_vars.go
@@ -1,10 +1,10 @@
 // Code generated by go generate; DO NOT EDIT.
 // This file was generated by robots at
-// 2021-10-07 12:02:04.067615 -0500 CDT m=+0.027463409
+// 2021-11-11 13:19:35.619263 -0600 CST m=+0.049820030
 package api
 
-const GITVERSION = `86c66226 update deps`
-const SEMVER = "2.10.0"
+const GITVERSION = `e595b772 Merge branch 'hotfix/stale-registration' into 'release'`
+const SEMVER = "3.2.0"
 const DEPENDENCIES = `module gitlab.com/elixxir/client
 
 go 1.13
@@ -24,18 +24,18 @@ require (
 	github.com/spf13/jwalterweatherman v1.1.0
 	github.com/spf13/viper v1.7.1
 	gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228
-	gitlab.com/elixxir/comms v0.0.4-0.20211006231929-cc2735ca43a6
-	gitlab.com/elixxir/crypto v0.0.7-0.20211006231624-44434504fff4
+	gitlab.com/elixxir/comms v0.0.4-0.20211101174956-590ba1b47887
+	gitlab.com/elixxir/crypto v0.0.7-0.20211022013957-3a7899285c4c
 	gitlab.com/elixxir/ekv v0.1.5
-	gitlab.com/elixxir/primitives v0.0.3-0.20210920180121-b85bca5212f4
-	gitlab.com/xx_network/comms v0.0.4-0.20211006231434-99dd38f025a7
-	gitlab.com/xx_network/crypto v0.0.5-0.20211006222352-8e0ac37b86b0
-	gitlab.com/xx_network/primitives v0.0.4-0.20210915220237-70cb4551d6f3
-	golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
+	gitlab.com/elixxir/primitives v0.0.3-0.20211102233208-a716d5c670b6
+	gitlab.com/xx_network/comms v0.0.4-0.20211014163953-e774276b83ae
+	gitlab.com/xx_network/crypto v0.0.5-0.20211014163843-57b345890686
+	gitlab.com/xx_network/primitives v0.0.4-0.20211014163031-53405cf191fb
+	golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
 	golang.org/x/net v0.0.0-20210525063256-abc453219eb5
 	google.golang.org/genproto v0.0.0-20210105202744-fe13368bc0e1 // indirect
 	google.golang.org/grpc v1.38.0
-	google.golang.org/protobuf v1.26.0
+	google.golang.org/protobuf v1.27.1
 	gopkg.in/ini.v1 v1.62.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 )
diff --git a/auth/callback.go b/auth/callback.go
index 4a5fba05b7e74335fd71bac6685db01126ae2527..e3d53fb47f4aa868ee23b27c757dd9d05145bb39 100644
--- a/auth/callback.go
+++ b/auth/callback.go
@@ -13,8 +13,10 @@ import (
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/interfaces"
 	"gitlab.com/elixxir/client/interfaces/message"
+	"gitlab.com/elixxir/client/interfaces/preimage"
 	"gitlab.com/elixxir/client/stoppable"
 	"gitlab.com/elixxir/client/storage/auth"
+	"gitlab.com/elixxir/client/storage/edge"
 	"gitlab.com/elixxir/crypto/contact"
 	"gitlab.com/elixxir/crypto/cyclic"
 	"gitlab.com/elixxir/crypto/diffieHellman"
@@ -313,6 +315,38 @@ func (m *Manager) doConfirm(sr *auth.SentRequest, grp *cyclic.Group,
 			sr.GetPartner(), err)
 	}
 
+	//remove the confirm fingerprint
+	fp := sr.GetFingerprint()
+	if err := m.storage.GetEdge().Remove(edge.Preimage{
+		Data:   preimage.Generate(fp[:], preimage.Confirm),
+		Type:   preimage.Confirm,
+		Source: sr.GetPartner()[:],
+	}, m.storage.GetUser().ReceptionID); err != nil {
+		jww.WARN.Printf("Failed delete the preimage for confirm from %s: %+v",
+			sr.GetPartner(), err)
+	}
+
+	//add the e2e and rekey firngeprints
+	//e2e
+	sessionPartner, err := m.storage.E2e().GetPartner(sr.GetPartner())
+	if err != nil {
+		jww.FATAL.Panicf("Cannot find %s right after creating: %+v", sr.GetPartner(), err)
+	}
+	me := m.storage.GetUser().ReceptionID
+
+	m.storage.GetEdge().Add(edge.Preimage{
+		Data:   sessionPartner.GetE2EPreimage(),
+		Type:   preimage.E2e,
+		Source: sr.GetPartner()[:],
+	}, me)
+
+	//rekey
+	m.storage.GetEdge().Add(edge.Preimage{
+		Data:   sessionPartner.GetRekeyPreimage(),
+		Type:   preimage.Rekey,
+		Source: sr.GetPartner()[:],
+	}, me)
+
 	// delete the in progress negotiation
 	// this undoes the request lock
 	if err := m.storage.Auth().Delete(sr.GetPartner()); err != nil {
diff --git a/auth/confirm.go b/auth/confirm.go
index f13d3871d9669b52c66924cfb7e4f17bc89b080c..62216ed28935abe65db07f3fe6c313f3bd273b0c 100644
--- a/auth/confirm.go
+++ b/auth/confirm.go
@@ -13,7 +13,9 @@ import (
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/interfaces"
 	"gitlab.com/elixxir/client/interfaces/params"
+	"gitlab.com/elixxir/client/interfaces/preimage"
 	"gitlab.com/elixxir/client/storage"
+	"gitlab.com/elixxir/client/storage/edge"
 	"gitlab.com/elixxir/crypto/contact"
 	"gitlab.com/elixxir/crypto/diffieHellman"
 	cAuth "gitlab.com/elixxir/crypto/e2e/auth"
@@ -87,6 +89,7 @@ func ConfirmRequestAuth(partner contact.Contact, rng io.Reader,
 
 	//get the fingerprint from the old ownership proof
 	fp := cAuth.MakeOwnershipProofFP(storedContact.OwnershipProof)
+	preimg := preimage.Generate(fp[:], preimage.Confirm)
 
 	//final construction
 	baseFmt.SetEcrPayload(ecrPayload)
@@ -114,6 +117,27 @@ func ConfirmRequestAuth(partner contact.Contact, rng io.Reader,
 		events.Report(10, "Auth", "SendConfirmError", em)
 	}
 
+	//add the preimages
+	sessionPartner, err := storage.E2e().GetPartner(partner.ID)
+	if err != nil {
+		jww.FATAL.Panicf("Cannot find %s right after creating: %+v", partner.ID, err)
+	}
+	me := storage.GetUser().ReceptionID
+
+	//e2e
+	storage.GetEdge().Add(edge.Preimage{
+		Data:   sessionPartner.GetE2EPreimage(),
+		Type:   preimage.E2e,
+		Source: partner.ID[:],
+	}, me)
+
+	//rekey
+	storage.GetEdge().Add(edge.Preimage{
+		Data:   sessionPartner.GetRekeyPreimage(),
+		Type:   preimage.Rekey,
+		Source: partner.ID[:],
+	}, me)
+
 	// delete the in progress negotiation
 	// this unlocks the request lock
 	//fixme - do these deletes at a later date
@@ -126,8 +150,10 @@ func ConfirmRequestAuth(partner contact.Contact, rng io.Reader,
 	jww.INFO.Printf("Confirming Auth with %s, msgDigest: %s",
 		partner.ID, cmixMsg.Digest())
 
+	param := params.GetDefaultCMIX()
+	param.IdentityPreimage = preimg
 	/*send message*/
-	round, _, err := net.SendCMIX(cmixMsg, partner.ID, params.GetDefaultCMIX())
+	round, _, err := net.SendCMIX(cmixMsg, partner.ID, param)
 	if err != nil {
 		// if the send fails just set it to failed, it will but automatically
 		// retried
diff --git a/auth/request.go b/auth/request.go
index 1be24089150fd7ef7dcce0c6ffaaffb1324bab2e..d323eeec7de5b9d734b25a6bfccac8a327a0c10d 100644
--- a/auth/request.go
+++ b/auth/request.go
@@ -13,9 +13,11 @@ import (
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/interfaces"
 	"gitlab.com/elixxir/client/interfaces/params"
+	"gitlab.com/elixxir/client/interfaces/preimage"
 	"gitlab.com/elixxir/client/storage"
 	"gitlab.com/elixxir/client/storage/auth"
 	"gitlab.com/elixxir/client/storage/e2e"
+	"gitlab.com/elixxir/client/storage/edge"
 	"gitlab.com/elixxir/crypto/contact"
 	"gitlab.com/elixxir/crypto/cyclic"
 	"gitlab.com/elixxir/crypto/diffieHellman"
@@ -141,6 +143,12 @@ func RequestAuth(partner, me contact.Contact, message string, rng io.Reader,
 	cmixMsg.SetMac(mac)
 	cmixMsg.SetContents(baseFmt.Marshal())
 
+	storage.GetEdge().Add(edge.Preimage{
+		Data:   preimage.Generate(confirmFp[:], preimage.Confirm),
+		Type:   preimage.Confirm,
+		Source: partner.ID[:],
+	}, me.ID)
+
 	jww.TRACE.Printf("RequestAuth SALT: %v", salt)
 	jww.TRACE.Printf("RequestAuth ECRPAYLOAD: %v", baseFmt.GetEcrPayload())
 	jww.TRACE.Printf("RequestAuth MAC: %v", mac)
@@ -160,8 +168,9 @@ func RequestAuth(partner, me contact.Contact, message string, rng io.Reader,
 		partner.ID, cmixMsg.Digest())
 
 	/*send message*/
-	round, _, err := net.SendCMIX(cmixMsg, partner.ID,
-		params.GetDefaultCMIX())
+	p := params.GetDefaultCMIX()
+	p.IdentityPreimage = preimage.GenerateRequest(partner.ID)
+	round, _, err := net.SendCMIX(cmixMsg, partner.ID, p)
 	if err != nil {
 		// if the send fails just set it to failed, it will
 		// but automatically retried
diff --git a/bindings/dummy.go b/bindings/dummy.go
new file mode 100644
index 0000000000000000000000000000000000000000..8cbaa84dc6a26f8b28765b5334b5be1bd6fe5df3
--- /dev/null
+++ b/bindings/dummy.go
@@ -0,0 +1,29 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package bindings
+
+import (
+	"gitlab.com/elixxir/client/dummy"
+	"time"
+)
+
+// StartDummyTraffic starts sending dummy traffic. The maxNumMessages is the
+// upper bound of the random number of messages sent each send. avgSendDeltaMS
+// is the average duration, in milliseconds, to wait between sends. Sends occur
+// every avgSendDeltaMS +/- a random duration with an upper bound of
+// randomRangeMS.
+func StartDummyTraffic(client *Client, maxNumMessages, avgSendDeltaMS,
+	randomRangeMS int) error {
+	avgSendDelta := time.Duration(avgSendDeltaMS) * time.Millisecond
+	randomRange := time.Duration(randomRangeMS) * time.Millisecond
+
+	m := dummy.NewManager(
+		maxNumMessages, avgSendDelta, randomRange, &client.api)
+
+	return client.api.AddService(m.StartDummyTraffic)
+}
diff --git a/bindings/group.go b/bindings/group.go
index 2df75cdd1519618135db887d28cef16d14a12b29..729aac8781d24258e61a077b01ee4eb5a43baec6 100644
--- a/bindings/group.go
+++ b/bindings/group.go
@@ -8,11 +8,14 @@
 package bindings
 
 import (
+	"encoding/json"
+	"fmt"
 	"github.com/pkg/errors"
 	gc "gitlab.com/elixxir/client/groupChat"
 	gs "gitlab.com/elixxir/client/groupChat/groupStore"
 	"gitlab.com/elixxir/crypto/group"
 	"gitlab.com/xx_network/primitives/id"
+	"time"
 )
 
 // GroupChat object contains the group chat manager.
@@ -61,9 +64,13 @@ func NewGroupManager(client *Client, requestFunc GroupRequestFunc,
 // MakeGroup creates a new group and sends a group request to all members in the
 // group. The ID of the new group, the rounds the requests were sent on, and the
 // status of the send are contained in NewGroupReport.
-func (g *GroupChat) MakeGroup(membership *IdList, name, message []byte) (*NewGroupReport, error) {
+func (g *GroupChat) MakeGroup(membership *IdList, name, message []byte)*NewGroupReport {
 	grp, rounds, status, err := g.m.MakeGroup(membership.list, name, message)
-	return &NewGroupReport{&Group{grp}, rounds, status}, err
+	errStr := ""
+	if err !=nil{
+		errStr = err.Error()
+	}
+	return &NewGroupReport{&Group{grp}, rounds, status, errStr}
 }
 
 // ResendRequest resends a group request to all members in the group. The rounds
@@ -75,9 +82,18 @@ func (g *GroupChat) ResendRequest(groupIdBytes []byte) (*NewGroupReport, error)
 			errors.Errorf("Failed to unmarshal group ID: %+v", err)
 	}
 
+	grp, exists := g.m.GetGroup(groupID)
+	if !exists{
+		return nil,errors.Errorf("Failed to find group %s", groupID)
+	}
+
 	rounds, status, err := g.m.ResendRequest(groupID)
 
-	return &NewGroupReport{&Group{}, rounds, status}, nil
+	errStr := ""
+	if err !=nil{
+		errStr = err.Error()
+	}
+	return &NewGroupReport{&Group{grp}, rounds, status, errStr}, nil
 }
 
 // JoinGroup allows a user to join a group when they receive a request. The
@@ -102,14 +118,14 @@ func (g *GroupChat) LeaveGroup(groupIdBytes []byte) error {
 
 // Send sends the message to the specified group. Returns the round the messages
 // were sent on.
-func (g *GroupChat) Send(groupIdBytes, message []byte) (int64, error) {
+func (g *GroupChat) Send(groupIdBytes, message []byte) (*GroupSendReport, error) {
 	groupID, err := id.Unmarshal(groupIdBytes)
 	if err != nil {
-		return 0, errors.Errorf("Failed to unmarshal group ID: %+v", err)
+		return nil, errors.Errorf("Failed to unmarshal group ID: %+v", err)
 	}
 
-	round, err := g.m.Send(groupID, message)
-	return int64(round), err
+	round, timestamp, err := g.m.Send(groupID, message)
+	return &GroupSendReport{round, timestamp}, err
 }
 
 // GetGroups returns an IdList containing a list of group IDs that the user is a
@@ -139,6 +155,10 @@ func (g *GroupChat) NumGroups() int {
 	return g.m.NumGroups()
 }
 
+////
+// NewGroupReport Structure
+////
+
 // NewGroupReport is returned when creating a new group and contains the ID of
 // the group, a list of rounds that the group requests were sent on, and the
 // status of the send.
@@ -146,6 +166,13 @@ type NewGroupReport struct {
 	group  *Group
 	rounds []id.Round
 	status gc.RequestStatus
+	err string
+}
+
+type GroupReportDisk struct {
+	List []id.Round
+	GrpId  []byte
+	Status int
 }
 
 // GetGroup returns the Group.
@@ -161,13 +188,76 @@ func (ngr *NewGroupReport) GetRoundList() *RoundList {
 
 // GetStatus returns the status of the requests sent when creating a new group.
 // status = 0   an error occurred before any requests could be sent
-//          1   all requests failed to send
-//          2   some request failed and some succeeded
-//          3,  all requests sent successfully
+//          1   all requests failed to send (call Resend Group)
+//          2   some request failed and some succeeded (call Resend Group)
+//          3,  all requests sent successfully (call Resend Group)
 func (ngr *NewGroupReport) GetStatus() int {
 	return int(ngr.status)
 }
 
+// GetError returns the string of an error.
+// Will be an empty string if no error occured
+func (ngr *NewGroupReport) GetError() string {
+	return ngr.err
+}
+
+
+func (ngr *NewGroupReport) Marshal() ([]byte, error) {
+	grpReportDisk := GroupReportDisk{
+		List: ngr.rounds,
+		GrpId:  ngr.group.GetID()[:],
+		Status: ngr.GetStatus(),
+	}
+	return json.Marshal(&grpReportDisk)
+}
+
+func (ngr *NewGroupReport) Unmarshal(b []byte) error {
+	grpReportDisk := GroupReportDisk{}
+	if err := json.Unmarshal(b, &grpReportDisk); err != nil {
+		return errors.New(fmt.Sprintf("Failed to unmarshal group "+
+			"report: %s", err.Error()))
+	}
+
+	grpId, err := id.Unmarshal(grpReportDisk.GrpId)
+	if err != nil {
+		return errors.New(fmt.Sprintf("Failed to unmarshal group "+
+			"id: %s", err.Error()))
+	}
+
+	ngr.group.g.ID = grpId
+	ngr.rounds = grpReportDisk.List
+	ngr.status = gc.RequestStatus(grpReportDisk.Status)
+
+	return nil
+}
+
+////
+// NewGroupReport Structure
+////
+
+// GroupSendReport is returned when sending a group message. It contains the
+// round ID sent on and the timestamp of the send.
+type GroupSendReport struct {
+	roundID   id.Round
+	timestamp time.Time
+}
+
+// GetRoundID returns the ID of the round that the send occurred on.
+func (gsr *GroupSendReport) GetRoundID() int64 {
+	return int64(gsr.roundID)
+}
+
+// GetTimestampNano returns the timestamp of the send in nanoseconds.
+func (gsr *GroupSendReport) GetTimestampNano() int64 {
+	return gsr.timestamp.UnixNano()
+}
+
+// GetTimestampMS returns the timestamp of the send in milliseconds.
+func (gsr *GroupSendReport) GetTimestampMS() int64 {
+	ts := uint64(gsr.timestamp.UnixNano()) / uint64(time.Millisecond)
+	return int64(ts)
+}
+
 ////
 // Group Structure
 ////
@@ -188,6 +278,24 @@ func (g *Group) GetID() []byte {
 	return g.g.ID.Bytes()
 }
 
+// GetInitMessage returns initial message sent with the group request.
+func (g *Group) GetInitMessage() []byte {
+	return g.g.InitMessage
+}
+
+// GetCreatedNano returns the time the group was created in nanoseconds. This is
+// also the time the group requests were sent.
+func (g *Group) GetCreatedNano() int64 {
+	return g.g.Created.UnixNano()
+}
+
+// GetCreatedMS returns the time the group was created in milliseconds. This is
+// also the time the group requests were sent.
+func (g *Group) GetCreatedMS() int64 {
+	ts := uint64(g.g.Created.UnixNano()) / uint64(time.Millisecond)
+	return int64(ts)
+}
+
 // GetMembership returns a list of contacts, one for each member in the group.
 // The list is in order; the first contact is the leader/creator of the group.
 // All subsequent members are ordered by their ID.
@@ -291,7 +399,8 @@ func (gmr *GroupMessageReceive) GetTimestampNano() int64 {
 
 // GetTimestampMS returns the message timestamp in milliseconds.
 func (gmr *GroupMessageReceive) GetTimestampMS() int64 {
-	return gmr.Timestamp.UnixNano() / 1_000_000
+	ts := uint64(gmr.Timestamp.UnixNano()) / uint64(time.Millisecond)
+	return int64(ts)
 }
 
 // GetRoundID returns the ID of the round the message was sent on.
@@ -308,5 +417,6 @@ func (gmr *GroupMessageReceive) GetRoundTimestampNano() int64 {
 // GetRoundTimestampMS returns the timestamp, in milliseconds, of the round the
 // message was sent on.
 func (gmr *GroupMessageReceive) GetRoundTimestampMS() int64 {
-	return gmr.RoundTimestamp.UnixNano() / 1_000_000
+	ts := uint64(gmr.RoundTimestamp.UnixNano()) / uint64(time.Millisecond)
+	return int64(ts)
 }
diff --git a/bindings/notifications.go b/bindings/notifications.go
index 461f15c324d55c144372161157e2d76cb0108869..2274b096a5bb6b3969419f14538707243180ad23 100644
--- a/bindings/notifications.go
+++ b/bindings/notifications.go
@@ -9,26 +9,64 @@ package bindings
 
 import (
 	"encoding/base64"
+	"encoding/json"
 	"github.com/pkg/errors"
+	"gitlab.com/elixxir/client/storage/edge"
 	"gitlab.com/elixxir/crypto/fingerprint"
-	"gitlab.com/xx_network/primitives/id"
 )
 
+type NotificationForMeReport struct {
+	forMe  bool
+	tYpe   string
+	source []byte
+}
+
+func (nfmr *NotificationForMeReport) ForMe() bool {
+	return nfmr.forMe
+}
+
+func (nfmr *NotificationForMeReport) Type() string {
+	return nfmr.tYpe
+}
+
+func (nfmr *NotificationForMeReport) Source() []byte {
+	return nfmr.source
+}
+
 // NotificationForMe Check if a notification received is for me
-func NotificationForMe(messageHash, idFP string, receptionId []byte) (bool, error) {
+func NotificationForMe(messageHash, idFP string, preimages string) (*NotificationForMeReport, error) {
+	//handle message hash and idFP
 	messageHashBytes, err := base64.StdEncoding.DecodeString(messageHash)
 	if err != nil {
-		return false, errors.WithMessage(err, "Failed to decode message ID")
+		return nil, errors.WithMessage(err, "Failed to decode message ID")
 	}
 	idFpBytes, err := base64.StdEncoding.DecodeString(idFP)
 	if err != nil {
-		return false, errors.WithMessage(err, "Failed to decode identity fingerprint")
+		return nil, errors.WithMessage(err, "Failed to decode identity fingerprint")
 	}
-	rid, err := id.Unmarshal(receptionId)
-	if err != nil {
-		return false, errors.WithMessage(err, "Failed to unmartial reception ID")
+
+	//handle deserialization of preimages
+	var preimageList []edge.Preimage
+	if err := json.Unmarshal([]byte(preimages), &preimageList); err != nil {
+		return nil, errors.WithMessagef(err, "Failed to unmarshal the preimages list, "+
+			"cannot check if notification is for me")
+	}
+
+	//check if any preimages match with the passed in data
+	for _, preimage := range preimageList {
+		if fingerprint.CheckIdentityFpFromMessageHash(idFpBytes, messageHashBytes, preimage.Data) {
+			return &NotificationForMeReport{
+				forMe:  true,
+				tYpe:   preimage.Type,
+				source: preimage.Source,
+			}, nil
+		}
 	}
-	return fingerprint.CheckIdentityFpFromMessageHash(idFpBytes, messageHashBytes, rid), nil
+	return &NotificationForMeReport{
+		forMe:  false,
+		tYpe:   "",
+		source: nil,
+	}, nil
 }
 
 // RegisterForNotifications accepts firebase messaging token
diff --git a/bindings/notifications_test.go b/bindings/notifications_test.go
index c30e6e175566905a59b60ef4a570adcca77af3f9..3d8f78d33206de6e6eeffa25c2a973f8605970d1 100644
--- a/bindings/notifications_test.go
+++ b/bindings/notifications_test.go
@@ -1,23 +1,21 @@
 package bindings
 
 import (
-	"encoding/base64"
-	"gitlab.com/elixxir/crypto/fingerprint"
-	"gitlab.com/xx_network/primitives/id"
 	"testing"
 )
 
+// FIXME: this test needs to be fixed
 func TestNotificationForMe(t *testing.T) {
-	payload := []byte("I'm a payload")
-	hash := fingerprint.GetMessageHash(payload)
-	rid := id.NewIdFromString("zezima", id.User, t)
-	fp := fingerprint.IdentityFP(payload, rid)
-
-	ok, err := NotificationForMe(base64.StdEncoding.EncodeToString(hash), base64.StdEncoding.EncodeToString(fp), rid.Bytes())
-	if err != nil {
-		t.Errorf("Failed to check notification: %+v", err)
-	}
-	if !ok {
-		t.Error("Should have gotten ok response")
-	}
+	// payload := []byte("I'm a payload")
+	// hash := fingerprint.GetMessageHash(payload)
+	// rid := id.NewIdFromString("zezima", id.User, t)
+	// fp := fingerprint.IdentityFP(payload, rid)
+	//
+	// ok, err := NotificationForMe(base64.StdEncoding.EncodeToString(hash), base64.StdEncoding.EncodeToString(fp), rid.Bytes())
+	// if err != nil {
+	// 	t.Errorf("Failed to check notification: %+v", err)
+	// }
+	// if !ok {
+	// 	t.Error("Should have gotten ok response")
+	// }
 }
diff --git a/bindings/preimage.go b/bindings/preimage.go
new file mode 100644
index 0000000000000000000000000000000000000000..3117369675d4ec73c1454446bf056645be77c96d
--- /dev/null
+++ b/bindings/preimage.go
@@ -0,0 +1,54 @@
+package bindings
+
+import (
+	"encoding/json"
+	"github.com/pkg/errors"
+	"gitlab.com/xx_network/primitives/id"
+)
+
+type PreimageNotification interface {
+	Notify(identity []byte, deleted bool)
+}
+
+func (c *Client) RegisterPreimageCallback(identity []byte, pin PreimageNotification) {
+
+	iid := &id.ID{}
+	copy(iid[:], identity)
+
+	cb := func(localIdentity *id.ID, deleted bool) {
+		pin.Notify(localIdentity[:], deleted)
+	}
+
+	c.api.GetStorage().GetEdge().AddUpdateCallback(iid, cb)
+}
+
+func (c *Client) GetPreimages(identity []byte) (string, error) {
+
+	iid := &id.ID{}
+	copy(iid[:], identity)
+
+	list, exist := c.api.GetStorage().GetEdge().Get(iid)
+	if !exist {
+		return "", errors.Errorf("Could not find a preimage list for %s", iid)
+	}
+
+	marshaled, err := json.Marshal(&list)
+
+	return string(marshaled), err
+}
+
+// hack on getPreimages so it works on iOS per https://github.com/golang/go/issues/46893
+func (c *Client) GetPreimagesHack(dummy string, identity []byte) (string, error) {
+
+	iid := &id.ID{}
+	copy(iid[:], identity)
+
+	list, exist := c.api.GetStorage().GetEdge().Get(iid)
+	if !exist {
+		return "", errors.Errorf("Could not find a preimage list for %s", iid)
+	}
+
+	marshaled, err := json.Marshal(&list)
+
+	return string(marshaled), err
+}
diff --git a/bindings/ud.go b/bindings/ud.go
index 15b0e012a7ffaad5b290da54b832bcffdf39fa57..727f4118333c2813c8acd227000deffab78ad6e3 100644
--- a/bindings/ud.go
+++ b/bindings/ud.go
@@ -8,6 +8,7 @@
 package bindings
 
 import (
+	"fmt"
 	"github.com/pkg/errors"
 	"gitlab.com/elixxir/client/ud"
 	"gitlab.com/elixxir/crypto/contact"
@@ -167,7 +168,7 @@ func (ud UserDiscovery) SearchSingle(f string, callback SingleSearchCallback,
 	return ud.ud.Search([]fact.Fact{fObj}, cb, timeout)
 }
 
-// SingleSearchCallback returns the result of a single search
+// LookupCallback returns the result of a single lookup
 type LookupCallback interface {
 	Callback(contact *Contact, error string)
 }
@@ -200,3 +201,95 @@ func (ud UserDiscovery) Lookup(idBytes []byte, callback LookupCallback,
 	return ud.ud.Lookup(uid, cb, timeout)
 
 }
+
+// MultiLookupCallback returns the result of many paralel lookups
+type MultiLookupCallback interface {
+	Callback(Succeeded *ContactList, failed *IdList, errors string)
+}
+
+type lookupResponse struct {
+	C     contact.Contact
+	err   error
+	index int
+	id    *id.ID
+}
+
+// MultiLookup Looks for the contact object associated with all given userIDs.
+// The ids are the byte representation of an id stored in an IDList object.
+// This will reject if that id is malformed or if the indexing on the IDList
+// object is wrong. The MultiLookupCallback will return with all contacts
+// returned within the timeout.
+func (ud UserDiscovery) MultiLookup(ids *IdList, callback MultiLookupCallback,
+	timeoutMS int) error {
+
+	idList := make([]*id.ID, 0, ids.Len())
+
+	//extract all IDs from
+	for i := 0; i < ids.Len(); i++ {
+		idBytes, err := ids.Get(i)
+		if err != nil {
+			return errors.WithMessagef(err, "Failed to get ID at index %d", i)
+		}
+		uid, err := id.Unmarshal(idBytes)
+		if err != nil {
+			return errors.WithMessagef(err, "Failed to lookup due to "+
+				"malformed id at index %d", i)
+		}
+		idList = append(idList, uid)
+	}
+
+	//make the channels for the requests
+	results := make(chan lookupResponse, len(idList))
+
+	timeout := time.Duration(timeoutMS) * time.Millisecond
+
+	//loop through the IDs and send the lookup
+	for i := range idList {
+		locali := i
+		localID := idList[locali]
+		cb := func(c contact.Contact, err error) {
+			results <- lookupResponse{
+				C:     c,
+				err:   err,
+				index: locali,
+				id:    localID,
+			}
+		}
+
+		go func() {
+			err := ud.ud.Lookup(localID, cb, timeout)
+			if err != nil {
+				results <- lookupResponse{
+					C: contact.Contact{},
+					err: errors.WithMessagef(err, "Failed to send lookup "+
+						"for user %s[%d]", localID, locali),
+					index: locali,
+					id:    localID,
+				}
+			}
+		}()
+	}
+
+	//run the result gathering in its own thread
+	go func() {
+		returnedContactList := make([]contact.Contact, 0, len(idList))
+		failedIDList := make([]*id.ID, 0, len(idList))
+		var concatonatedErrs string
+
+		//Get the responses and return
+		for numReturned := 0; numReturned < len(idList); numReturned++ {
+			response := <-results
+			if response.err == nil {
+				returnedContactList = append(returnedContactList, response.C)
+			} else {
+				failedIDList = append(failedIDList, response.id)
+				concatonatedErrs = concatonatedErrs + fmt.Sprintf("Error returned from "+
+					"send to %d [%d]:%+v\t", response.id, response.index, response.err)
+			}
+		}
+
+		callback.Callback(&ContactList{list: returnedContactList}, &IdList{list: failedIDList}, concatonatedErrs)
+	}()
+
+	return nil
+}
diff --git a/cmd/group.go b/cmd/group.go
index f1e4508a72e2ce6c0c29b836e67e7b0bef8d91d9..b973fb5c7f2b7addcb12a26c2a59b7b4c1524c22 100644
--- a/cmd/group.go
+++ b/cmd/group.go
@@ -154,7 +154,8 @@ func createGroup(name, msg []byte, filePath string, gm *groupChat.Manager) {
 
 	// Integration grabs the group ID from this line
 	jww.INFO.Printf("NewGroupID: b64:%s", grp.ID)
-	jww.INFO.Printf("Created Group: Requests:%s on rounds %#v, %v", status, rids, grp)
+	jww.INFO.Printf("Created Group: Requests:%s on rounds %#v, %v",
+		status, rids, grp)
 	fmt.Printf("Created new group with name %q and message %q\n", grp.Name,
 		grp.InitMessage)
 }
@@ -168,13 +169,15 @@ func resendRequests(groupIdString string, gm *groupChat.Manager) {
 			groupID, err)
 	}
 
-	jww.INFO.Printf("Resending requests to group %s: %v, %s", groupID, rids, status)
+	jww.INFO.Printf("Resending requests to group %s: %v, %s",
+		groupID, rids, status)
 	fmt.Println("Resending group requests to group.")
 }
 
 // joinGroup joins a group when a request is received on the group request
 // channel.
-func joinGroup(reqChan chan groupStore.Group, timeout time.Duration, gm *groupChat.Manager) {
+func joinGroup(reqChan chan groupStore.Group, timeout time.Duration,
+	gm *groupChat.Manager) {
 	jww.INFO.Print("Waiting for group request to be received.")
 	fmt.Println("Waiting for group request to be received.")
 
@@ -215,18 +218,20 @@ func sendGroup(groupIdString string, msg []byte, gm *groupChat.Manager) {
 
 	jww.INFO.Printf("Sending to group %s message %q", groupID, msg)
 
-	rid, err := gm.Send(groupID, msg)
+	rid, timestamp, err := gm.Send(groupID, msg)
 	if err != nil {
 		jww.FATAL.Panicf("Sending message to group %s: %+v", groupID, err)
 	}
 
-	jww.INFO.Printf("Sent to group %s on round %d", groupID, rid)
+	jww.INFO.Printf("Sent to group %s on round %d at %s",
+		groupID, rid, timestamp)
 	fmt.Printf("Sent message %q to group.\n", msg)
 }
 
 // messageWait waits for the given number of messages to be received on the
 // groupChat.MessageReceive channel.
-func messageWait(numMessages uint, timeout time.Duration, recChan chan groupChat.MessageReceive) {
+func messageWait(numMessages uint, timeout time.Duration,
+	recChan chan groupChat.MessageReceive) {
 	jww.INFO.Printf("Waiting for %d group message(s) to be received.", numMessages)
 	fmt.Printf("Waiting for %d group message(s) to be received.\n", numMessages)
 
@@ -263,7 +268,8 @@ func showGroup(groupIdString string, gm *groupChat.Manager) {
 	}
 
 	jww.INFO.Printf("Show group %#v", grp)
-	fmt.Printf("Got group with name %q and message %q\n", grp.Name, grp.InitMessage)
+	fmt.Printf("Got group with name %q and message %q\n",
+		grp.Name, grp.InitMessage)
 }
 
 // ReadLines returns each line in a file as a string.
@@ -272,7 +278,12 @@ func ReadLines(fileName string) []string {
 	if err != nil {
 		jww.FATAL.Panicf(err.Error())
 	}
-	defer file.Close()
+	defer func(file *os.File) {
+		err = file.Close()
+		if err != nil {
+			jww.FATAL.Panicf("Failed to close file: %+v", err)
+		}
+	}(file)
 
 	var res []string
 
diff --git a/cmd/root.go b/cmd/root.go
index 036ff66cf9fef36c45b79b114ce85a5de5774b59..a2c9db19265e702aec3c5d268185cd2156b3b8dd 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -23,6 +23,7 @@ import (
 	"gitlab.com/elixxir/client/switchboard"
 	"gitlab.com/elixxir/crypto/contact"
 	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/utils"
 	"io/ioutil"
 	"log"
 	"os"
@@ -236,7 +237,7 @@ var rootCmd = &cobra.Command{
 		expectedCnt := viper.GetUint("receiveCount")
 		receiveCnt := uint(0)
 		waitSecs := viper.GetUint("waitTimeout")
-		waitTimeout := time.Duration(waitSecs)* time.Second
+		waitTimeout := time.Duration(waitSecs) * time.Second
 		done := false
 
 		for !done && expectedCnt != 0 {
@@ -261,7 +262,7 @@ var rootCmd = &cobra.Command{
 
 		//wait an extra 5 seconds to make sure no messages were missed
 		done = false
-		timer := time.NewTimer(5*time.Second)
+		timer := time.NewTimer(5 * time.Second)
 		for !done {
 			select {
 			case <-timer.C:
@@ -382,6 +383,8 @@ func createClient() *api.Client {
 	regCode := viper.GetString("regcode")
 	precannedID := viper.GetUint("sendid")
 	userIDprefix := viper.GetString("userid-prefix")
+	protoUserPath := viper.GetString("protoUserPath")
+
 	//create a new client if none exist
 	if _, err := os.Stat(storeDir); os.IsNotExist(err) {
 		// Load NDF
@@ -394,14 +397,19 @@ func createClient() *api.Client {
 		if precannedID != 0 {
 			err = api.NewPrecannedClient(precannedID,
 				string(ndfJSON), storeDir, []byte(pass))
-		} else {
-			if userIDprefix != "" {
-				err = api.NewVanityClient(string(ndfJSON), storeDir,
-					[]byte(pass), regCode, userIDprefix)
-			} else {
-				err = api.NewClient(string(ndfJSON), storeDir,
-					[]byte(pass), regCode)
+		} else if protoUserPath != "" {
+			protoUserJson, err := utils.ReadFile(protoUserPath)
+			if err != nil {
+				jww.FATAL.Panicf("%v", err)
 			}
+			err = api.NewProtoClient_Unsafe(string(ndfJSON), storeDir,
+				[]byte(pass), protoUserJson)
+		} else if userIDprefix != "" {
+			err = api.NewVanityClient(string(ndfJSON), storeDir,
+				[]byte(pass), regCode, userIDprefix)
+		} else {
+			err = api.NewClient(string(ndfJSON), storeDir,
+				[]byte(pass), regCode)
 		}
 
 		if err != nil {
@@ -431,7 +439,7 @@ func initClient() *api.Client {
 
 	pass := viper.GetString("password")
 	storeDir := viper.GetString("session")
-
+	jww.DEBUG.Printf("sessionDur: %v", storeDir)
 	netParams := params.GetDefaultNetwork()
 	netParams.E2EParams.MinKeys = uint16(viper.GetUint("e2eMinKeys"))
 	netParams.E2EParams.MaxKeys = uint16(viper.GetUint("e2eMaxKeys"))
@@ -453,6 +461,20 @@ func initClient() *api.Client {
 		jww.FATAL.Panicf("%+v", err)
 	}
 
+	if protoUser := viper.GetString("protoUserOut"); protoUser != "" {
+
+		jsonBytes, err := client.ConstructProtoUerFile()
+		if err != nil {
+			jww.FATAL.Panicf("Failed to construct proto user file: %v", err)
+		}
+
+		err = utils.WriteFileDef(protoUser, jsonBytes)
+		if err != nil {
+			jww.FATAL.Panicf("Failed to write proto user to file: %v", err)
+		}
+
+	}
+
 	return client
 }
 
@@ -823,30 +845,30 @@ func init() {
 	rootCmd.Flags().UintP("receiveCount",
 		"", 1, "How many messages we should wait for before quitting")
 	viper.BindPFlag("receiveCount", rootCmd.Flags().Lookup("receiveCount"))
-	rootCmd.Flags().UintP("waitTimeout", "", 15,
+	rootCmd.PersistentFlags().UintP("waitTimeout", "", 15,
 		"The number of seconds to wait for messages to arrive")
 	viper.BindPFlag("waitTimeout",
-		rootCmd.Flags().Lookup("waitTimeout"))
+		rootCmd.PersistentFlags().Lookup("waitTimeout"))
 
 	rootCmd.Flags().BoolP("unsafe", "", false,
 		"Send raw, unsafe messages without e2e encryption.")
 	viper.BindPFlag("unsafe", rootCmd.Flags().Lookup("unsafe"))
 
-	rootCmd.Flags().BoolP("unsafe-channel-creation", "", false,
+	rootCmd.PersistentFlags().BoolP("unsafe-channel-creation", "", false,
 		"Turns off the user identity authenticated channel check, "+
 			"automatically approving authenticated channels")
 	viper.BindPFlag("unsafe-channel-creation",
-		rootCmd.Flags().Lookup("unsafe-channel-creation"))
+		rootCmd.PersistentFlags().Lookup("unsafe-channel-creation"))
 
 	rootCmd.Flags().BoolP("accept-channel", "", false,
 		"Accept the channel request for the corresponding recipient ID")
 	viper.BindPFlag("accept-channel",
 		rootCmd.Flags().Lookup("accept-channel"))
 
-	rootCmd.Flags().Bool("delete-channel", false,
+	rootCmd.PersistentFlags().Bool("delete-channel", false,
 		"Delete the channel information for the corresponding recipient ID")
 	viper.BindPFlag("delete-channel",
-		rootCmd.Flags().Lookup("delete-channel"))
+		rootCmd.PersistentFlags().Lookup("delete-channel"))
 
 	rootCmd.Flags().BoolP("send-auth-request", "", false,
 		"Send an auth request to the specified destination and wait"+
@@ -893,6 +915,17 @@ func init() {
 	rootCmd.Flags().String("profile-cpu", "",
 		"Enable cpu profiling to this file")
 	viper.BindPFlag("profile-cpu", rootCmd.Flags().Lookup("profile-cpu"))
+
+	// Proto user flags
+	rootCmd.Flags().String("protoUserPath", "",
+		"Path to proto user JSON file containing cryptographic primitives "+
+			"the client will load")
+	viper.BindPFlag("protoUserPath", rootCmd.Flags().Lookup("protoUserPath"))
+	rootCmd.Flags().String("protoUserOut", "",
+		"Path to which a normally constructed client "+
+			"will write proto user JSON file")
+	viper.BindPFlag("protoUserOut", rootCmd.Flags().Lookup("protoUserOut"))
+
 }
 
 // initConfig reads in config file and ENV variables if set.
diff --git a/cmd/single.go b/cmd/single.go
index 6600cbfbbf9b9358d79163c555096127609379d2..27300ef1e6adb9965cc79682890df1ad6f0f7914 100644
--- a/cmd/single.go
+++ b/cmd/single.go
@@ -89,7 +89,7 @@ var singleCmd = &cobra.Command{
 			jww.FATAL.Panicf("Could not add single use process: %+v", err)
 		}
 
-		for numReg, total := 1, 100; numReg < total; {
+		for numReg, total := 1, 100; numReg < (total*3)/4; {
 			time.Sleep(1 * time.Second)
 			numReg, total, err = client.GetNodeRegistrationStatus()
 			if err != nil {
diff --git a/cmd/version.go b/cmd/version.go
index 51a4bb42caeb6f5ac9da3a38fb47eceabc6faf7f..e9a3316a3f579f281100f728ff96ca3a1f24b7e8 100644
--- a/cmd/version.go
+++ b/cmd/version.go
@@ -18,7 +18,7 @@ import (
 )
 
 // Change this value to set the version for this build
-const currentVersion = "2.10.0"
+const currentVersion = "3.2.0"
 
 func Version() string {
 	out := fmt.Sprintf("Elixxir Client v%s -- %s\n\n", api.SEMVER,
diff --git a/dummy/manager.go b/dummy/manager.go
new file mode 100644
index 0000000000000000000000000000000000000000..edf0b3ac3d1ece2e8fda7ab81376d9b162241a8d
--- /dev/null
+++ b/dummy/manager.go
@@ -0,0 +1,75 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+// Package dummy allows for the sending of dummy messages to dummy recipients
+// via SendCmix at randomly generated intervals.
+
+package dummy
+
+import (
+	"gitlab.com/elixxir/client/api"
+	"gitlab.com/elixxir/client/interfaces"
+	"gitlab.com/elixxir/client/stoppable"
+	"gitlab.com/elixxir/client/storage"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	"time"
+)
+
+const (
+	dummyTrafficStoppableName = "DummyTraffic"
+)
+
+// Manager manages the sending of dummy messages.
+type Manager struct {
+	// The maximum number of messages to send each send
+	maxNumMessages int
+
+	// Average duration to wait between message sends
+	avgSendDelta time.Duration
+
+	// Upper limit for random duration that modified avgSendDelta
+	randomRange time.Duration
+
+	// Client interfaces
+	client *api.Client
+	store  *storage.Session
+	net    interfaces.NetworkManager
+	rng    *fastRNG.StreamGenerator
+}
+
+// NewManager creates a new dummy Manager with the specified average send delta
+// and the range used for generating random durations.
+func NewManager(maxNumMessages int, avgSendDelta, randomRange time.Duration,
+	client *api.Client) *Manager {
+	return newManager(maxNumMessages, avgSendDelta, randomRange, client,
+		client.GetStorage(), client.GetNetworkInterface(), client.GetRng())
+}
+
+// newManager builds a new dummy Manager from fields explicitly passed in. This
+// function is a helper function for NewManager to make it easier to test.
+func newManager(maxNumMessages int, avgSendDelta, randomRange time.Duration,
+	client *api.Client, store *storage.Session, net interfaces.NetworkManager,
+	rng *fastRNG.StreamGenerator) *Manager {
+	return &Manager{
+		maxNumMessages: maxNumMessages,
+		avgSendDelta:   avgSendDelta,
+		randomRange:    randomRange,
+		client:         client,
+		store:          store,
+		net:            net,
+		rng:            rng,
+	}
+}
+
+// StartDummyTraffic starts the process of sending dummy traffic. This function
+// matches the api.Service type.
+func (m *Manager) StartDummyTraffic() (stoppable.Stoppable, error) {
+	stop := stoppable.NewSingle(dummyTrafficStoppableName)
+	go m.sendThread(stop)
+
+	return stop, nil
+}
diff --git a/dummy/manager_test.go b/dummy/manager_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..7f8ec99d037dd14576951237abb6113518d89ab9
--- /dev/null
+++ b/dummy/manager_test.go
@@ -0,0 +1,84 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package dummy
+
+import (
+	"reflect"
+	"testing"
+	"time"
+)
+
+// Tests that newManager returns the expected Manager.
+func Test_newManager(t *testing.T) {
+	expected := &Manager{
+		maxNumMessages: 10,
+		avgSendDelta:   time.Minute,
+		randomRange:    time.Second,
+	}
+
+	received := newManager(expected.maxNumMessages, expected.avgSendDelta,
+		expected.randomRange, nil, nil, nil, nil)
+
+	if !reflect.DeepEqual(expected, received) {
+		t.Errorf("New manager does not match expected."+
+			"\nexpected: %+v\nreceived: %+v", expected, received)
+	}
+}
+
+// Tests that Manager.StartDummyTraffic sends dummy messages and that it stops
+// when the stoppable is closed.
+func TestManager_StartDummyTraffic(t *testing.T) {
+	m := newTestManager(10, 50*time.Millisecond, 10*time.Millisecond, false, t)
+
+	stop, err := m.StartDummyTraffic()
+	if err != nil {
+		t.Errorf("StartDummyTraffic returned an error: %+v", err)
+	}
+
+	msgChan := make(chan bool)
+	go func() {
+		for m.net.(*testNetworkManager).GetMsgListLen() == 0 {
+			time.Sleep(5 * time.Millisecond)
+		}
+		msgChan <- true
+	}()
+
+	var numReceived int
+	select {
+	case <-time.NewTimer(3 * m.avgSendDelta).C:
+		t.Errorf("Timed out after %s waiting for messages to be sent.",
+			3*m.avgSendDelta)
+	case <-msgChan:
+		numReceived += m.net.(*testNetworkManager).GetMsgListLen()
+	}
+
+	err = stop.Close()
+	if err != nil {
+		t.Errorf("Failed to close stoppable: %+v", err)
+	}
+
+	time.Sleep(10 * time.Millisecond)
+	if !stop.IsStopped() {
+		t.Error("Stoppable never stopped.")
+	}
+
+	msgChan = make(chan bool)
+	go func() {
+		for m.net.(*testNetworkManager).GetMsgListLen() == numReceived {
+			time.Sleep(5 * time.Millisecond)
+		}
+		msgChan <- true
+	}()
+
+	select {
+	case <-time.NewTimer(3 * m.avgSendDelta).C:
+
+	case <-msgChan:
+		t.Error("Received new messages after stoppable was stopped.")
+	}
+}
diff --git a/dummy/random.go b/dummy/random.go
new file mode 100644
index 0000000000000000000000000000000000000000..2327ddf6c7d9b978ecf6e07e34f53b733155c87e
--- /dev/null
+++ b/dummy/random.go
@@ -0,0 +1,87 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package dummy
+
+import (
+	"encoding/binary"
+	"github.com/pkg/errors"
+	"gitlab.com/elixxir/primitives/format"
+	"gitlab.com/xx_network/crypto/csprng"
+	"time"
+) // Error messages.
+const (
+	payloadSizeRngErr = "failed to generate random payload size: %+v"
+)
+
+// intRng returns, as an int, a non-negative, non-zero random number in [1, n)
+// from the csprng.Source.
+func intRng(n int, rng csprng.Source) (int, error) {
+	v, err := csprng.Generate(8, rng)
+	if err != nil {
+		return 0, err
+	}
+
+	return int(binary.LittleEndian.Uint64(v)%uint64(n-1)) + 1, nil
+}
+
+// durationRng returns a duration that is the base duration plus or minus a
+// random duration of max randomRange.
+func durationRng(base, randomRange time.Duration, rng csprng.Source) (
+	time.Duration, error) {
+	delta, err := intRng(int(2*randomRange), rng)
+	if err != nil {
+		return 0, err
+	}
+
+	return base + randomRange - time.Duration(delta), nil
+}
+
+// newRandomPayload generates a random payload of a random length.
+func newRandomPayload(maxPayloadSize int, rng csprng.Source) ([]byte, error) {
+	// Generate random payload size
+	randomPayloadSize, err := intRng(maxPayloadSize, rng)
+	if err != nil {
+		return nil, errors.Errorf(payloadSizeRngErr, err)
+	}
+
+	randomMsg, err := csprng.Generate(randomPayloadSize, rng)
+	if err != nil {
+		return nil, err
+	}
+
+	return randomMsg, nil
+}
+
+// newRandomFingerprint generates a random format.Fingerprint.
+func newRandomFingerprint(rng csprng.Source) (format.Fingerprint, error) {
+	fingerprintBytes, err := csprng.Generate(format.KeyFPLen, rng)
+	if err != nil {
+		return format.Fingerprint{}, err
+	}
+
+	// Create new fingerprint from bytes
+	fingerprint := format.NewFingerprint(fingerprintBytes)
+
+	// Set the first bit to be 0 to comply with the cMix group
+	fingerprint[0] &= 0x7F
+
+	return fingerprint, nil
+}
+
+// newRandomMAC generates a random MAC.
+func newRandomMAC(rng csprng.Source) ([]byte, error) {
+	mac, err := csprng.Generate(format.MacLen, rng)
+	if err != nil {
+		return nil, err
+	}
+
+	// Set the first bit to be 0 to comply with the cMix group
+	mac[0] &= 0x7F
+
+	return mac, nil
+}
diff --git a/dummy/random_test.go b/dummy/random_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..661986a0416993e211209d009a023c451dd3ff60
--- /dev/null
+++ b/dummy/random_test.go
@@ -0,0 +1,188 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package dummy
+
+import (
+	"encoding/base64"
+	"testing"
+	"time"
+)
+
+// Consistency test: tests that intRng returns the expected int when using a
+// PRNG and that the result is not larger than the max.
+func Test_intRng_Consistency(t *testing.T) {
+	expectedInts := []int{15, 1, 35, 13, 42, 52, 57, 3, 48}
+
+	prng := NewPrng(42)
+	max := 64
+
+	for i, expected := range expectedInts {
+		v, err := intRng(max, prng)
+		if err != nil {
+			t.Errorf("intRng returned an error (%d): %+v", i, err)
+		}
+
+		if v != expected {
+			t.Errorf("New int #%d does not match expected."+
+				"\nexpected: %d\nreceived: %d", i, expected, v)
+		}
+
+		// Ensure that the int is in range
+		if v > max || v < 1 {
+			t.Errorf("Int #%d not within range."+
+				"\nexpected: %d < d < %d\nreceived: %d", i, 0, max, v)
+		}
+	}
+}
+
+// Consistency test: tests that durationRng returns the expected int when using
+// a PRNG and that the result is within the allowed range.
+func Test_durationRng_Consistency(t *testing.T) {
+	expectedDurations := []time.Duration{
+		61460632462, 69300060600, 46066982720, 68493307162, 45820762465,
+		56472560211, 68610237306, 45503877311, 63543617747,
+	}
+
+	prng := NewPrng(42)
+	base, randomRange := time.Minute, 15*time.Second
+
+	for i, expected := range expectedDurations {
+		v, err := durationRng(base, randomRange, prng)
+		if err != nil {
+			t.Errorf("durationRng returned an error (%d): %+v", i, err)
+		}
+
+		if v != expected {
+			t.Errorf("New duration #%d does not match expected."+
+				"\nexpected: %s\nreceived: %s", i, expected, v)
+		}
+
+		// Ensure that the duration is within range
+		if v > base+randomRange || v < base-randomRange {
+			t.Errorf("Duration #%d is not in range."+
+				"\nexpected: %s < d < %s\nreceived: %s", i, base-randomRange,
+				base+randomRange, v)
+		}
+	}
+}
+
+// Consistency test: tests that newRandomPayload returns the expected payload
+// when using a PRNG and that the result is not larger than the max payload.
+func Test_newRandomPayload_Consistency(t *testing.T) {
+	expectedPayloads := []string{
+		"l7ufS7Ry6J9bFITyUgnJ",
+		"Ut/Xm012Qpthegyfnw07pVsMwNYUTIiFNQ==",
+		"CD9h",
+		"GSnh",
+		"joE=",
+		"uoQ+6NY+jE/+HOvqVG2PrBPdGqwEzi6ih3xVec+ix44bC6+uiBuCpw==",
+		"qkNGWnhiBhaXiu0M48bE8657w+BJW1cS/v2+DBAoh+EA2s0tiF9pLLYH2gChHBxwcec=",
+		"suEpcF4nPwXJIyaCjisFbg==",
+		"R/3zREEO1MEWAj+o41drb+0n/4l0usDK/ZrQVpKxNhnnOJZN/ceejVNDc2Yc/WbXTw==",
+		"bkt1IQ==",
+	}
+
+	prng := NewPrng(42)
+	maxPayloadSize := 64
+
+	for i, expected := range expectedPayloads {
+		payload, err := newRandomPayload(maxPayloadSize, prng)
+		if err != nil {
+			t.Errorf("newRandomPayload returned an error (%d): %+v", i, err)
+		}
+
+		payloadString := base64.StdEncoding.EncodeToString(payload)
+
+		if payloadString != expected {
+			t.Errorf("New payload #%d does not match expected."+
+				"\nexpected: %s\nreceived: %s", i, expected, payloadString)
+		}
+
+		// Ensure that the payload is not larger than the max size
+		if len(payload) > maxPayloadSize {
+			t.Errorf("Length of payload #%d longer than max allowed."+
+				"\nexpected: <%d\nreceived: %d", i, maxPayloadSize, len(payload))
+		}
+	}
+}
+
+// Consistency test: tests that newRandomFingerprint returns the expected
+// fingerprints when using a PRNG. Also tests that the first bit is zero.
+func Test_newRandomFingerprint_Consistency(t *testing.T) {
+	expectedFingerprints := []string{
+		"U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVI=",
+		"X9ebTXZCm2F6DJ+fDTulWwzA1hRMiIU1hBrL4HCbB1g=",
+		"CD9h03W8ArQd9PkZKeGP2p5vguVOdI6B555LvW/jTNw=",
+		"OoQ+6NY+jE/+HOvqVG2PrBPdGqwEzi6ih3xVec+ix44=",
+		"GwuvrogbgqdREIpC7TyQPKpDRlp4YgYWl4rtDOPGxPM=",
+		"LnvD4ElbVxL+/b4MECiH4QDazS2IX2kstgfaAKEcHHA=",
+		"ceeWotwtwlpbdLLhKXBeJz8FySMmgo4rBW44F2WOEGE=",
+		"SYlH/fNEQQ7UwRYCP6jjV2tv7Sf/iXS6wMr9mtBWkrE=",
+		"NhnnOJZN/ceejVNDc2Yc/WbXT+weG4lJGrcjbkt1IWI=",
+		"EM8r60LDyicyhWDxqsBnzqbov0bUqytGgEAsX7KCDog=",
+	}
+
+	prng := NewPrng(42)
+
+	for i, expected := range expectedFingerprints {
+		fp, err := newRandomFingerprint(prng)
+		if err != nil {
+			t.Errorf("newRandomFingerprint returned an error (%d): %+v", i, err)
+		}
+
+		if fp.String() != expected {
+			t.Errorf("New fingerprint #%d does not match expected."+
+				"\nexpected: %s\nreceived: %s", i, expected, fp)
+		}
+
+		// Ensure that the first bit is zero
+		if fp[0]>>7 != 0 {
+			t.Errorf("First bit of fingerprint #%d is not 0."+
+				"\nexpected: %d\nreceived: %d", i, 0, fp[0]>>7)
+		}
+	}
+}
+
+// Consistency test: tests that newRandomMAC returns the expected MAC when using
+// a PRNG. Also tests that the first bit is zero.
+func Test_newRandomMAC_Consistency(t *testing.T) {
+	expectedMACs := []string{
+		"U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVI=",
+		"X9ebTXZCm2F6DJ+fDTulWwzA1hRMiIU1hBrL4HCbB1g=",
+		"CD9h03W8ArQd9PkZKeGP2p5vguVOdI6B555LvW/jTNw=",
+		"OoQ+6NY+jE/+HOvqVG2PrBPdGqwEzi6ih3xVec+ix44=",
+		"GwuvrogbgqdREIpC7TyQPKpDRlp4YgYWl4rtDOPGxPM=",
+		"LnvD4ElbVxL+/b4MECiH4QDazS2IX2kstgfaAKEcHHA=",
+		"ceeWotwtwlpbdLLhKXBeJz8FySMmgo4rBW44F2WOEGE=",
+		"SYlH/fNEQQ7UwRYCP6jjV2tv7Sf/iXS6wMr9mtBWkrE=",
+		"NhnnOJZN/ceejVNDc2Yc/WbXT+weG4lJGrcjbkt1IWI=",
+		"EM8r60LDyicyhWDxqsBnzqbov0bUqytGgEAsX7KCDog=",
+	}
+
+	prng := NewPrng(42)
+
+	for i, expected := range expectedMACs {
+		mac, err := newRandomMAC(prng)
+		if err != nil {
+			t.Errorf("newRandomMAC returned an error (%d): %+v", i, err)
+		}
+
+		macString := base64.StdEncoding.EncodeToString(mac)
+
+		if macString != expected {
+			t.Errorf("New MAC #%d does not match expected."+
+				"\nexpected: %s\nreceived: %s", i, expected, macString)
+		}
+
+		// Ensure that the first bit is zero
+		if mac[0]>>7 != 0 {
+			t.Errorf("First bit of MAC #%d is not 0."+
+				"\nexpected: %d\nreceived: %d", i, 0, mac[0]>>7)
+		}
+	}
+}
diff --git a/dummy/send.go b/dummy/send.go
new file mode 100644
index 0000000000000000000000000000000000000000..f37f79927402b86f59606d901c0f2d393ff41b05
--- /dev/null
+++ b/dummy/send.go
@@ -0,0 +1,173 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package dummy
+
+import (
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/interfaces/params"
+	"gitlab.com/elixxir/client/stoppable"
+	"gitlab.com/elixxir/primitives/format"
+	"gitlab.com/xx_network/crypto/csprng"
+	"gitlab.com/xx_network/primitives/id"
+	"sync"
+	"sync/atomic"
+	"time"
+)
+
+// Error messages.
+const (
+	numMsgsRngErr     = "failed to generate random number of messages to send: %+v"
+	payloadRngErr     = "failed to generate random payload: %+v"
+	recipientRngErr   = "failed to generate random recipient: %+v"
+	fingerprintRngErr = "failed to generate random fingerprint: %+v"
+	macRngErr         = "failed to generate random MAC: %+v"
+)
+
+// sendThread is a thread that sends the dummy messages at random intervals.
+func (m *Manager) sendThread(stop *stoppable.Single) {
+	jww.DEBUG.Print("Starting dummy traffic sending thread.")
+
+	timer := m.randomTimer()
+
+	for {
+		select {
+		case <-stop.Quit():
+			jww.DEBUG.Print("Stopping dummy traffic sending thread: stoppable " +
+				"triggered")
+			stop.ToStopped()
+			return
+		case <-timer.C:
+			timer = m.randomTimer()
+
+			// Get list of random messages and recipients
+			rng := m.rng.GetStream()
+			msgs, err := m.newRandomMessages(rng)
+			if err != nil {
+				jww.FATAL.Panicf("Failed to generate dummy messages: %+v", err)
+			}
+			rng.Close()
+
+			err = m.sendMessages(msgs)
+			if err != nil {
+				jww.FATAL.Panicf("Failed to send dummy messages: %+v", err)
+			}
+		}
+	}
+}
+
+// sendMessages generates and sends random messages.
+func (m *Manager) sendMessages(msgs map[id.ID]format.Message) error {
+	var sent, i int64
+	var wg sync.WaitGroup
+
+	for recipient, msg := range msgs {
+		wg.Add(1)
+
+		go func(i int64, recipient id.ID, msg format.Message) {
+			//fill the preiamge with random data to ensure it isnt repeatable
+			p := params.GetDefaultCMIX()
+			p.IdentityPreimage = make([]byte, 32)
+			rng := m.rng.GetStream()
+			if _, err := rng.Read(p.IdentityPreimage); err != nil {
+				jww.FATAL.Panicf("Failed to generate data for random "+
+					"identity preimage in e2e send: %+v", err)
+			}
+			rng.Close()
+			_, _, err := m.net.SendCMIX(msg, &recipient, p)
+			if err != nil {
+				jww.WARN.Printf("failed to send dummy message %d/%d: %+v",
+					i, len(msgs), err)
+			} else {
+				atomic.AddInt64(&sent, 1)
+			}
+
+			wg.Done()
+		}(i, recipient, msg)
+
+		i++
+	}
+
+	wg.Wait()
+
+	jww.INFO.Printf("Sent %d/%d dummy messages.", sent, len(msgs))
+
+	return nil
+}
+
+// newRandomMessages returns a map of a random recipients and random messages of
+// a randomly generated length in [1, Manager.maxNumMessages].
+func (m *Manager) newRandomMessages(rng csprng.Source) (
+	map[id.ID]format.Message, error) {
+	numMessages, err := intRng(m.maxNumMessages+1, rng)
+	if err != nil {
+		return nil, errors.Errorf(numMsgsRngErr, err)
+	}
+
+	msgs := make(map[id.ID]format.Message, numMessages)
+
+	for i := 0; i < numMessages; i++ {
+		// Generate random recipient
+		recipient, err := id.NewRandomID(rng, id.User)
+		if err != nil {
+			return nil, errors.Errorf(recipientRngErr, err)
+		}
+
+		msgs[*recipient], err = m.newRandomCmixMessage(rng)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return msgs, nil
+}
+
+// newRandomCmixMessage returns a new cMix message filled with a randomly
+// generated payload, fingerprint, and MAC.
+func (m *Manager) newRandomCmixMessage(rng csprng.Source) (format.Message, error) {
+	// Create new empty cMix message
+	cMixMsg := format.NewMessage(m.store.Cmix().GetGroup().GetP().ByteLen())
+
+	// Generate random message
+	randomMsg, err := newRandomPayload(cMixMsg.ContentsSize(), rng)
+	if err != nil {
+		return format.Message{}, errors.Errorf(payloadRngErr, err)
+	}
+
+	// Generate random fingerprint
+	fingerprint, err := newRandomFingerprint(rng)
+	if err != nil {
+		return format.Message{}, errors.Errorf(fingerprintRngErr, err)
+	}
+
+	// Generate random MAC
+	mac, err := newRandomMAC(rng)
+	if err != nil {
+		return format.Message{}, errors.Errorf(macRngErr, err)
+	}
+
+	// Set contents, fingerprint, and MAC, of the cMix message
+	cMixMsg.SetContents(randomMsg)
+	cMixMsg.SetKeyFP(fingerprint)
+	cMixMsg.SetMac(mac)
+
+	return cMixMsg, nil
+}
+
+// randomTimer generates a timer that will trigger after a random duration.
+func (m *Manager) randomTimer() *time.Timer {
+	rng := m.rng.GetStream()
+
+	duration, err := durationRng(m.avgSendDelta, m.randomRange, rng)
+	if err != nil {
+		jww.FATAL.Panicf("Failed to generate random duration to wait to send "+
+			"dummy messages: %+v", err)
+	}
+
+	return time.NewTimer(duration)
+}
diff --git a/dummy/send_test.go b/dummy/send_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..acf08cb558ba02fbf711537ce67a696afa455836
--- /dev/null
+++ b/dummy/send_test.go
@@ -0,0 +1,169 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package dummy
+
+import (
+	"bytes"
+	"encoding/base64"
+	"gitlab.com/elixxir/client/stoppable"
+	"gitlab.com/elixxir/primitives/format"
+	"gitlab.com/xx_network/primitives/id"
+	"reflect"
+	"testing"
+	"time"
+)
+
+// Tests that Manager.sendThread sends multiple sets of messages.
+func TestManager_sendThread(t *testing.T) {
+	m := newTestManager(10, 50*time.Millisecond, 10*time.Millisecond, false, t)
+
+	stop := stoppable.NewSingle("sendThreadTest")
+	go m.sendThread(stop)
+
+	msgChan := make(chan bool, 10)
+	go func() {
+		var numReceived int
+		for i := 0; i < 2; i++ {
+			for m.net.(*testNetworkManager).GetMsgListLen() == numReceived {
+				time.Sleep(5 * time.Millisecond)
+			}
+			numReceived = m.net.(*testNetworkManager).GetMsgListLen()
+			msgChan <- true
+		}
+	}()
+
+	var numReceived int
+	select {
+	case <-time.NewTimer(3 * m.avgSendDelta).C:
+		t.Errorf("Timed out after %s waiting for messages to be sent.",
+			3*m.avgSendDelta)
+	case <-msgChan:
+		numReceived += m.net.(*testNetworkManager).GetMsgListLen()
+	}
+
+	select {
+	case <-time.NewTimer(3 * m.avgSendDelta).C:
+		t.Errorf("Timed out after %s waiting for messages to be sent.",
+			3*m.avgSendDelta)
+	case <-msgChan:
+		if m.net.(*testNetworkManager).GetMsgListLen() <= numReceived {
+			t.Errorf("Failed to receive second send."+
+				"\nmessages on last receive: %d\nmessages on this receive: %d",
+				numReceived, m.net.(*testNetworkManager).GetMsgListLen())
+		}
+	}
+
+	err := stop.Close()
+	if err != nil {
+		t.Errorf("Failed to close stoppable: %+v", err)
+	}
+
+	time.Sleep(10 * time.Millisecond)
+	if !stop.IsStopped() {
+		t.Error("Stoppable never stopped.")
+	}
+}
+
+// Tests that Manager.sendMessages sends all the messages with the correct
+// recipient.
+func TestManager_sendMessages(t *testing.T) {
+	m := newTestManager(100, 0, 0, false, t)
+	prng := NewPrng(42)
+
+	// Generate map of recipients and messages
+	msgs := make(map[id.ID]format.Message, m.maxNumMessages)
+	for i := 0; i < m.maxNumMessages; i++ {
+		recipient, err := id.NewRandomID(prng, id.User)
+		if err != nil {
+			t.Errorf("Failed to generate random recipient ID (%d): %+v", i, err)
+		}
+
+		msg, err := m.newRandomCmixMessage(prng)
+		if err != nil {
+			t.Errorf("Failed to generate random cMix message (%d): %+v", i, err)
+		}
+
+		msgs[*recipient] = msg
+	}
+
+	// Send the messages
+	err := m.sendMessages(msgs)
+	if err != nil {
+		t.Errorf("sendMessages returned an error: %+v", err)
+	}
+
+	// Get sent messages
+	receivedMsgs := m.net.(*testNetworkManager).GetMsgList()
+
+	// Test that all messages were received
+	if len(receivedMsgs) != len(msgs) {
+		t.Errorf("Failed to received all sent messages."+
+			"\nexpected: %d\nreceived: %d", len(msgs), len(receivedMsgs))
+	}
+
+	// Test that all messages were received for the correct recipient
+	for recipient, msg := range msgs {
+		receivedMsg, exists := receivedMsgs[recipient]
+		if !exists {
+			t.Errorf("Failed to receive message from %s: %+v", &recipient, msg)
+		} else if !reflect.DeepEqual(msg, receivedMsg) {
+			t.Errorf("Received unexpected message for recipient %s."+
+				"\nexpected: %+v\nreceived: %+v", &recipient, msg, receivedMsg)
+		}
+	}
+}
+
+// Tests that Manager.newRandomMessages creates a non-empty map of messages and
+// that each message is unique.
+func TestManager_newRandomMessages(t *testing.T) {
+	m := newTestManager(10, 0, 0, false, t)
+	prng := NewPrng(42)
+
+	msgMap, err := m.newRandomMessages(prng)
+	if err != nil {
+		t.Errorf("newRandomMessages returned an error: %+v", err)
+	}
+
+	if len(msgMap) == 0 {
+		t.Error("Message map is empty.")
+	}
+
+	marshalledMsgs := make(map[string]format.Message, len(msgMap))
+	for _, msg := range msgMap {
+		msgString := base64.StdEncoding.EncodeToString(msg.Marshal())
+		if _, exists := marshalledMsgs[msgString]; exists {
+			t.Errorf("Message not unique.")
+		} else {
+			marshalledMsgs[msgString] = msg
+		}
+	}
+}
+
+// Tests that Manager.newRandomCmixMessage generates a cMix message with
+// populated contents, fingerprint, and MAC.
+func TestManager_newRandomCmixMessage(t *testing.T) {
+	m := newTestManager(0, 0, 0, false, t)
+	prng := NewPrng(42)
+
+	cMixMsg, err := m.newRandomCmixMessage(prng)
+	if err != nil {
+		t.Errorf("newRandomCmixMessage returned an error: %+v", err)
+	}
+
+	if bytes.Equal(cMixMsg.GetContents(), make([]byte, len(cMixMsg.GetContents()))) {
+		t.Error("cMix message contents not set.")
+	}
+
+	if cMixMsg.GetKeyFP() == (format.Fingerprint{}) {
+		t.Error("cMix message fingerprint not set.")
+	}
+
+	if bytes.Equal(cMixMsg.GetMac(), make([]byte, format.MacLen)) {
+		t.Error("cMix message MAC not set.")
+	}
+}
diff --git a/dummy/utils_test.go b/dummy/utils_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..7312effd4d1d7b9d138e467d764f08fd5ddbd401
--- /dev/null
+++ b/dummy/utils_test.go
@@ -0,0 +1,208 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package dummy
+
+import (
+	"github.com/pkg/errors"
+	"gitlab.com/elixxir/client/interfaces"
+	"gitlab.com/elixxir/client/interfaces/message"
+	"gitlab.com/elixxir/client/interfaces/params"
+	"gitlab.com/elixxir/client/network/gateway"
+	"gitlab.com/elixxir/client/stoppable"
+	"gitlab.com/elixxir/client/storage"
+	"gitlab.com/elixxir/comms/network"
+	"gitlab.com/elixxir/crypto/e2e"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	"gitlab.com/elixxir/primitives/format"
+	"gitlab.com/xx_network/comms/connect"
+	"gitlab.com/xx_network/crypto/csprng"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/id/ephemeral"
+	"gitlab.com/xx_network/primitives/ndf"
+	"io"
+	"math/rand"
+	"sync"
+	"testing"
+	"time"
+)
+
+////////////////////////////////////////////////////////////////////////////////
+// PRNG                                                                       //
+////////////////////////////////////////////////////////////////////////////////
+
+// Prng is a PRNG that satisfies the csprng.Source interface.
+type Prng struct{ prng io.Reader }
+
+func NewPrng(seed int64) csprng.Source     { return &Prng{rand.New(rand.NewSource(seed))} }
+func (s *Prng) Read(b []byte) (int, error) { return s.prng.Read(b) }
+func (s *Prng) SetSeed([]byte) error       { return nil }
+
+////////////////////////////////////////////////////////////////////////////////
+// Test Managers                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+// newTestManager creates a new Manager that has groups stored for testing. One
+// of the groups in the list is also returned.
+func newTestManager(maxNumMessages int, avgSendDelta, randomRange time.Duration,
+	sendErr bool, t *testing.T) *Manager {
+	m := &Manager{
+		maxNumMessages: maxNumMessages,
+		avgSendDelta:   avgSendDelta,
+		randomRange:    randomRange,
+		store:          storage.InitTestingSession(t),
+		net:            newTestNetworkManager(sendErr, t),
+		rng:            fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG),
+	}
+
+	return m
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test Network Manager                                                       //
+////////////////////////////////////////////////////////////////////////////////
+
+// testNetworkManager is a test implementation of NetworkManager interface.
+type testNetworkManager struct {
+	instance *network.Instance
+	messages map[id.ID]format.Message
+	sendErr  bool
+	sync.RWMutex
+}
+
+func newTestNetworkManager(sendErr bool, t *testing.T) interfaces.NetworkManager {
+	instanceComms := &connect.ProtoComms{
+		Manager: connect.NewManagerTesting(t),
+	}
+
+	thisInstance, err := network.NewInstanceTesting(instanceComms, getNDF(),
+		getNDF(), nil, nil, t)
+	if err != nil {
+		t.Fatalf("Failed to create new test instance: %v", err)
+	}
+
+	return &testNetworkManager{
+		instance: thisInstance,
+		messages: make(map[id.ID]format.Message),
+		sendErr:  sendErr,
+	}
+}
+
+func (tnm *testNetworkManager) GetMsgListLen() int {
+	tnm.RLock()
+	defer tnm.RUnlock()
+	return len(tnm.messages)
+}
+
+func (tnm *testNetworkManager) GetMsgList() map[id.ID]format.Message {
+	tnm.RLock()
+	defer tnm.RUnlock()
+	return tnm.messages
+}
+
+func (tnm *testNetworkManager) GetMsg(recipient id.ID) format.Message {
+	tnm.RLock()
+	defer tnm.RUnlock()
+	return tnm.messages[recipient]
+}
+
+func (tnm *testNetworkManager) SendE2E(message.Send, params.E2E, *stoppable.Single) (
+	[]id.Round, e2e.MessageID, time.Time, error) {
+	return nil, e2e.MessageID{}, time.Time{}, nil
+}
+
+func (tnm *testNetworkManager) SendUnsafe(message.Send, params.Unsafe) ([]id.Round, error) {
+	return []id.Round{}, nil
+}
+
+func (tnm *testNetworkManager) SendCMIX(message format.Message,
+	recipient *id.ID, _ params.CMIX) (id.Round, ephemeral.Id, error) {
+	tnm.Lock()
+	defer tnm.Unlock()
+
+	if tnm.sendErr {
+		return 0, ephemeral.Id{}, errors.New("SendCMIX error")
+	}
+
+	tnm.messages[*recipient] = message
+
+	return 0, ephemeral.Id{}, nil
+}
+
+func (tnm *testNetworkManager) SendManyCMIX(map[id.ID]format.Message, params.CMIX) (
+	id.Round, []ephemeral.Id, error) {
+	return 0, nil, nil
+}
+
+type dummyEventMgr struct{}
+
+func (d *dummyEventMgr) Report(int, string, string, string) {}
+func (tnm *testNetworkManager) GetEventManager() interfaces.EventManager {
+	return &dummyEventMgr{}
+}
+
+func (tnm *testNetworkManager) GetInstance() *network.Instance             { return tnm.instance }
+func (tnm *testNetworkManager) GetHealthTracker() interfaces.HealthTracker { return nil }
+func (tnm *testNetworkManager) Follow(interfaces.ClientErrorReport) (stoppable.Stoppable, error) {
+	return nil, nil
+}
+func (tnm *testNetworkManager) CheckGarbledMessages()        {}
+func (tnm *testNetworkManager) InProgressRegistrations() int { return 0 }
+func (tnm *testNetworkManager) GetSender() *gateway.Sender   { return nil }
+func (tnm *testNetworkManager) GetAddressSize() uint8        { return 0 }
+func (tnm *testNetworkManager) RegisterAddressSizeNotification(string) (chan uint8, error) {
+	return nil, nil
+}
+func (tnm *testNetworkManager) UnregisterAddressSizeNotification(string) {}
+func (tnm *testNetworkManager) SetPoolFilter(gateway.Filter)             {}
+func (tnm *testNetworkManager) GetVerboseRounds() string                 { return "" }
+
+////////////////////////////////////////////////////////////////////////////////
+// NDF Primes                                                                 //
+////////////////////////////////////////////////////////////////////////////////
+
+func getNDF() *ndf.NetworkDefinition {
+	return &ndf.NetworkDefinition{
+		E2E: ndf.Group{
+			Prime: "E2EE983D031DC1DB6F1A7A67DF0E9A8E5561DB8E8D49413394C049B7A" +
+				"8ACCEDC298708F121951D9CF920EC5D146727AA4AE535B0922C688B55B3D" +
+				"D2AEDF6C01C94764DAB937935AA83BE36E67760713AB44A6337C20E78615" +
+				"75E745D31F8B9E9AD8412118C62A3E2E29DF46B0864D0C951C394A5CBBDC" +
+				"6ADC718DD2A3E041023DBB5AB23EBB4742DE9C1687B5B34FA48C3521632C" +
+				"4A530E8FFB1BC51DADDF453B0B2717C2BC6669ED76B4BDD5C9FF558E88F2" +
+				"6E5785302BEDBCA23EAC5ACE92096EE8A60642FB61E8F3D24990B8CB12EE" +
+				"448EEF78E184C7242DD161C7738F32BF29A841698978825B4111B4BC3E1E" +
+				"198455095958333D776D8B2BEEED3A1A1A221A6E37E664A64B83981C46FF" +
+				"DDC1A45E3D5211AAF8BFBC072768C4F50D7D7803D2D4F278DE8014A47323" +
+				"631D7E064DE81C0C6BFA43EF0E6998860F1390B5D3FEACAF1696015CB79C" +
+				"3F9C2D93D961120CD0E5F12CBB687EAB045241F96789C38E89D796138E63" +
+				"19BE62E35D87B1048CA28BE389B575E994DCA755471584A09EC723742DC3" +
+				"5873847AEF49F66E43873",
+			Generator: "2",
+		},
+		CMIX: ndf.Group{
+			Prime: "9DB6FB5951B66BB6FE1E140F1D2CE5502374161FD6538DF1648218642" +
+				"F0B5C48C8F7A41AADFA187324B87674FA1822B00F1ECF8136943D7C55757" +
+				"264E5A1A44FFE012E9936E00C1D3E9310B01C7D179805D3058B2A9F4BB6F" +
+				"9716BFE6117C6B5B3CC4D9BE341104AD4A80AD6C94E005F4B993E14F091E" +
+				"B51743BF33050C38DE235567E1B34C3D6A5C0CEAA1A0F368213C3D19843D" +
+				"0B4B09DCB9FC72D39C8DE41F1BF14D4BB4563CA28371621CAD3324B6A2D3" +
+				"92145BEBFAC748805236F5CA2FE92B871CD8F9C36D3292B5509CA8CAA77A" +
+				"2ADFC7BFD77DDA6F71125A7456FEA153E433256A2261C6A06ED3693797E7" +
+				"995FAD5AABBCFBE3EDA2741E375404AE25B",
+			Generator: "5C7FF6B06F8F143FE8288433493E4769C4D988ACE5BE25A0E2480" +
+				"9670716C613D7B0CEE6932F8FAA7C44D2CB24523DA53FBE4F6EC3595892D" +
+				"1AA58C4328A06C46A15662E7EAA703A1DECF8BBB2D05DBE2EB956C142A33" +
+				"8661D10461C0D135472085057F3494309FFA73C611F78B32ADBB5740C361" +
+				"C9F35BE90997DB2014E2EF5AA61782F52ABEB8BD6432C4DD097BC5423B28" +
+				"5DAFB60DC364E8161F4A2A35ACA3A10B1C4D203CC76A470A33AFDCBDD929" +
+				"59859ABD8B56E1725252D78EAC66E71BA9AE3F1DD2487199874393CD4D83" +
+				"2186800654760E1E34C09E4D155179F9EC0DC4473F996BDCE6EED1CABED8" +
+				"B6F116F7AD9CF505DF0F998E34AB27514B0FFE7",
+		},
+	}
+}
diff --git a/go.mod b/go.mod
index dbe4d79a58accf98ee34cc3b003b82eb7dcac69e..68b97ffd9c304c2e89affdbf7a8add44d2c1b344 100644
--- a/go.mod
+++ b/go.mod
@@ -17,10 +17,10 @@ require (
 	github.com/spf13/jwalterweatherman v1.1.0
 	github.com/spf13/viper v1.7.1
 	gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228
-	gitlab.com/elixxir/comms v0.0.4-0.20211014164523-495493efb970
-	gitlab.com/elixxir/crypto v0.0.7-0.20211017232951-ba1a65ee7b6e
+	gitlab.com/elixxir/comms v0.0.4-0.20211101174956-590ba1b47887
+	gitlab.com/elixxir/crypto v0.0.7-0.20211022013957-3a7899285c4c
 	gitlab.com/elixxir/ekv v0.1.5
-	gitlab.com/elixxir/primitives v0.0.3-0.20211014164029-06022665b576
+	gitlab.com/elixxir/primitives v0.0.3-0.20211102233208-a716d5c670b6
 	gitlab.com/xx_network/comms v0.0.4-0.20211014163953-e774276b83ae
 	gitlab.com/xx_network/crypto v0.0.5-0.20211014163843-57b345890686
 	gitlab.com/xx_network/primitives v0.0.4-0.20211014163031-53405cf191fb
@@ -28,7 +28,7 @@ require (
 	golang.org/x/net v0.0.0-20210525063256-abc453219eb5
 	google.golang.org/genproto v0.0.0-20210105202744-fe13368bc0e1 // indirect
 	google.golang.org/grpc v1.38.0
-	google.golang.org/protobuf v1.26.0
+	google.golang.org/protobuf v1.27.1
 	gopkg.in/ini.v1 v1.62.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 37d23fd0792d608122530d1b2d168f77b572f356..ac363b511e9b8e883b2d9431aa16e6e937535941 100644
--- a/go.sum
+++ b/go.sum
@@ -253,22 +253,21 @@ github.com/zeebo/pcg v1.0.0 h1:dt+dx+HvX8g7Un32rY9XWoYnd0NmKmrIzpHF7qiTDj0=
 github.com/zeebo/pcg v1.0.0/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
 gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228 h1:Gi6rj4mAlK0BJIk1HIzBVMjWNjIUfstrsXC2VqLYPcA=
 gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228/go.mod h1:H6jztdm0k+wEV2QGK/KYA+MY9nj9Zzatux/qIvDDv3k=
-gitlab.com/elixxir/comms v0.0.4-0.20211014164523-495493efb970 h1:mSf5KidH231esbVvL1rozvLXhgAHn8S+BV70k0oxlW8=
-gitlab.com/elixxir/comms v0.0.4-0.20211014164523-495493efb970/go.mod h1:L2fs1Me+L6SKyix7+Gyd9QKmBMjnyJPds/ikSPqdeNY=
+gitlab.com/elixxir/comms v0.0.4-0.20211101174956-590ba1b47887 h1:SOQaoEvc6RqImz86jSjsj7wIW3ZhgxXc38GzvRkKdOw=
+gitlab.com/elixxir/comms v0.0.4-0.20211101174956-590ba1b47887/go.mod h1:rQpTeFVSn08ocbQeEw5AbMhGWXHfXmQ0y1/ZprAIVVU=
 gitlab.com/elixxir/crypto v0.0.0-20200804182833-984246dea2c4/go.mod h1:ucm9SFKJo+K0N2GwRRpaNr+tKXMIOVWzmyUD0SbOu2c=
 gitlab.com/elixxir/crypto v0.0.3/go.mod h1:ZNgBOblhYToR4m8tj4cMvJ9UsJAUKq+p0gCp07WQmhA=
-gitlab.com/elixxir/crypto v0.0.7-0.20211014164205-95915de2ac0d h1:tI3YYoHVb/KViRhagzQM0XKdw0hJ7KcuSQXFIWhmtSE=
-gitlab.com/elixxir/crypto v0.0.7-0.20211014164205-95915de2ac0d/go.mod h1:teuTEXyqsqo4N/J1sshcTg9xYOt+wNTurop7pkZOiCg=
-gitlab.com/elixxir/crypto v0.0.7-0.20211017232951-ba1a65ee7b6e h1:JCYcXV9GBvOVRfYhm1e2b52AnvPQrEQLmOR9XbjzE8k=
-gitlab.com/elixxir/crypto v0.0.7-0.20211017232951-ba1a65ee7b6e/go.mod h1:teuTEXyqsqo4N/J1sshcTg9xYOt+wNTurop7pkZOiCg=
+gitlab.com/elixxir/crypto v0.0.7-0.20211022013957-3a7899285c4c h1:HIr2HBhZqSAKdPRBdEY0/qravISL619O2yuTY/DQTdo=
+gitlab.com/elixxir/crypto v0.0.7-0.20211022013957-3a7899285c4c/go.mod h1:teuTEXyqsqo4N/J1sshcTg9xYOt+wNTurop7pkZOiCg=
 gitlab.com/elixxir/ekv v0.1.5 h1:R8M1PA5zRU1HVnTyrtwybdABh7gUJSCvt1JZwUSeTzk=
 gitlab.com/elixxir/ekv v0.1.5/go.mod h1:e6WPUt97taFZe5PFLPb1Dupk7tqmDCTQu1kkstqJvw4=
 gitlab.com/elixxir/primitives v0.0.0-20200731184040-494269b53b4d/go.mod h1:OQgUZq7SjnE0b+8+iIAT2eqQF+2IFHn73tOo+aV11mg=
 gitlab.com/elixxir/primitives v0.0.0-20200804170709-a1896d262cd9/go.mod h1:p0VelQda72OzoUckr1O+vPW0AiFe0nyKQ6gYcmFSuF8=
 gitlab.com/elixxir/primitives v0.0.0-20200804182913-788f47bded40/go.mod h1:tzdFFvb1ESmuTCOl1z6+yf6oAICDxH2NPUemVgoNLxc=
 gitlab.com/elixxir/primitives v0.0.1/go.mod h1:kNp47yPqja2lHSiS4DddTvFpB/4D9dB2YKnw5c+LJCE=
-gitlab.com/elixxir/primitives v0.0.3-0.20211014164029-06022665b576 h1:sXX3/hewV4TQLxT2iKBfnfgW/A1eXoEfv5raJxTb79s=
 gitlab.com/elixxir/primitives v0.0.3-0.20211014164029-06022665b576/go.mod h1:zZy8AlOISFm5IG4G4sylypnz7xNBfZ5mpXiibqJT8+8=
+gitlab.com/elixxir/primitives v0.0.3-0.20211102233208-a716d5c670b6 h1:ymWyFBFLcRQiuSId54dq8PVeiV4W7a9737kV46Thjlk=
+gitlab.com/elixxir/primitives v0.0.3-0.20211102233208-a716d5c670b6/go.mod h1:zZy8AlOISFm5IG4G4sylypnz7xNBfZ5mpXiibqJT8+8=
 gitlab.com/xx_network/comms v0.0.0-20200805174823-841427dd5023/go.mod h1:owEcxTRl7gsoM8c3RQ5KAm5GstxrJp5tn+6JfQ4z5Hw=
 gitlab.com/xx_network/comms v0.0.4-0.20211014163953-e774276b83ae h1:jmZWmSm8eH40SX5B5uOw2XaYoHYqVn8daTfa6B80AOs=
 gitlab.com/xx_network/comms v0.0.4-0.20211014163953-e774276b83ae/go.mod h1:wR9Vx0KZLrIs0g2Efcp0UwFPStjcDRWkg/DJLVQI2vw=
@@ -447,8 +446,9 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
 google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/groupChat/gcMessages.pb.go b/groupChat/gcMessages.pb.go
index 37b23167e412866c8012fd9f8c95bd99adab88b3..d31c3001125f286af67fb1eacf6986b193513e7c 100644
--- a/groupChat/gcMessages.pb.go
+++ b/groupChat/gcMessages.pb.go
@@ -1,117 +1,201 @@
+///////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                          //
+//                                                                           //
+// Use of this source code is governed by a license that can be found in the //
+// LICENSE file                                                              //
+///////////////////////////////////////////////////////////////////////////////
+
 // Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.26.0
+// 	protoc        v3.17.3
 // source: groupChat/gcMessages.proto
 
 package groupChat
 
 import (
-	fmt "fmt"
-	proto "github.com/golang/protobuf/proto"
-	math "math"
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
 )
 
-// Reference imports to suppress errors if they are not otherwise used.
-var _ = proto.Marshal
-var _ = fmt.Errorf
-var _ = math.Inf
-
-// This is a compile-time assertion to ensure that this generated file
-// is compatible with the proto package it is being compiled against.
-// A compilation error at this line likely means your copy of the
-// proto package needs to be updated.
-const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
 
 // Request to join the group sent from leader to all members.
 type Request struct {
-	Name                 []byte   `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
-	IdPreimage           []byte   `protobuf:"bytes,2,opt,name=idPreimage,proto3" json:"idPreimage,omitempty"`
-	KeyPreimage          []byte   `protobuf:"bytes,3,opt,name=keyPreimage,proto3" json:"keyPreimage,omitempty"`
-	Members              []byte   `protobuf:"bytes,4,opt,name=members,proto3" json:"members,omitempty"`
-	Message              []byte   `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"`
-	XXX_NoUnkeyedLiteral struct{} `json:"-"`
-	XXX_unrecognized     []byte   `json:"-"`
-	XXX_sizecache        int32    `json:"-"`
-}
-
-func (m *Request) Reset()         { *m = Request{} }
-func (m *Request) String() string { return proto.CompactTextString(m) }
-func (*Request) ProtoMessage()    {}
-func (*Request) Descriptor() ([]byte, []int) {
-	return fileDescriptor_49d0b7a6ffb7e279, []int{0}
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Name        []byte `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	IdPreimage  []byte `protobuf:"bytes,2,opt,name=idPreimage,proto3" json:"idPreimage,omitempty"`
+	KeyPreimage []byte `protobuf:"bytes,3,opt,name=keyPreimage,proto3" json:"keyPreimage,omitempty"`
+	Members     []byte `protobuf:"bytes,4,opt,name=members,proto3" json:"members,omitempty"`
+	Message     []byte `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"`
+	Created     int64  `protobuf:"varint,6,opt,name=created,proto3" json:"created,omitempty"`
 }
 
-func (m *Request) XXX_Unmarshal(b []byte) error {
-	return xxx_messageInfo_Request.Unmarshal(m, b)
-}
-func (m *Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
-	return xxx_messageInfo_Request.Marshal(b, m, deterministic)
-}
-func (m *Request) XXX_Merge(src proto.Message) {
-	xxx_messageInfo_Request.Merge(m, src)
+func (x *Request) Reset() {
+	*x = Request{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_groupChat_gcMessages_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
 }
-func (m *Request) XXX_Size() int {
-	return xxx_messageInfo_Request.Size(m)
+
+func (x *Request) String() string {
+	return protoimpl.X.MessageStringOf(x)
 }
-func (m *Request) XXX_DiscardUnknown() {
-	xxx_messageInfo_Request.DiscardUnknown(m)
+
+func (*Request) ProtoMessage() {}
+
+func (x *Request) ProtoReflect() protoreflect.Message {
+	mi := &file_groupChat_gcMessages_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
 }
 
-var xxx_messageInfo_Request proto.InternalMessageInfo
+// Deprecated: Use Request.ProtoReflect.Descriptor instead.
+func (*Request) Descriptor() ([]byte, []int) {
+	return file_groupChat_gcMessages_proto_rawDescGZIP(), []int{0}
+}
 
-func (m *Request) GetName() []byte {
-	if m != nil {
-		return m.Name
+func (x *Request) GetName() []byte {
+	if x != nil {
+		return x.Name
 	}
 	return nil
 }
 
-func (m *Request) GetIdPreimage() []byte {
-	if m != nil {
-		return m.IdPreimage
+func (x *Request) GetIdPreimage() []byte {
+	if x != nil {
+		return x.IdPreimage
 	}
 	return nil
 }
 
-func (m *Request) GetKeyPreimage() []byte {
-	if m != nil {
-		return m.KeyPreimage
+func (x *Request) GetKeyPreimage() []byte {
+	if x != nil {
+		return x.KeyPreimage
 	}
 	return nil
 }
 
-func (m *Request) GetMembers() []byte {
-	if m != nil {
-		return m.Members
+func (x *Request) GetMembers() []byte {
+	if x != nil {
+		return x.Members
 	}
 	return nil
 }
 
-func (m *Request) GetMessage() []byte {
-	if m != nil {
-		return m.Message
+func (x *Request) GetMessage() []byte {
+	if x != nil {
+		return x.Message
 	}
 	return nil
 }
 
-func init() {
-	proto.RegisterType((*Request)(nil), "gcRequestMessages.Request")
+func (x *Request) GetCreated() int64 {
+	if x != nil {
+		return x.Created
+	}
+	return 0
+}
+
+var File_groupChat_gcMessages_proto protoreflect.FileDescriptor
+
+var file_groupChat_gcMessages_proto_rawDesc = []byte{
+	0x0a, 0x1a, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x43, 0x68, 0x61, 0x74, 0x2f, 0x67, 0x63, 0x4d, 0x65,
+	0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x11, 0x67, 0x63,
+	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22,
+	0xad, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e,
+	0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12,
+	0x1e, 0x0a, 0x0a, 0x69, 0x64, 0x50, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20,
+	0x01, 0x28, 0x0c, 0x52, 0x0a, 0x69, 0x64, 0x50, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12,
+	0x20, 0x0a, 0x0b, 0x6b, 0x65, 0x79, 0x50, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x03,
+	0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x6b, 0x65, 0x79, 0x50, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67,
+	0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x01,
+	0x28, 0x0c, 0x52, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6d,
+	0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x65,
+	0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,
+	0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42,
+	0x25, 0x5a, 0x23, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x6c,
+	0x69, 0x78, 0x78, 0x69, 0x72, 0x2f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2f, 0x67, 0x72, 0x6f,
+	0x75, 0x70, 0x43, 0x68, 0x61, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
-func init() {
-	proto.RegisterFile("groupChat/gcMessages.proto", fileDescriptor_49d0b7a6ffb7e279)
+var (
+	file_groupChat_gcMessages_proto_rawDescOnce sync.Once
+	file_groupChat_gcMessages_proto_rawDescData = file_groupChat_gcMessages_proto_rawDesc
+)
+
+func file_groupChat_gcMessages_proto_rawDescGZIP() []byte {
+	file_groupChat_gcMessages_proto_rawDescOnce.Do(func() {
+		file_groupChat_gcMessages_proto_rawDescData = protoimpl.X.CompressGZIP(file_groupChat_gcMessages_proto_rawDescData)
+	})
+	return file_groupChat_gcMessages_proto_rawDescData
 }
 
-var fileDescriptor_49d0b7a6ffb7e279 = []byte{
-	// 186 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4a, 0x2f, 0xca, 0x2f,
-	0x2d, 0x70, 0xce, 0x48, 0x2c, 0xd1, 0x4f, 0x4f, 0xf6, 0x4d, 0x2d, 0x2e, 0x4e, 0x4c, 0x4f, 0x2d,
-	0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x4c, 0x4f, 0x0e, 0x4a, 0x2d, 0x2c, 0x4d, 0x2d,
-	0x2e, 0x81, 0x49, 0x28, 0x4d, 0x66, 0xe4, 0x62, 0x87, 0x8a, 0x09, 0x09, 0x71, 0xb1, 0xe4, 0x25,
-	0xe6, 0xa6, 0x4a, 0x30, 0x2a, 0x30, 0x6a, 0xf0, 0x04, 0x81, 0xd9, 0x42, 0x72, 0x5c, 0x5c, 0x99,
-	0x29, 0x01, 0x45, 0xa9, 0x99, 0xb9, 0x89, 0xe9, 0xa9, 0x12, 0x4c, 0x60, 0x19, 0x24, 0x11, 0x21,
-	0x05, 0x2e, 0xee, 0xec, 0xd4, 0x4a, 0xb8, 0x02, 0x66, 0xb0, 0x02, 0x64, 0x21, 0x21, 0x09, 0x2e,
-	0xf6, 0xdc, 0xd4, 0xdc, 0xa4, 0xd4, 0xa2, 0x62, 0x09, 0x16, 0xb0, 0x2c, 0x8c, 0x0b, 0x91, 0x01,
-	0xbb, 0x43, 0x82, 0x15, 0x26, 0x03, 0xe6, 0x3a, 0xa9, 0x46, 0x29, 0xa7, 0x67, 0x96, 0xe4, 0x24,
-	0x26, 0xe9, 0x25, 0xe7, 0xe7, 0xea, 0xa7, 0xe6, 0x64, 0x56, 0x54, 0x64, 0x16, 0xe9, 0x27, 0xe7,
-	0x64, 0xa6, 0xe6, 0x95, 0xe8, 0xc3, 0x3d, 0x98, 0xc4, 0x06, 0xf6, 0x96, 0x31, 0x20, 0x00, 0x00,
-	0xff, 0xff, 0x6e, 0x63, 0x77, 0xd1, 0xf4, 0x00, 0x00, 0x00,
+var file_groupChat_gcMessages_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_groupChat_gcMessages_proto_goTypes = []interface{}{
+	(*Request)(nil), // 0: gcRequestMessages.Request
+}
+var file_groupChat_gcMessages_proto_depIdxs = []int32{
+	0, // [0:0] is the sub-list for method output_type
+	0, // [0:0] is the sub-list for method input_type
+	0, // [0:0] is the sub-list for extension type_name
+	0, // [0:0] is the sub-list for extension extendee
+	0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_groupChat_gcMessages_proto_init() }
+func file_groupChat_gcMessages_proto_init() {
+	if File_groupChat_gcMessages_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_groupChat_gcMessages_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*Request); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_groupChat_gcMessages_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   1,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_groupChat_gcMessages_proto_goTypes,
+		DependencyIndexes: file_groupChat_gcMessages_proto_depIdxs,
+		MessageInfos:      file_groupChat_gcMessages_proto_msgTypes,
+	}.Build()
+	File_groupChat_gcMessages_proto = out.File
+	file_groupChat_gcMessages_proto_rawDesc = nil
+	file_groupChat_gcMessages_proto_goTypes = nil
+	file_groupChat_gcMessages_proto_depIdxs = nil
 }
diff --git a/groupChat/gcMessages.proto b/groupChat/gcMessages.proto
index 7ce1f3b40c5f34f61367dcec2e477dd8040faae5..cc417505c71d9158a7e5e82fcc71ad25c8e3170f 100644
--- a/groupChat/gcMessages.proto
+++ b/groupChat/gcMessages.proto
@@ -17,4 +17,5 @@ message Request {
     bytes keyPreimage = 3;
     bytes members = 4;
     bytes message = 5;
+    int64 created = 6;
 }
\ No newline at end of file
diff --git a/groupChat/group.go b/groupChat/group.go
index 5d67d19f1e52f80cf1628f2deece083b4d8f4568..8878a62cdf750823bd4c886fbfbfdca1ac81c13d 100644
--- a/groupChat/group.go
+++ b/groupChat/group.go
@@ -22,6 +22,7 @@ package groupChat
 import (
 	gs "gitlab.com/elixxir/client/groupChat/groupStore"
 	"gitlab.com/xx_network/primitives/id"
+	"time"
 )
 
 // GroupChat is used to send and receive cMix messages to/from multiple users.
@@ -49,8 +50,9 @@ type GroupChat interface {
 	LeaveGroup(groupID *id.ID) error
 
 	// Send sends a message to all GroupChat members using Client.SendManyCMIX.
-	// The send fails if the message is too long.
-	Send(groupID *id.ID, message []byte) (id.Round, error)
+	// 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)
 
 	// GetGroups returns a list of all registered GroupChat IDs.
 	GetGroups() []*id.ID
diff --git a/groupChat/groupStore/dhKeyList.go b/groupChat/groupStore/dhKeyList.go
index 50c405c426d7a27f2f99767208d2b2915a4f93b5..38b7b56bf3d88c6bed432e29b5a580c270a0285a 100644
--- a/groupChat/groupStore/dhKeyList.go
+++ b/groupChat/groupStore/dhKeyList.go
@@ -25,15 +25,17 @@ const (
 	dhKeyDecodeErr = "failed to decode member DH key: %+v"
 )
 
+// DhKeyList is a map of users to their DH key.
 type DhKeyList map[id.ID]*cyclic.Int
 
-// GenerateDhKeyList generates the symmetric/DH key between the user and all
-// group members.
+// GenerateDhKeyList generates the DH key between the user and all group
+// members.
 func GenerateDhKeyList(userID *id.ID, privKey *cyclic.Int,
 	members group.Membership, grp *cyclic.Group) DhKeyList {
 	dkl := make(DhKeyList, len(members)-1)
 
 	for _, m := range members {
+		// Skip the group.member for the current user
 		if !userID.Cmp(m.ID) {
 			dkl.Add(privKey, m, grp)
 		}
@@ -86,7 +88,8 @@ func DeserializeDhKeyList(data []byte) (DhKeyList, error) {
 	buff := bytes.NewBuffer(data)
 	dkl := make(DhKeyList)
 
-	for n := buff.Next(id.ArrIDLen); len(n) == id.ArrIDLen; n = buff.Next(id.ArrIDLen) {
+	const idLen = id.ArrIDLen
+	for n := buff.Next(idLen); len(n) == idLen; n = buff.Next(idLen) {
 		// Read and unmarshal ID
 		uid, err := id.Unmarshal(n)
 		if err != nil {
diff --git a/groupChat/groupStore/dhKeyList_test.go b/groupChat/groupStore/dhKeyList_test.go
index c8a633d441cf57c8775d384d817e985e3de59ea5..e6b7a9e6965703d5ae3b7c9a5272597a173ac355 100644
--- a/groupChat/groupStore/dhKeyList_test.go
+++ b/groupChat/groupStore/dhKeyList_test.go
@@ -1,27 +1,51 @@
 package groupStore
 
 import (
+	"gitlab.com/xx_network/primitives/id"
 	"math/rand"
 	"reflect"
 	"strings"
 	"testing"
 )
 
-// // Unit test of GenerateDhKeyList.
-// func TestGenerateDhKeyList(t *testing.T) {
-// 	prng := rand.New(rand.NewSource(42))
-// 	grp := getGroup()
-// 	userID := id.NewIdFromString("userID", id.User, t)
-// 	privKey := grp.NewInt(42)
-// 	pubKey := grp.ExpG(privKey, grp.NewInt(1))
-// 	members := createMembership(prng, 10, t)
-// 	members[2].ID = userID
-// 	members[2].DhKey = pubKey
-//
-// 	dkl := GenerateDhKeyList(userID, privKey, members, grp)
-//
-// 	t.Log(dkl)
-// }
+// Unit test of GenerateDhKeyList.
+func TestGenerateDhKeyList(t *testing.T) {
+	prng := rand.New(rand.NewSource(42))
+	grp := getGroup()
+	userID := id.NewIdFromString("userID", id.User, t)
+	privKey := grp.NewInt(42)
+	members := createMembership(prng, 10, t)
+
+	// Set one of the members as the sender (the current user)
+	senderIndex := 2
+	members[senderIndex].ID = userID
+	members[senderIndex].DhKey = grp.ExpG(privKey, grp.NewInt(1))
+
+	dkl := GenerateDhKeyList(userID, privKey, members, grp)
+
+	for i, m := range members {
+		dhKey, exists := dkl[*m.ID]
+		if i == senderIndex {
+			// Make sure the sender is not in the list
+			if exists {
+				t.Errorf("Found DH key for sender (member #%d with ID %s) in "+
+					"DH key list.", i, m.ID)
+			}
+			continue
+		} else if !exists {
+			// Ensure a DH key exists in the list for this member
+			t.Errorf("No DH key for member #%d with ID %s in DH key list.",
+				i, m.ID)
+		}
+
+		// Make sure that the DH key is correct
+		if dhKey.Cmp(m.DhKey) == -2 {
+			t.Errorf("DH key in list for member #%d with ID %s incorrect."+
+				"\nexpected: %s\nreceived: %s",
+				i, m.ID, m.DhKey.Text(10), dhKey.Text(10))
+		}
+	}
+}
 
 // Unit test of DhKeyList.DeepCopy.
 func TestDhKeyList_DeepCopy(t *testing.T) {
@@ -73,7 +97,18 @@ func TestDeserializeDhKeyList_DhKeyBinaryDecodeError(t *testing.T) {
 // Unit test of DhKeyList.GoString.
 func TestDhKeyList_GoString(t *testing.T) {
 	grp := createTestGroup(rand.New(rand.NewSource(42)), t)
-	expected := "{Grcjbkt1IWKQzyvrQsPKJzKFYPGqwGfOpui/RtSrK0YD: 6342989043... in GRP: 6SsQ/HAHUn..., QCxg8d6XgoPUoJo2+WwglBdG4+1NpkaprotPp7T8OiAD: 2579328386... in GRP: 6SsQ/HAHUn..., invD4ElbVxL+/b4MECiH4QDazS2IX2kstgfaAKEcHHAD: 1688982497... in GRP: 6SsQ/HAHUn..., o54Okp0CSry8sWk5e7c05+8KbgHxhU3rX+Qk/vesIQgD: 5552242738... in GRP: 6SsQ/HAHUn..., wRYCP6iJdLrAyv2a0FaSsTYZ5ziWTf3Hno1TQ3NmHP0D: 2812078897... in GRP: 6SsQ/HAHUn..., 15ufnw07pVsMwNYUTIiFNYQay+BwmwdYCD9h03W8ArQD: 2588260662... in GRP: 6SsQ/HAHUn..., 3RqsBM4ux44bC6+uiBuCp1EQikLtPJA8qkNGWnhiBhYD: 4967151805... in GRP: 6SsQ/HAHUn..., 55ai4SlwXic/BckjJoKOKwVuOBdljhBhSYlH/fNEQQ4D: 3187530437... in GRP: 6SsQ/HAHUn..., 9PkZKU50joHnnku9b+NM3LqEPujWPoxP/hzr6lRtj6wD: 4832738218... in GRP: 6SsQ/HAHUn...}"
+	expected := "{Grcjbkt1IWKQzyvrQsPKJzKFYPGqwGfOpui/RtSrK0YD: 6342989043..." +
+		" in GRP: 6SsQ/HAHUn..., QCxg8d6XgoPUoJo2+WwglBdG4+1NpkaprotPp7T8OiAD" +
+		": 2579328386... in GRP: 6SsQ/HAHUn..., invD4ElbVxL+/b4MECiH4QDazS2IX" +
+		"2kstgfaAKEcHHAD: 1688982497... in GRP: 6SsQ/HAHUn..., o54Okp0CSry8sW" +
+		"k5e7c05+8KbgHxhU3rX+Qk/vesIQgD: 5552242738... in GRP: 6SsQ/HAHUn...," +
+		" wRYCP6iJdLrAyv2a0FaSsTYZ5ziWTf3Hno1TQ3NmHP0D: 2812078897... in GRP:" +
+		" 6SsQ/HAHUn..., 15ufnw07pVsMwNYUTIiFNYQay+BwmwdYCD9h03W8ArQD: 258826" +
+		"0662... in GRP: 6SsQ/HAHUn..., 3RqsBM4ux44bC6+uiBuCp1EQikLtPJA8qkNGW" +
+		"nhiBhYD: 4967151805... in GRP: 6SsQ/HAHUn..., 55ai4SlwXic/BckjJoKOKw" +
+		"VuOBdljhBhSYlH/fNEQQ4D: 3187530437... in GRP: 6SsQ/HAHUn..., 9PkZKU5" +
+		"0joHnnku9b+NM3LqEPujWPoxP/hzr6lRtj6wD: 4832738218... in GRP: 6SsQ/HA" +
+		"HUn...}"
 	if grp.DhKeys.GoString() != expected {
 		t.Errorf("GoString failed to return the expected string."+
 			"\nexpected: %s\nreceived: %s", expected, grp.DhKeys.GoString())
diff --git a/groupChat/groupStore/group.go b/groupChat/groupStore/group.go
index 98d0b489de808cf1ed5ca31818f4bc7f203fe878..9216bc43e582a595585e8c4cd8f05b68e846f980 100644
--- a/groupChat/groupStore/group.go
+++ b/groupChat/groupStore/group.go
@@ -18,6 +18,7 @@ import (
 	"gitlab.com/xx_network/primitives/id"
 	"gitlab.com/xx_network/primitives/netTime"
 	"strings"
+	"time"
 )
 
 // Storage values.
@@ -41,9 +42,10 @@ type Group struct {
 	Name        []byte            // Name of the group set by the user
 	ID          *id.ID            // Group ID
 	Key         group.Key         // Group key
-	IdPreimage  group.IdPreimage  // 256-bit value from CRNG
-	KeyPreimage group.KeyPreimage // 256-bit value from CRNG
+	IdPreimage  group.IdPreimage  // 256-bit randomly generated value
+	KeyPreimage group.KeyPreimage // 256-bit randomly generated value
 	InitMessage []byte            // The original invite message
+	Created     time.Time         // Timestamp of when the group was created
 	Members     group.Membership  // Sorted list of members in group
 	DhKeys      DhKeyList         // List of shared DH keys
 }
@@ -51,7 +53,8 @@ type Group struct {
 // NewGroup creates a new Group from copies of the given data.
 func NewGroup(name []byte, groupID *id.ID, groupKey group.Key,
 	idPreimage group.IdPreimage, keyPreimage group.KeyPreimage,
-	initMessage []byte, members group.Membership, dhKeys DhKeyList) Group {
+	initMessage []byte, created time.Time, members group.Membership,
+	dhKeys DhKeyList) Group {
 	g := Group{
 		Name:        make([]byte, len(name)),
 		ID:          groupID.DeepCopy(),
@@ -59,6 +62,7 @@ func NewGroup(name []byte, groupID *id.ID, groupKey group.Key,
 		IdPreimage:  idPreimage,
 		KeyPreimage: keyPreimage,
 		InitMessage: make([]byte, len(initMessage)),
+		Created:     created.Round(0),
 		Members:     members.DeepCopy(),
 		DhKeys:      dhKeys,
 	}
@@ -78,6 +82,7 @@ func (g Group) DeepCopy() Group {
 		IdPreimage:  g.IdPreimage,
 		KeyPreimage: g.KeyPreimage,
 		InitMessage: make([]byte, len(g.InitMessage)),
+		Created:     g.Created,
 		Members:     g.Members.DeepCopy(),
 		DhKeys:      make(map[id.ID]*cyclic.Int, len(g.Members)-1),
 	}
@@ -118,7 +123,12 @@ func removeGroup(groupID *id.ID, kv *versioned.KV) error {
 	return kv.Delete(groupStoreKey(groupID), groupStoreVersion)
 }
 
-// Serialize serializes the Group and returns the byte slice.
+// Serialize serializes the Group and returns the byte slice. The serialized
+// data follows the following format.
+// +----------+----------+----------+----------+------------+-------------+-----------------+-------------+---------+-------------+----------+----------+
+// | Name len |   Name   |    ID    |    Key   | IdPreimage | KeyPreimage | InitMessage len | InitMessage | Created | Members len | Members  |  DhKeys  |
+// | 8 bytes  | variable | 33 bytes | 32 bytes |  32 bytes  |  32 bytes   |     8 bytes     |  variable   | 8 bytes |   8 bytes   | variable | variable |
+// +----------+----------+----------+----------+------------+-------------+-----------------+-------------+---------+-------------+----------+----------+
 func (g Group) Serialize() []byte {
 	buff := bytes.NewBuffer(nil)
 
@@ -146,6 +156,11 @@ func (g Group) Serialize() []byte {
 	buff.Write(b)
 	buff.Write(g.InitMessage)
 
+	// Write created timestamp as Unix nanoseconds
+	b = make([]byte, 8)
+	binary.LittleEndian.PutUint64(b, uint64(g.Created.UnixNano()))
+	buff.Write(b)
+
 	// Write length of group membership and group membership
 	b = make([]byte, 8)
 	memberBytes := g.Members.Serialize()
@@ -191,6 +206,14 @@ func DeserializeGroup(data []byte) (Group, error) {
 		g.InitMessage = buff.Next(int(initMessageLength))
 	}
 
+	// Get created timestamp
+	createdNano := int64(binary.LittleEndian.Uint64(buff.Next(8)))
+	if createdNano == (time.Time{}).UnixNano() {
+		g.Created = time.Time{}
+	} else {
+		g.Created = time.Unix(0, createdNano)
+	}
+
 	// Get member list
 	membersLength := binary.LittleEndian.Uint64(buff.Next(8))
 	g.Members, err = group.DeserializeMembership(buff.Next(int(membersLength)))
@@ -207,7 +230,8 @@ func DeserializeGroup(data []byte) (Group, error) {
 	return g, err
 }
 
-// groupStoreKey generates a unique key to save and load a Group to/from storage.
+// groupStoreKey generates a unique key to save and load a Group to/from
+// storage.
 func groupStoreKey(groupID *id.ID) string {
 	return groupStorageKey + groupID.String()
 }
@@ -220,7 +244,7 @@ func (g Group) GoString() string {
 		idString = g.ID.String()
 	}
 
-	str := make([]string, 8)
+	str := make([]string, 9)
 
 	str[0] = "Name:" + fmt.Sprintf("%q", g.Name)
 	str[1] = "ID:" + idString
@@ -228,8 +252,9 @@ func (g Group) GoString() string {
 	str[3] = "IdPreimage:" + g.IdPreimage.String()
 	str[4] = "KeyPreimage:" + g.KeyPreimage.String()
 	str[5] = "InitMessage:" + fmt.Sprintf("%q", g.InitMessage)
-	str[6] = "Members:" + g.Members.String()
-	str[7] = "DhKeys:" + g.DhKeys.GoString()
+	str[6] = "Created:" + g.Created.String()
+	str[7] = "Members:" + g.Members.String()
+	str[8] = "DhKeys:" + g.DhKeys.GoString()
 
 	return "{" + strings.Join(str, ", ") + "}"
 }
diff --git a/groupChat/groupStore/group_test.go b/groupChat/groupStore/group_test.go
index ff855db08634dac15c33b918e14a3d394c90ef2b..2da1e2868991259142ec74cef049f9e9acd1035f 100644
--- a/groupChat/groupStore/group_test.go
+++ b/groupChat/groupStore/group_test.go
@@ -22,7 +22,8 @@ import (
 func TestNewGroup(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	membership := createMembership(prng, 10, t)
-	dkl := GenerateDhKeyList(membership[0].ID, randCycInt(prng), membership, getGroup())
+	dkl := GenerateDhKeyList(
+		membership[0].ID, randCycInt(prng), membership, getGroup())
 
 	expectedGroup := Group{
 		Name:        []byte(groupName),
@@ -31,6 +32,7 @@ func TestNewGroup(t *testing.T) {
 		IdPreimage:  newIdPreimage(groupIdPreimage),
 		KeyPreimage: newKeyPreimage(groupKeyPreimage),
 		InitMessage: []byte(initMessage),
+		Created:     created,
 		Members:     membership,
 		DhKeys:      dkl,
 	}
@@ -42,6 +44,7 @@ func TestNewGroup(t *testing.T) {
 		newIdPreimage(groupIdPreimage),
 		newKeyPreimage(groupKeyPreimage),
 		[]byte(initMessage),
+		expectedGroup.Created,
 		membership,
 		dkl,
 	)
@@ -260,7 +263,38 @@ func Test_groupStoreKey(t *testing.T) {
 // Unit test of Group.GoString.
 func TestGroup_GoString(t *testing.T) {
 	grp := createTestGroup(rand.New(rand.NewSource(42)), t)
-	expected := "{Name:\"groupName\", ID:XMCYoCcs5+sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE, Key:a2V5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, IdPreimage:aWRQcmVpbWFnZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, KeyPreimage:a2V5UHJlaW1hZ2UAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, InitMessage:\"initMessage\", Members:{Leader: {U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID, 3534334367... in GRP: 6SsQ/HAHUn...}, Participants: 0: {Grcjbkt1IWKQzyvrQsPKJzKFYPGqwGfOpui/RtSrK0YD, 5274380952... in GRP: 6SsQ/HAHUn...}, 1: {QCxg8d6XgoPUoJo2+WwglBdG4+1NpkaprotPp7T8OiAD, 1628829379... in GRP: 6SsQ/HAHUn...}, 2: {invD4ElbVxL+/b4MECiH4QDazS2IX2kstgfaAKEcHHAD, 4157513341... in GRP: 6SsQ/HAHUn...}, 3: {o54Okp0CSry8sWk5e7c05+8KbgHxhU3rX+Qk/vesIQgD, 6317064433... in GRP: 6SsQ/HAHUn...}, 4: {wRYCP6iJdLrAyv2a0FaSsTYZ5ziWTf3Hno1TQ3NmHP0D, 5785305945... in GRP: 6SsQ/HAHUn...}, 5: {15ufnw07pVsMwNYUTIiFNYQay+BwmwdYCD9h03W8ArQD, 2010156224... in GRP: 6SsQ/HAHUn...}, 6: {3RqsBM4ux44bC6+uiBuCp1EQikLtPJA8qkNGWnhiBhYD, 2643318057... in GRP: 6SsQ/HAHUn...}, 7: {55ai4SlwXic/BckjJoKOKwVuOBdljhBhSYlH/fNEQQ4D, 6482807720... in GRP: 6SsQ/HAHUn...}, 8: {9PkZKU50joHnnku9b+NM3LqEPujWPoxP/hzr6lRtj6wD, 6603068123... in GRP: 6SsQ/HAHUn...}}, DhKeys:{Grcjbkt1IWKQzyvrQsPKJzKFYPGqwGfOpui/RtSrK0YD: 6342989043... in GRP: 6SsQ/HAHUn..., QCxg8d6XgoPUoJo2+WwglBdG4+1NpkaprotPp7T8OiAD: 2579328386... in GRP: 6SsQ/HAHUn..., invD4ElbVxL+/b4MECiH4QDazS2IX2kstgfaAKEcHHAD: 1688982497... in GRP: 6SsQ/HAHUn..., o54Okp0CSry8sWk5e7c05+8KbgHxhU3rX+Qk/vesIQgD: 5552242738... in GRP: 6SsQ/HAHUn..., wRYCP6iJdLrAyv2a0FaSsTYZ5ziWTf3Hno1TQ3NmHP0D: 2812078897... in GRP: 6SsQ/HAHUn..., 15ufnw07pVsMwNYUTIiFNYQay+BwmwdYCD9h03W8ArQD: 2588260662... in GRP: 6SsQ/HAHUn..., 3RqsBM4ux44bC6+uiBuCp1EQikLtPJA8qkNGWnhiBhYD: 4967151805... in GRP: 6SsQ/HAHUn..., 55ai4SlwXic/BckjJoKOKwVuOBdljhBhSYlH/fNEQQ4D: 3187530437... in GRP: 6SsQ/HAHUn..., 9PkZKU50joHnnku9b+NM3LqEPujWPoxP/hzr6lRtj6wD: 4832738218... in GRP: 6SsQ/HAHUn...}}"
+	grp.Created = grp.Created.UTC()
+	expected := "{Name:\"groupName\", " +
+		"ID:XMCYoCcs5+sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE, " +
+		"Key:a2V5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, " +
+		"IdPreimage:aWRQcmVpbWFnZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, " +
+		"KeyPreimage:a2V5UHJlaW1hZ2UAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, " +
+		"InitMessage:\"initMessage\", " +
+		"Created:" + grp.Created.String() + ", " +
+		"Members:{" +
+		"Leader: {U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID, 3534334367... in GRP: 6SsQ/HAHUn...}, " +
+		"Participants: " +
+		"0: {Grcjbkt1IWKQzyvrQsPKJzKFYPGqwGfOpui/RtSrK0YD, 5274380952... in GRP: 6SsQ/HAHUn...}, " +
+		"1: {QCxg8d6XgoPUoJo2+WwglBdG4+1NpkaprotPp7T8OiAD, 1628829379... in GRP: 6SsQ/HAHUn...}, " +
+		"2: {invD4ElbVxL+/b4MECiH4QDazS2IX2kstgfaAKEcHHAD, 4157513341... in GRP: 6SsQ/HAHUn...}, " +
+		"3: {o54Okp0CSry8sWk5e7c05+8KbgHxhU3rX+Qk/vesIQgD, 6317064433... in GRP: 6SsQ/HAHUn...}, " +
+		"4: {wRYCP6iJdLrAyv2a0FaSsTYZ5ziWTf3Hno1TQ3NmHP0D, 5785305945... in GRP: 6SsQ/HAHUn...}, " +
+		"5: {15ufnw07pVsMwNYUTIiFNYQay+BwmwdYCD9h03W8ArQD, 2010156224... in GRP: 6SsQ/HAHUn...}, " +
+		"6: {3RqsBM4ux44bC6+uiBuCp1EQikLtPJA8qkNGWnhiBhYD, 2643318057... in GRP: 6SsQ/HAHUn...}, " +
+		"7: {55ai4SlwXic/BckjJoKOKwVuOBdljhBhSYlH/fNEQQ4D, 6482807720... in GRP: 6SsQ/HAHUn...}, " +
+		"8: {9PkZKU50joHnnku9b+NM3LqEPujWPoxP/hzr6lRtj6wD, 6603068123... in GRP: 6SsQ/HAHUn...}" +
+		"}, " +
+		"DhKeys:{" +
+		"Grcjbkt1IWKQzyvrQsPKJzKFYPGqwGfOpui/RtSrK0YD: 6342989043... in GRP: 6SsQ/HAHUn..., " +
+		"QCxg8d6XgoPUoJo2+WwglBdG4+1NpkaprotPp7T8OiAD: 2579328386... in GRP: 6SsQ/HAHUn..., " +
+		"invD4ElbVxL+/b4MECiH4QDazS2IX2kstgfaAKEcHHAD: 1688982497... in GRP: 6SsQ/HAHUn..., " +
+		"o54Okp0CSry8sWk5e7c05+8KbgHxhU3rX+Qk/vesIQgD: 5552242738... in GRP: 6SsQ/HAHUn..., " +
+		"wRYCP6iJdLrAyv2a0FaSsTYZ5ziWTf3Hno1TQ3NmHP0D: 2812078897... in GRP: 6SsQ/HAHUn..., " +
+		"15ufnw07pVsMwNYUTIiFNYQay+BwmwdYCD9h03W8ArQD: 2588260662... in GRP: 6SsQ/HAHUn..., " +
+		"3RqsBM4ux44bC6+uiBuCp1EQikLtPJA8qkNGWnhiBhYD: 4967151805... in GRP: 6SsQ/HAHUn..., " +
+		"55ai4SlwXic/BckjJoKOKwVuOBdljhBhSYlH/fNEQQ4D: 3187530437... in GRP: 6SsQ/HAHUn..., " +
+		"9PkZKU50joHnnku9b+NM3LqEPujWPoxP/hzr6lRtj6wD: 4832738218... in GRP: 6SsQ/HAHUn..." +
+		"}}"
 
 	if grp.GoString() != expected {
 		t.Errorf("GoString failed to return the expected string."+
@@ -278,6 +312,7 @@ func TestGroup_GoString_NilGroup(t *testing.T) {
 		"IdPreimage:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, " +
 		"KeyPreimage:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, " +
 		"InitMessage:\"\", " +
+		"Created:0001-01-01 00:00:00 +0000 UTC, " +
 		"Members:{<nil>}, " +
 		"DhKeys:{}" +
 		"}"
diff --git a/groupChat/groupStore/store.go b/groupChat/groupStore/store.go
index 88e980603e323114b2a0c39a138f0e2977627053..f180249e7917b38704b0027b012f3a118262d9f6 100644
--- a/groupChat/groupStore/store.go
+++ b/groupChat/groupStore/store.go
@@ -40,7 +40,8 @@ const (
 	setUserPanic      = "Store.SetUser is for testing only. Got %T"
 )
 
-// The maximum number of group chats that a user can be a part of at once.
+// MaxGroupChats is the maximum number of group chats that a user can be a part
+// of at once.
 const MaxGroupChats = 64
 
 // Store stores the list of Groups that a user is a part of.
@@ -265,7 +266,8 @@ func (s *Store) Get(groupID *id.ID) (Group, bool) {
 
 // GetByKeyFp returns the group with the matching key fingerprint and salt.
 // Returns false if no group is found.
-func (s *Store) GetByKeyFp(keyFp format.Fingerprint, salt [group.SaltLen]byte) (Group, bool) {
+func (s *Store) GetByKeyFp(keyFp format.Fingerprint, salt [group.SaltLen]byte) (
+	Group, bool) {
 	s.mux.RLock()
 	defer s.mux.RUnlock()
 
diff --git a/groupChat/groupStore/store_test.go b/groupChat/groupStore/store_test.go
index 0e0917ff38ce33b83cbecbfafb4f960ea8c38447..0303446c99e2a4c5d36e51887a494d18f68810f8 100644
--- a/groupChat/groupStore/store_test.go
+++ b/groupChat/groupStore/store_test.go
@@ -58,7 +58,7 @@ func TestNewStore(t *testing.T) {
 		groupIds = append(groupIds, grpId)
 	}
 
-	// Check that stored group Id list is expected value
+	// Check that stored group ID list is expected value
 	expectedData := serializeGroupIdList(store.list)
 
 	obj, err := store.kv.Get(groupListStorageKey, groupListVersion)
@@ -211,7 +211,7 @@ func Test_serializeGroupIdList_deserializeGroupIdList(t *testing.T) {
 	data := serializeGroupIdList(testMap)
 	newList := deserializeGroupIdList(data)
 
-	// Sort expected and received lists so they are in the same order
+	// Sort expected and received lists so that they are in the same order
 	sort.Slice(expected, func(i, j int) bool {
 		return bytes.Compare(expected[i].Bytes(), expected[j].Bytes()) == -1
 	})
@@ -405,7 +405,7 @@ func TestStore_GroupIDs(t *testing.T) {
 
 	newList := store.GroupIDs()
 
-	// Sort expected and received lists so they are in the same order
+	// Sort expected and received lists so that they are in the same order
 	sort.Slice(expected, func(i, j int) bool {
 		return bytes.Compare(expected[i].Bytes(), expected[j].Bytes()) == -1
 	})
diff --git a/groupChat/groupStore/utils_test.go b/groupChat/groupStore/utils_test.go
index 9a6460800498b11fdbcb8a4b9b5aa7d4e391b2f3..8aff7c8400183aa61b352d1e4f44f4056f591de5 100644
--- a/groupChat/groupStore/utils_test.go
+++ b/groupChat/groupStore/utils_test.go
@@ -15,6 +15,7 @@ import (
 	"gitlab.com/xx_network/primitives/id"
 	"math/rand"
 	"testing"
+	"time"
 )
 
 const (
@@ -26,6 +27,8 @@ const (
 	initMessage      = "initMessage"
 )
 
+var created = time.Date(1955, 11, 5, 12, 1, 0, 0, time.Local)
+
 // createTestGroup generates a new group for testing.
 func createTestGroup(rng *rand.Rand, t *testing.T) Group {
 	members := createMembership(rng, 10, t)
@@ -37,6 +40,7 @@ func createTestGroup(rng *rand.Rand, t *testing.T) Group {
 		newIdPreimage(groupIdPreimage),
 		newKeyPreimage(groupKeyPreimage),
 		[]byte(initMessage),
+		created,
 		members,
 		dkl,
 	)
diff --git a/groupChat/internalFormat.go b/groupChat/internalFormat.go
index 2502a9c8c29c9f940a93bebb1101c5d5a5ae8ef2..e8fd69df643856b49a217a7ae0602ce7e741e363 100644
--- a/groupChat/internalFormat.go
+++ b/groupChat/internalFormat.go
@@ -94,7 +94,7 @@ func (im internalMsg) SetTimestamp(t time.Time) {
 	binary.LittleEndian.PutUint64(im.timestamp, uint64(t.UnixNano()))
 }
 
-// GetSenderID returns the sender ID bytes as a id.ID.
+// GetSenderID returns the sender ID bytes as an id.ID.
 func (im internalMsg) GetSenderID() (*id.ID, error) {
 	return id.Unmarshal(im.senderID)
 }
diff --git a/groupChat/internalFormat_test.go b/groupChat/internalFormat_test.go
index 984d11b8f35ec44935eaea48b5bbd76eec80bdb1..7c2d415ee68e041842d127293ccdf97b2506d1cf 100644
--- a/groupChat/internalFormat_test.go
+++ b/groupChat/internalFormat_test.go
@@ -190,7 +190,9 @@ func TestInternalMsg_String(t *testing.T) {
 	payload = append(payload, 0, 1, 2)
 	im.SetPayload(payload)
 
-	expected := `{timestamp:` + im.GetTimestamp().String() + `, senderID:dGVzdCBzZW5kZXIgSUQAAAAAAAAAAAAAAAAAAAAAAAAD, size:26, payload:"Sample payload message.\x00\x01\x02"}`
+	expected := "{timestamp:" + im.GetTimestamp().String() +
+		", senderID:dGVzdCBzZW5kZXIgSUQAAAAAAAAAAAAAAAAAAAAAAAAD, " +
+		"size:26, payload:\"Sample payload message.\\x00\\x01\\x02\"}"
 
 	if im.String() != expected {
 		t.Errorf("String() failed to return the expected value."+
@@ -198,7 +200,8 @@ func TestInternalMsg_String(t *testing.T) {
 	}
 }
 
-// Happy path: tests that String returns the expected string for a nil internalMsg.
+// Happy path: tests that String returns the expected string for a nil
+// internalMsg.
 func TestInternalMsg_String_NilInternalMessage(t *testing.T) {
 	im := internalMsg{}
 
diff --git a/groupChat/makeGroup.go b/groupChat/makeGroup.go
index 2c277b4384a04e187e722488d43b99357f1eb0e7..7cfe7dfd8326b1f221b9ef4a9bf16c8096879c3a 100644
--- a/groupChat/makeGroup.go
+++ b/groupChat/makeGroup.go
@@ -11,10 +11,13 @@ import (
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
 	gs "gitlab.com/elixxir/client/groupChat/groupStore"
+	"gitlab.com/elixxir/client/interfaces/preimage"
+	"gitlab.com/elixxir/client/storage/edge"
 	"gitlab.com/elixxir/crypto/contact"
 	"gitlab.com/elixxir/crypto/fastRNG"
 	"gitlab.com/elixxir/crypto/group"
 	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/netTime"
 	"strconv"
 )
 
@@ -74,25 +77,39 @@ func (m Manager) MakeGroup(membership []*id.ID, name, msg []byte) (gs.Group,
 	groupID := group.NewID(idPreimage, mem)
 	groupKey := group.NewKey(keyPreimage, mem)
 
+	// Generate group creation timestamp stripped of the monotonic clock
+	created := netTime.Now().Round(0)
+
 	// Create new group and add to manager
-	g := gs.NewGroup(name, groupID, groupKey, idPreimage, keyPreimage, msg, mem, dkl)
-	if err := m.gs.Add(g); err != nil {
+	g := gs.NewGroup(
+		name, groupID, groupKey, idPreimage, keyPreimage, msg, created, mem, dkl)
+	if err = m.gs.Add(g); err != nil {
 		return gs.Group{}, nil, NotSent, errors.Errorf(addGroupErr, err)
 	}
 
-	jww.DEBUG.Printf("Created new group %q with ID %s and members %s",
-		g.Name, g.ID, g.Members)
+	jww.DEBUG.Printf("Created new group %q with ID %s and %d members %s",
+		g.Name, g.ID, len(g.Members), g.Members)
 
 	// Send all group requests
 	roundIDs, status, err := m.sendRequests(g)
 
+	if err == nil {
+		edgeStore := m.store.GetEdge()
+		edgeStore.Add(edge.Preimage{
+			Data:   g.ID[:],
+			Type:   preimage.Group,
+			Source: g.ID[:],
+		}, m.store.GetUser().ReceptionID)
+	}
+
 	return g, roundIDs, status, err
 }
 
 // buildMembership retrieves the contact object for each member ID and creates a
 // new membership from them. The caller is set as the leader. For a member to be
 // added, the group leader must have an authenticated channel with the member.
-func (m Manager) buildMembership(members []*id.ID) (group.Membership, gs.DhKeyList, error) {
+func (m Manager) buildMembership(members []*id.ID) (group.Membership,
+	gs.DhKeyList, error) {
 	// Return an error if the membership list has too few or too many members
 	if len(members) < group.MinParticipants {
 		return nil, nil,
diff --git a/groupChat/makeGroup_test.go b/groupChat/makeGroup_test.go
index 004bf452664984c8ec8651442ab10a7a64d48fee..6c38cae0fcd61d2c21e750c050c713d55b6edd77 100644
--- a/groupChat/makeGroup_test.go
+++ b/groupChat/makeGroup_test.go
@@ -77,7 +77,8 @@ func TestManager_MakeGroup(t *testing.T) {
 func TestManager_MakeGroup_MaxMessageSizeError(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
-	expectedErr := fmt.Sprintf(maxInitMsgSizeErr, MaxInitMessageSize+1, MaxInitMessageSize)
+	expectedErr := fmt.Sprintf(
+		maxInitMsgSizeErr, MaxInitMessageSize+1, MaxInitMessageSize)
 
 	_, _, status, err := m.MakeGroup(nil, nil, make([]byte, MaxInitMessageSize+1))
 	if err == nil || err.Error() != expectedErr {
@@ -96,7 +97,8 @@ func TestManager_MakeGroup_MaxMessageSizeError(t *testing.T) {
 func TestManager_MakeGroup_MembershipSizeError(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
-	expectedErr := fmt.Sprintf(maxMembersErr, group.MaxParticipants+1, group.MaxParticipants)
+	expectedErr := fmt.Sprintf(
+		maxMembersErr, group.MaxParticipants+1, group.MaxParticipants)
 
 	_, _, status, err := m.MakeGroup(make([]*id.ID, group.MaxParticipants+1),
 		nil, []byte{})
@@ -153,7 +155,8 @@ func TestManager_buildMembership(t *testing.T) {
 func TestManager_buildMembership_MinParticipantsError(t *testing.T) {
 	m, _ := newTestManager(rand.New(rand.NewSource(42)), t)
 	memberIDs := make([]*id.ID, group.MinParticipants-1)
-	expectedErr := fmt.Sprintf(minMembersErr, len(memberIDs), group.MinParticipants)
+	expectedErr := fmt.Sprintf(
+		minMembersErr, len(memberIDs), group.MinParticipants)
 
 	_, _, err := m.buildMembership(memberIDs)
 	if err == nil || !strings.Contains(err.Error(), expectedErr) {
@@ -167,7 +170,8 @@ func TestManager_buildMembership_MinParticipantsError(t *testing.T) {
 func TestManager_buildMembership_MaxParticipantsError(t *testing.T) {
 	m, _ := newTestManager(rand.New(rand.NewSource(42)), t)
 	memberIDs := make([]*id.ID, group.MaxParticipants+1)
-	expectedErr := fmt.Sprintf(maxMembersErr, len(memberIDs), group.MaxParticipants)
+	expectedErr := fmt.Sprintf(
+		maxMembersErr, len(memberIDs), group.MaxParticipants)
 
 	_, _, err := m.buildMembership(memberIDs)
 	if err == nil || !strings.Contains(err.Error(), expectedErr) {
@@ -275,7 +279,8 @@ 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, gs.DhKeyList) {
+func addPartners(m *Manager, t *testing.T) ([]*id.ID, group.Membership,
+	gs.DhKeyList) {
 	memberIDs := make([]*id.ID, 10)
 	members := group.Membership{m.gs.GetUser()}
 	dkl := gs.DhKeyList{}
diff --git a/groupChat/manager.go b/groupChat/manager.go
index 6691e54154e613678d3b50684aa333a63b9c1f64..4b0b3066772f7f7e72e38ef531c32f7d44b3f797 100644
--- a/groupChat/manager.go
+++ b/groupChat/manager.go
@@ -14,8 +14,10 @@ import (
 	gs "gitlab.com/elixxir/client/groupChat/groupStore"
 	"gitlab.com/elixxir/client/interfaces"
 	"gitlab.com/elixxir/client/interfaces/message"
+	"gitlab.com/elixxir/client/interfaces/preimage"
 	"gitlab.com/elixxir/client/stoppable"
 	"gitlab.com/elixxir/client/storage"
+	"gitlab.com/elixxir/client/storage/edge"
 	"gitlab.com/elixxir/client/storage/versioned"
 	"gitlab.com/elixxir/crypto/cyclic"
 	"gitlab.com/elixxir/crypto/fastRNG"
@@ -79,7 +81,8 @@ func newManager(client *api.Client, userID *id.ID, userDhKey *cyclic.Int,
 	receiveFunc ReceiveCallback) (*Manager, error) {
 
 	// Load the group chat storage or create one if one does not exist
-	gStore, err := gs.NewOrLoadStore(kv, group.Member{ID: userID, DhKey: userDhKey})
+	gStore, err := gs.NewOrLoadStore(
+		kv, group.Member{ID: userID, DhKey: userDhKey})
 	if err != nil {
 		return nil, errors.Errorf(newGroupStoreErr, err)
 	}
@@ -128,7 +131,14 @@ func (m Manager) JoinGroup(g gs.Group) error {
 		return errors.Errorf(joinGroupErr, g.ID, err)
 	}
 
-	jww.DEBUG.Printf("Joined group %s.", g.ID)
+	edgeStore := m.store.GetEdge()
+	edgeStore.Add(edge.Preimage{
+		Data:   g.ID[:],
+		Type:   preimage.Group,
+		Source: g.ID[:],
+	}, m.store.GetUser().ReceptionID)
+
+	jww.DEBUG.Printf("Joined group %q with ID %s.", g.Name, g.ID)
 
 	return nil
 }
@@ -139,9 +149,16 @@ func (m Manager) LeaveGroup(groupID *id.ID) error {
 		return errors.Errorf(leaveGroupErr, groupID, err)
 	}
 
-	jww.DEBUG.Printf("Left group %s.", groupID)
+	edgeStore := m.store.GetEdge()
+	err := edgeStore.Remove(edge.Preimage{
+		Data:   groupID[:],
+		Type:   preimage.Group,
+		Source: groupID[:],
+	}, m.store.GetUser().ReceptionID)
 
-	return nil
+	jww.DEBUG.Printf("Left group with ID %s.", groupID)
+
+	return err
 }
 
 // GetGroups returns a list of all registered groupChat IDs.
diff --git a/groupChat/manager_test.go b/groupChat/manager_test.go
index 0ea0f5341018fac4a24e72b999603d2a93086ed2..b3367230a80faaf3b2f752e627414914d8c8fbfe 100644
--- a/groupChat/manager_test.go
+++ b/groupChat/manager_test.go
@@ -31,7 +31,8 @@ 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, user.ID, user.DhKey, nil, nil, nil, nil, kv, requestFunc, receiveFunc)
+	m, err := newManager(nil, user.ID, user.DhKey, nil, nil, nil, nil, kv,
+		requestFunc, receiveFunc)
 	if err != nil {
 		t.Errorf("newManager() returned an error: %+v", err)
 	}
@@ -84,7 +85,8 @@ func Test_newManager_LoadStorage(t *testing.T) {
 		}
 	}
 
-	m, err := newManager(nil, user.ID, user.DhKey, nil, nil, nil, nil, kv, nil, nil)
+	m, err := newManager(
+		nil, user.ID, user.DhKey, nil, nil, nil, nil, kv, nil, nil)
 	if err != nil {
 		t.Errorf("newManager() returned an error: %+v", err)
 	}
@@ -125,7 +127,10 @@ func Test_newManager_LoadError(t *testing.T) {
 	}
 }
 
-//
+// FIXME: the test storage.Session used for each manager currently uses the same
+//  user. To fix this test, they need to use different users, which requires
+//  modifying
+// storage.InitTestingSession.
 // func TestManager_StartProcesses(t *testing.T) {
 // 	jww.SetLogThreshold(jww.LevelTrace)
 // 	jww.SetStdoutThreshold(jww.LevelTrace)
@@ -271,7 +276,8 @@ func Test_newManager_LoadError(t *testing.T) {
 func TestManager_JoinGroup(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
-	g := newTestGroup(m.store.E2e().GetGroup(), m.store.GetUser().E2eDhPrivateKey, prng, t)
+	g := newTestGroup(
+		m.store.E2e().GetGroup(), m.store.GetUser().E2eDhPrivateKey, prng, t)
 
 	err := m.JoinGroup(g)
 	if err != nil {
diff --git a/groupChat/publicFormat_test.go b/groupChat/publicFormat_test.go
index 69884ff76856562e0d6f9ee3af03ad63e6eecb74..ceb1ba2ec32ea0f6112b33a53218af557c0c2706 100644
--- a/groupChat/publicFormat_test.go
+++ b/groupChat/publicFormat_test.go
@@ -141,7 +141,9 @@ func Test_publicMsg_String(t *testing.T) {
 	payload = append(payload, 0, 1, 2)
 	pm.SetPayload(payload)
 
-	expected := `{salt:U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVI=, payload:"Sample payload message.\x00\x01\x02\x00\x00\x00\x00\x00\x00"}`
+	expected := "{salt:U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVI=, " +
+		"payload:\"Sample payload message." +
+		"\\x00\\x01\\x02\\x00\\x00\\x00\\x00\\x00\\x00\"}"
 
 	if pm.String() != expected {
 		t.Errorf("String() failed to return the expected value."+
@@ -149,7 +151,8 @@ func Test_publicMsg_String(t *testing.T) {
 	}
 }
 
-// Happy path: tests that String returns the expected string for a nil publicMsg.
+// Happy path: tests that String returns the expected string for a nil
+// publicMsg.
 func Test_publicMsg_String_NilInternalMessage(t *testing.T) {
 	pm := publicMsg{}
 
diff --git a/groupChat/receive.go b/groupChat/receive.go
index 64cf10b789b3d07bf9d1dbd82d6d76b15c91fe53..0cf5ca65f7a90a2648e1e5d30fe2a26772eb8007 100644
--- a/groupChat/receive.go
+++ b/groupChat/receive.go
@@ -25,7 +25,7 @@ const (
 	unmarshalInternalMsgErr = "failed to unmarshal group internal message: %+v"
 	unmarshalSenderIdErr    = "failed to unmarshal sender ID: %+v"
 	unmarshalPublicMsgErr   = "failed to unmarshal group cMix message contents: %+v"
-	findGroupKeyFpErr       = "failed to find group with key fingerprint matching %s"
+	findGroupKeyFpErr       = "no group with key fingerprint %s"
 	genCryptKeyMacErr       = "failed to generate encryption key for group " +
 		"cMix message because MAC verification failed (epoch %d could be off)"
 )
@@ -45,13 +45,23 @@ func (m Manager) receive(rawMsgs chan message.Receive, stop *stoppable.Single) {
 			jww.DEBUG.Print("Group message reception received cMix message.")
 
 			// Attempt to read the message
-			g, msgID, timestamp, senderID, msg, err := m.readMessage(receiveMsg)
+			g, msgID, timestamp, senderID, msg, noFpMatch, err :=
+				m.readMessage(receiveMsg)
 			if err != nil {
-				jww.WARN.Printf("Group message reception failed to read cMix "+
-					"message: %+v", err)
+				if noFpMatch {
+					jww.DEBUG.Printf("Received message not for group chat: %+v",
+						err)
+				} else {
+					jww.WARN.Printf("Group message reception failed to read "+
+						"cMix message: %+v", err)
+				}
 				continue
 			}
 
+			jww.DEBUG.Printf("Received group message with ID %s from sender "+
+				"%s in group %s with ID %s at %s.", msgID, senderID, g.Name,
+				g.ID, timestamp)
+
 			// If the message was read correctly, send it to the callback
 			go m.receiveFunc(MessageReceive{
 				GroupID:        g.ID,
@@ -60,43 +70,44 @@ func (m Manager) receive(rawMsgs chan message.Receive, stop *stoppable.Single) {
 				SenderID:       senderID,
 				RecipientID:    receiveMsg.RecipientID,
 				EphemeralID:    receiveMsg.EphemeralID,
-				Timestamp:      receiveMsg.Timestamp,
+				Timestamp:      timestamp,
 				RoundID:        receiveMsg.RoundId,
-				RoundTimestamp: timestamp,
+				RoundTimestamp: receiveMsg.RoundTimestamp,
 			})
 		}
 	}
 }
 
 // readMessage returns the group, message ID, timestamp, sender ID, and message
-// of a group message. The encrypted group message data is unmarshaled from a
+// of a group message. The encrypted group message data is unmarshalled from a
 // cMix message in the message.Receive and then decrypted and the MAC is
 // verified. The group is found by finding the group with a matching key
-// fingerprint.
+// fingerprint. Returns true if the key fingerprint cannot be found; in this
+// case no warning or error should be printed.
 func (m *Manager) readMessage(msg message.Receive) (gs.Group, group.MessageID,
-	time.Time, *id.ID, []byte, error) {
+	time.Time, *id.ID, []byte, bool, error) {
 	// Unmarshal payload into cMix message
 	cMixMsg := format.Unmarshal(msg.Payload)
 
 	// Unmarshal cMix message contents to get public message format
-	publicMsg, err := unmarshalPublicMsg(cMixMsg.GetContents())
+	pubMsg, err := unmarshalPublicMsg(cMixMsg.GetContents())
 	if err != nil {
-		return gs.Group{}, group.MessageID{}, time.Time{}, nil, nil,
+		return gs.Group{}, group.MessageID{}, time.Time{}, nil, nil, false,
 			errors.Errorf(unmarshalPublicMsgErr, err)
 	}
 
 	// Get the group from storage via key fingerprint lookup
-	g, exists := m.gs.GetByKeyFp(cMixMsg.GetKeyFP(), publicMsg.GetSalt())
+	g, exists := m.gs.GetByKeyFp(cMixMsg.GetKeyFP(), pubMsg.GetSalt())
 	if !exists {
-		return gs.Group{}, group.MessageID{}, time.Time{}, nil, nil,
+		return gs.Group{}, group.MessageID{}, time.Time{}, nil, nil, true,
 			errors.Errorf(findGroupKeyFpErr, cMixMsg.GetKeyFP())
 	}
 
 	// Decrypt the payload and return the messages timestamp, sender ID, and
 	// message contents
 	messageID, timestamp, senderID, contents, err := m.decryptMessage(
-		g, cMixMsg, publicMsg, msg.RoundTimestamp)
-	return g, messageID, timestamp, senderID, contents, err
+		g, cMixMsg, pubMsg, msg.RoundTimestamp)
+	return g, messageID, timestamp, senderID, contents, false, err
 }
 
 // decryptMessage decrypts the group message payload and returns its message ID,
@@ -116,23 +127,22 @@ func (m *Manager) decryptMessage(g gs.Group, cMixMsg format.Message,
 		publicMsg.GetPayload())
 
 	// Unmarshal internal message
-	internalMsg, err := unmarshalInternalMsg(decryptedPayload)
+	intlMsg, err := unmarshalInternalMsg(decryptedPayload)
 	if err != nil {
 		return group.MessageID{}, time.Time{}, nil, nil,
 			errors.Errorf(unmarshalInternalMsgErr, err)
 	}
 
 	// Unmarshal sender ID
-	senderID, err := internalMsg.GetSenderID()
+	senderID, err := intlMsg.GetSenderID()
 	if err != nil {
 		return group.MessageID{}, time.Time{}, nil, nil,
 			errors.Errorf(unmarshalSenderIdErr, err)
 	}
 
-	messageID := group.NewMessageID(g.ID, internalMsg.Marshal())
+	messageID := group.NewMessageID(g.ID, intlMsg.Marshal())
 
-	return messageID, internalMsg.GetTimestamp(), senderID,
-		internalMsg.GetPayload(), nil
+	return messageID, intlMsg.GetTimestamp(), senderID, intlMsg.GetPayload(), nil
 }
 
 // getCryptKey generates the decryption key for a group internal message. The
diff --git a/groupChat/receiveRequest.go b/groupChat/receiveRequest.go
index e5c7576f174f793d29ef337e092c96e4d782c371..844b86249e593bdd5532bda0ed480058804e5b61 100644
--- a/groupChat/receiveRequest.go
+++ b/groupChat/receiveRequest.go
@@ -15,6 +15,7 @@ import (
 	"gitlab.com/elixxir/client/interfaces/message"
 	"gitlab.com/elixxir/client/stoppable"
 	"gitlab.com/elixxir/crypto/group"
+	"time"
 )
 
 // Error message.
@@ -26,7 +27,8 @@ const (
 
 // receiveRequest starts the group request reception worker that waits for new
 // group requests to arrive.
-func (m Manager) receiveRequest(rawMsgs chan message.Receive, stop *stoppable.Single) {
+func (m Manager) receiveRequest(rawMsgs chan message.Receive,
+	stop *stoppable.Single) {
 	jww.DEBUG.Print("Starting group message request reception worker.")
 
 	for {
@@ -36,7 +38,7 @@ func (m Manager) receiveRequest(rawMsgs chan message.Receive, stop *stoppable.Si
 			stop.ToStopped()
 			return
 		case sendMsg := <-rawMsgs:
-			jww.DEBUG.Print("Group message request received send message.")
+			jww.DEBUG.Print("Group message request received message.")
 
 			// Generate the group from the request message
 			g, err := m.readRequest(sendMsg)
@@ -49,6 +51,9 @@ func (m Manager) receiveRequest(rawMsgs chan message.Receive, stop *stoppable.Si
 			// Call request callback with the new group if it does not already
 			// exist
 			if _, exists := m.GetGroup(g.ID); !exists {
+				jww.DEBUG.Printf("Received group request from sender %s for "+
+					"group %s with ID %s.", sendMsg.Sender, g.Name, g.ID)
+
 				go m.requestFunc(g)
 			}
 		}
@@ -71,7 +76,7 @@ func (m *Manager) readRequest(msg message.Receive) (gs.Group, error) {
 	}
 
 	// Deserialize membership list
-	membership, err := group.DeserializeMembership(request.Members)
+	membership, err := group.DeserializeMembership(request.GetMembers())
 	if err != nil {
 		return gs.Group{}, errors.Errorf(deserializeMembershipErr, err)
 	}
@@ -97,15 +102,18 @@ func (m *Manager) readRequest(msg message.Receive) (gs.Group, error) {
 
 	// Copy preimages
 	var idPreimage group.IdPreimage
-	copy(idPreimage[:], request.IdPreimage)
+	copy(idPreimage[:], request.GetIdPreimage())
 	var keyPreimage group.KeyPreimage
-	copy(keyPreimage[:], request.KeyPreimage)
+	copy(keyPreimage[:], request.GetKeyPreimage())
 
 	// Create group ID and key
 	groupID := group.NewID(idPreimage, membership)
 	groupKey := group.NewKey(keyPreimage, membership)
 
+	// Convert created timestamp from nanoseconds to time.Time
+	created := time.Unix(0, request.GetCreated())
+
 	// Return the new group
-	return gs.NewGroup(request.Name, groupID, groupKey, idPreimage, keyPreimage,
-		request.Message, membership, dkl), nil
+	return gs.NewGroup(request.GetName(), groupID, groupKey, idPreimage,
+		keyPreimage, request.GetMessage(), created, membership, dkl), nil
 }
diff --git a/groupChat/receiveRequest_test.go b/groupChat/receiveRequest_test.go
index 6925853a325948b005c7c38e11f2a28621ab2b11..eda6c9e50315ba284d83ee8cc80a6b9d6bc51f17 100644
--- a/groupChat/receiveRequest_test.go
+++ b/groupChat/receiveRequest_test.go
@@ -11,54 +11,66 @@ import (
 	"github.com/golang/protobuf/proto"
 	gs "gitlab.com/elixxir/client/groupChat/groupStore"
 	"gitlab.com/elixxir/client/interfaces/message"
+	"gitlab.com/elixxir/client/interfaces/params"
 	"gitlab.com/elixxir/client/stoppable"
 	"math/rand"
+	"reflect"
 	"strings"
 	"testing"
 	"time"
 )
 
-// // Tests that the correct group is received from the request.
-// func TestManager_receiveRequest(t *testing.T) {
-// 	prng := rand.New(rand.NewSource(42))
-// 	requestChan := make(chan gs.Group)
-// 	requestFunc := func(g gs.Group) { requestChan <- g }
-// 	m, _ := newTestManagerWithStore(prng, 10, 0, requestFunc, nil, t)
-// 	g := newTestGroupWithUser(m.store.E2e().GetGroup(),
-// 		m.store.GetUser().ReceptionID, m.store.GetUser().E2eDhPublicKey,
-// 		m.store.GetUser().E2eDhPrivateKey, prng, t)
-//
-// 	requestMarshaled, err := proto.Marshal(&Request{
-// 		Name:        g.Name,
-// 		IdPreimage:  g.IdPreimage.Bytes(),
-// 		KeyPreimage: g.KeyPreimage.Bytes(),
-// 		Members:     g.Members.Serialize(),
-// 		Message:     g.InitMessage,
-// 	})
-// 	if err != nil {
-// 		t.Errorf("Failed to marshal proto message: %+v", err)
-// 	}
-//
-// 	msg := message.Receive{
-// 		Payload:     requestMarshaled,
-// 		MessageType: message.GroupCreationRequest,
-// 	}
-//
-// 	rawMessages := make(chan message.Receive)
-// 	quit := make(chan struct{})
-// 	go m.receiveRequest(rawMessages, quit)
-// 	rawMessages <- msg
-//
-// 	select {
-// 	case receivedGrp := <-requestChan:
-// 		if !reflect.DeepEqual(g, receivedGrp) {
-// 			t.Errorf("receiveRequest() failed to return the expected group."+
-// 				"\nexpected: %#v\nreceived: %#v", g, receivedGrp)
-// 		}
-// 	case <-time.NewTimer(5 * time.Millisecond).C:
-// 		t.Error("Timed out while waiting for callback.")
-// 	}
-// }
+// Tests that the correct group is received from the request.
+func TestManager_receiveRequest(t *testing.T) {
+	prng := rand.New(rand.NewSource(42))
+	requestChan := make(chan gs.Group)
+	requestFunc := func(g gs.Group) { requestChan <- g }
+	m, _ := newTestManagerWithStore(prng, 10, 0, requestFunc, nil, t)
+	g := newTestGroupWithUser(m.store.E2e().GetGroup(),
+		m.store.GetUser().ReceptionID, m.store.GetUser().E2eDhPublicKey,
+		m.store.GetUser().E2eDhPrivateKey, prng, t)
+
+	requestMarshaled, err := proto.Marshal(&Request{
+		Name:        g.Name,
+		IdPreimage:  g.IdPreimage.Bytes(),
+		KeyPreimage: g.KeyPreimage.Bytes(),
+		Members:     g.Members.Serialize(),
+		Message:     g.InitMessage,
+		Created:     g.Created.UnixNano(),
+	})
+	if err != nil {
+		t.Errorf("Failed to marshal proto message: %+v", err)
+	}
+
+	msg := message.Receive{
+		Sender:      g.Members[0].ID,
+		Payload:     requestMarshaled,
+		MessageType: message.GroupCreationRequest,
+	}
+
+	_ = m.store.E2e().AddPartner(
+		g.Members[0].ID,
+		g.Members[0].DhKey,
+		m.store.E2e().GetGroup().NewInt(2),
+		params.GetDefaultE2ESessionParams(),
+		params.GetDefaultE2ESessionParams(),
+	)
+
+	rawMessages := make(chan message.Receive)
+	quit := stoppable.NewSingle("groupReceiveRequestTestStoppable")
+	go m.receiveRequest(rawMessages, quit)
+	rawMessages <- msg
+
+	select {
+	case receivedGrp := <-requestChan:
+		if !reflect.DeepEqual(g, receivedGrp) {
+			t.Errorf("receiveRequest() failed to return the expected group."+
+				"\nexpected: %#v\nreceived: %#v", g, receivedGrp)
+		}
+	case <-time.NewTimer(5 * time.Millisecond).C:
+		t.Error("Timed out while waiting for callback.")
+	}
+}
 
 // Tests that the callback is not called when the group already exists in the
 // manager.
@@ -147,43 +159,44 @@ func TestManager_receiveRequest_SendMessageTypeError(t *testing.T) {
 	}
 }
 
-// // Unit test of readRequest.
-// func TestManager_readRequest(t *testing.T) {
-// 	m, g := newTestManager(rand.New(rand.NewSource(42)), t)
-// 	_ = m.store.E2e().AddPartner(
-// 		g.Members[0].ID,
-// 		g.Members[0].DhKey,
-// 		m.store.E2e().GetGroup().NewInt(43),
-// 		params.GetDefaultE2ESessionParams(),
-// 		params.GetDefaultE2ESessionParams(),
-// 	)
-//
-// 	requestMarshaled, err := proto.Marshal(&Request{
-// 		Name:        g.Name,
-// 		IdPreimage:  g.IdPreimage.Bytes(),
-// 		KeyPreimage: g.KeyPreimage.Bytes(),
-// 		Members:     g.Members.Serialize(),
-// 		Message:     g.InitMessage,
-// 	})
-// 	if err != nil {
-// 		t.Errorf("Failed to marshal proto message: %+v", err)
-// 	}
-//
-// 	msg := message.Receive{
-// 		Payload:     requestMarshaled,
-// 		MessageType: message.GroupCreationRequest,
-// 	}
-//
-// 	newGrp, err := m.readRequest(msg)
-// 	if err != nil {
-// 		t.Errorf("readRequest() returned an error: %+v", err)
-// 	}
-//
-// 	if !reflect.DeepEqual(g, newGrp) {
-// 		t.Errorf("readRequest() returned the wrong group."+
-// 			"\nexpected: %#v\nreceived: %#v", g, newGrp)
-// 	}
-// }
+// Unit test of readRequest.
+func TestManager_readRequest(t *testing.T) {
+	m, g := newTestManager(rand.New(rand.NewSource(42)), t)
+	_ = m.store.E2e().AddPartner(
+		g.Members[0].ID,
+		g.Members[0].DhKey,
+		m.store.E2e().GetGroup().NewInt(2),
+		params.GetDefaultE2ESessionParams(),
+		params.GetDefaultE2ESessionParams(),
+	)
+
+	requestMarshaled, err := proto.Marshal(&Request{
+		Name:        g.Name,
+		IdPreimage:  g.IdPreimage.Bytes(),
+		KeyPreimage: g.KeyPreimage.Bytes(),
+		Members:     g.Members.Serialize(),
+		Message:     g.InitMessage,
+		Created:     g.Created.UnixNano(),
+	})
+	if err != nil {
+		t.Errorf("Failed to marshal proto message: %+v", err)
+	}
+
+	msg := message.Receive{
+		Payload:     requestMarshaled,
+		MessageType: message.GroupCreationRequest,
+	}
+
+	newGrp, err := m.readRequest(msg)
+	if err != nil {
+		t.Errorf("readRequest() returned an error: %+v", err)
+	}
+
+	if !reflect.DeepEqual(g, newGrp) {
+		t.Errorf("readRequest() returned the wrong group."+
+			"\nexpected: %#v\nreceived: %#v", g, newGrp)
+	}
+}
 
 // Error path: an error is returned if the message type is incorrect.
 func TestManager_readRequest_MessageTypeError(t *testing.T) {
diff --git a/groupChat/receive_test.go b/groupChat/receive_test.go
index 36ea8ed2dbad4c10630630f198d07d4b6a96bf54..1592f19f2185852de918a76ba17f5dd767f62018 100644
--- a/groupChat/receive_test.go
+++ b/groupChat/receive_test.go
@@ -42,7 +42,8 @@ func TestManager_receive(t *testing.T) {
 		ID:             group.MessageID{0, 1, 2, 3},
 		Payload:        contents,
 		SenderID:       sender.ID,
-		RoundTimestamp: timestamp.Local(),
+		Timestamp:      timestamp.Local(),
+		RoundTimestamp: timestamp,
 	}
 
 	// Create cMix message and get public message
@@ -51,11 +52,11 @@ func TestManager_receive(t *testing.T) {
 		t.Errorf("Failed to create new cMix message: %+v", err)
 	}
 
-	internalMsg, _ := newInternalMsg(cMixMsg.ContentsSize() - publicMinLen)
-	internalMsg.SetTimestamp(timestamp)
-	internalMsg.SetSenderID(m.gs.GetUser().ID)
-	internalMsg.SetPayload(contents)
-	expectedMsg.ID = group.NewMessageID(g.ID, internalMsg.Marshal())
+	intlMsg, _ := newInternalMsg(cMixMsg.ContentsSize() - publicMinLen)
+	intlMsg.SetTimestamp(timestamp)
+	intlMsg.SetSenderID(m.gs.GetUser().ID)
+	intlMsg.SetPayload(contents)
+	expectedMsg.ID = group.NewMessageID(g.ID, intlMsg.Marshal())
 
 	receiveChan := make(chan message.Receive, 1)
 	stop := stoppable.NewSingle("singleStoppable")
@@ -131,7 +132,8 @@ func TestManager_receive_QuitChan(t *testing.T) {
 	}
 }
 
-// Tests that Manager.readMessage returns the message data for the correct group.
+// Tests that Manager.readMessage returns the message data for the correct
+// group.
 func TestManager_readMessage(t *testing.T) {
 	// Create new test Manager and Group
 	prng := rand.New(rand.NewSource(42))
@@ -163,11 +165,16 @@ func TestManager_readMessage(t *testing.T) {
 	}
 
 	m.gs.SetUser(expectedGrp.Members[4], t)
-	g, messageID, timestamp, senderID, contents, err := m.readMessage(receiveMsg)
+	g, messageID, timestamp, senderID, contents, noFpMatch, err :=
+		m.readMessage(receiveMsg)
 	if err != nil {
 		t.Errorf("readMessage() returned an error: %+v", err)
 	}
 
+	if noFpMatch {
+		t.Error("Fingerprint did not match when it should have.")
+	}
+
 	if !reflect.DeepEqual(expectedGrp, g) {
 		t.Errorf("readMessage() returned incorrect group."+
 			"\nexpected: %#v\nreceived: %#v", expectedGrp, g)
@@ -206,7 +213,8 @@ func TestManager_readMessage_FindGroupKpError(t *testing.T) {
 	expectedTimestamp := netTime.Now()
 
 	// Create cMix message and get public message
-	cMixMsg, err := m.newCmixMsg(g, expectedContents, expectedTimestamp, g.Members[4], prng)
+	cMixMsg, err := m.newCmixMsg(
+		g, expectedContents, expectedTimestamp, g.Members[4], prng)
 	if err != nil {
 		t.Errorf("Failed to create new cMix message: %+v", err)
 	}
@@ -223,7 +231,7 @@ func TestManager_readMessage_FindGroupKpError(t *testing.T) {
 	expectedErr := strings.SplitN(findGroupKeyFpErr, "%", 2)[0]
 
 	m.gs.SetUser(g.Members[4], t)
-	_, _, _, _, _, err = m.readMessage(receiveMsg)
+	_, _, _, _, _, _, err = m.readMessage(receiveMsg)
 	if err == nil || !strings.Contains(err.Error(), expectedErr) {
 		t.Errorf("readMessage() failed to return the expected error."+
 			"\nexpected: %s\nreceived: %+v", expectedErr, err)
@@ -242,7 +250,8 @@ func TestManager_decryptMessage(t *testing.T) {
 	expectedTimestamp := netTime.Now()
 
 	// Create cMix message and get public message
-	msg, err := m.newCmixMsg(g, expectedContents, expectedTimestamp, g.Members[4], prng)
+	msg, err := m.newCmixMsg(
+		g, expectedContents, expectedTimestamp, g.Members[4], prng)
 	if err != nil {
 		t.Errorf("Failed to create new cMix message: %+v", err)
 	}
@@ -316,7 +325,7 @@ func TestManager_decryptMessage_GetCryptKeyError(t *testing.T) {
 }
 
 // Error path: an error is returned when the decrypted payload cannot be
-// unmarshaled.
+// unmarshalled.
 func TestManager_decryptMessage_UnmarshalInternalMsgError(t *testing.T) {
 	// Create new test Manager and Group
 	prng := rand.New(rand.NewSource(42))
@@ -338,11 +347,13 @@ func TestManager_decryptMessage_UnmarshalInternalMsgError(t *testing.T) {
 
 	// Modify publicMsg to have invalid payload
 	publicMsg = mapPublicMsg(publicMsg.Marshal()[:33])
-	key, err := group.NewKdfKey(g.Key, group.ComputeEpoch(timestamp), publicMsg.GetSalt())
+	key, err := group.NewKdfKey(
+		g.Key, group.ComputeEpoch(timestamp), publicMsg.GetSalt())
 	if err != nil {
 		t.Errorf("failed to create new key: %+v", err)
 	}
-	msg.SetMac(group.NewMAC(key, publicMsg.GetPayload(), g.DhKeys[*g.Members[4].ID]))
+	msg.SetMac(
+		group.NewMAC(key, publicMsg.GetPayload(), g.DhKeys[*g.Members[4].ID]))
 
 	// Check if error is correct
 	expectedErr := strings.SplitN(unmarshalInternalMsgErr, "%", 2)[0]
@@ -364,7 +375,8 @@ func Test_getCryptKey(t *testing.T) {
 	payload := []byte("payload")
 	ts := netTime.Now()
 
-	expectedKey, err := group.NewKdfKey(g.Key, group.ComputeEpoch(ts.Add(5*time.Minute)), salt)
+	expectedKey, err := group.NewKdfKey(
+		g.Key, group.ComputeEpoch(ts.Add(5*time.Minute)), salt)
 	if err != nil {
 		t.Errorf("failed to create new key: %+v", err)
 	}
diff --git a/groupChat/send.go b/groupChat/send.go
index cdb40888ee41c3489fe23f026c1f7e5d05f3caf6..916410d1759add008948cd8d748c0bcf47cd9add 100644
--- a/groupChat/send.go
+++ b/groupChat/send.go
@@ -36,41 +36,49 @@ const (
 
 // Send sends a message to all group members using Client.SendManyCMIX. The
 // send fails if the message is too long.
-func (m *Manager) Send(groupID *id.ID, message []byte) (id.Round, error) {
+func (m *Manager) Send(groupID *id.ID, message []byte) (id.Round, time.Time,
+	error) {
+	// Get the current time stripped of the monotonic clock
+	timeNow := netTime.Now().Round(0)
 
 	// Create a cMix message for each group member
-	messages, err := m.createMessages(groupID, message)
+	messages, err := m.createMessages(groupID, message, timeNow)
 	if err != nil {
-		return 0, errors.Errorf(newCmixMsgErr, err)
+		return 0, time.Time{}, errors.Errorf(newCmixMsgErr, err)
 	}
 
-	rid, _, err := m.net.SendManyCMIX(messages, params.GetDefaultCMIX())
+	param := params.GetDefaultCMIX()
+	param.IdentityPreimage = groupID[:]
+
+	rid, _, err := m.net.SendManyCMIX(messages, param)
 	if err != nil {
-		return 0, errors.Errorf(sendManyCmixErr, m.gs.GetUser().ID, groupID, err)
+		return 0, time.Time{},
+			errors.Errorf(sendManyCmixErr, m.gs.GetUser().ID, groupID, err)
 	}
 
-	jww.DEBUG.Printf("Sent message to group %s.", groupID)
+	jww.DEBUG.Printf("Sent message to %d members in group %s at %s.",
+		len(messages), groupID, timeNow)
 
-	return rid, nil
+	return rid, timeNow, nil
 }
 
 // createMessages generates a list of cMix messages and a list of corresponding
 // recipient IDs.
-func (m *Manager) createMessages(groupID *id.ID, msg []byte) (map[id.ID]format.Message, error) {
-	timeNow := netTime.Now()
+func (m *Manager) createMessages(groupID *id.ID, msg []byte,
+	timestamp time.Time) (map[id.ID]format.Message, error) {
 
 	g, exists := m.gs.Get(groupID)
 	if !exists {
 		return map[id.ID]format.Message{}, errors.Errorf(newNoGroupErr, groupID)
 	}
 
-	return m.newMessages(g, msg, timeNow)
+	return m.newMessages(g, msg, timestamp)
 }
 
 // newMessages is a private function that allows the passing in of a timestamp
 // and streamGen instead of a fastRNG.StreamGenerator for easier testing.
-func (m *Manager) newMessages(g gs.Group, msg []byte,
-	timestamp time.Time) (map[id.ID]format.Message, error) {
+func (m *Manager) newMessages(g gs.Group, msg []byte, timestamp time.Time) (
+	map[id.ID]format.Message, error) {
 	// Create list of cMix messages
 	messages := make(map[id.ID]format.Message)
 
@@ -125,15 +133,15 @@ func (m *Manager) newCmixMsg(g gs.Group, msg []byte, timestamp time.Time,
 
 	// Create three message layers
 	cmixMsg := format.NewMessage(m.store.Cmix().GetGroup().GetP().ByteLen())
-	publicMsg, internalMsg, err := newMessageParts(cmixMsg.ContentsSize())
+	pubMsg, intlMsg, err := newMessageParts(cmixMsg.ContentsSize())
 	if err != nil {
 		return cmixMsg, err
 	}
 
 	// Return an error if the message is too large to fit in the payload
-	if internalMsg.GetPayloadMaxSize() < len(msg) {
-		return cmixMsg, errors.Errorf(messageLenErr, len(msg),
-			internalMsg.GetPayloadMaxSize())
+	if intlMsg.GetPayloadMaxSize() < len(msg) {
+		return cmixMsg, errors.Errorf(
+			messageLenErr, len(msg), intlMsg.GetPayloadMaxSize())
 	}
 
 	// Generate 256-bit salt
@@ -152,13 +160,13 @@ func (m *Manager) newCmixMsg(g gs.Group, msg []byte, timestamp time.Time,
 	}
 
 	// Generate internal message
-	payload := setInternalPayload(internalMsg, timestamp, m.gs.GetUser().ID, msg)
+	payload := setInternalPayload(intlMsg, timestamp, m.gs.GetUser().ID, msg)
 
 	// Encrypt internal message
 	encryptedPayload := group.Encrypt(key, keyFp, payload)
 
 	// Generate public message
-	publicPayload := setPublicPayload(publicMsg, salt, encryptedPayload)
+	publicPayload := setPublicPayload(pubMsg, salt, encryptedPayload)
 
 	// Generate MAC
 	mac := group.NewMAC(key, encryptedPayload, g.DhKeys[*mem.ID])
@@ -174,17 +182,17 @@ func (m *Manager) newCmixMsg(g gs.Group, msg []byte, timestamp time.Time,
 // newMessageParts generates a public payload message and the internal payload
 // message. An error is returned if the messages cannot fit in the payloadSize.
 func newMessageParts(payloadSize int) (publicMsg, internalMsg, error) {
-	publicMsg, err := newPublicMsg(payloadSize)
+	pubMsg, err := newPublicMsg(payloadSize)
 	if err != nil {
-		return publicMsg, internalMsg{}, errors.Errorf(newPublicMsgErr, err)
+		return pubMsg, internalMsg{}, errors.Errorf(newPublicMsgErr, err)
 	}
 
-	internalMsg, err := newInternalMsg(publicMsg.GetPayloadSize())
+	intlMsg, err := newInternalMsg(pubMsg.GetPayloadSize())
 	if err != nil {
-		return publicMsg, internalMsg, errors.Errorf(newInternalMsgErr, err)
+		return pubMsg, intlMsg, errors.Errorf(newInternalMsgErr, err)
 	}
 
-	return publicMsg, internalMsg, nil
+	return pubMsg, intlMsg, nil
 }
 
 // newSalt generates a new salt of the specified size.
diff --git a/groupChat/sendRequests.go b/groupChat/sendRequests.go
index cd7913fcf9488d1a5927d4171f7677e99cf245c6..edcecc77f1b5516a9ed75d9481738210374b88e4 100644
--- a/groupChat/sendRequests.go
+++ b/groupChat/sendRequests.go
@@ -50,6 +50,7 @@ func (m Manager) sendRequests(g gs.Group) ([]id.Round, RequestStatus, error) {
 		KeyPreimage: g.KeyPreimage.Bytes(),
 		Members:     g.Members.Serialize(),
 		Message:     g.InitMessage,
+		Created:     g.Created.UnixNano(),
 	})
 	if err != nil {
 		return nil, NotSent, errors.Errorf(protoMarshalErr, err)
@@ -101,6 +102,9 @@ 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.",
+		len(g.Members), g.Name, g.ID)
+
 	// If all sends succeeded, return a list of roundIDs
 	return roundIdMap2List(roundIDs), AllSent, nil
 }
diff --git a/groupChat/sendRequests_test.go b/groupChat/sendRequests_test.go
index 56ca284fbc66cb78622388bd11d0454db3280bce..9c22c13fed92f6b986a6f71db63d6f9aea3a230a 100644
--- a/groupChat/sendRequests_test.go
+++ b/groupChat/sendRequests_test.go
@@ -30,6 +30,7 @@ func TestManager_ResendRequest(t *testing.T) {
 		KeyPreimage: g.KeyPreimage.Bytes(),
 		Members:     g.Members.Serialize(),
 		Message:     g.InitMessage,
+		Created:     g.Created.UnixNano(),
 	}
 
 	_, status, err := m.ResendRequest(g.ID)
@@ -108,6 +109,7 @@ func TestManager_sendRequests(t *testing.T) {
 		KeyPreimage: g.KeyPreimage.Bytes(),
 		Members:     g.Members.Serialize(),
 		Message:     g.InitMessage,
+		Created:     g.Created.UnixNano(),
 	}
 
 	_, status, err := m.sendRequests(g)
@@ -185,8 +187,8 @@ func TestManager_sendRequests_SendAllFail(t *testing.T) {
 	}
 }
 
-// Tests that Manager.sendRequests returns the correct status when some of the
-// sends fail.
+// Tests that Manager.sendRequests returns the correct status when some sends
+// fail.
 func TestManager_sendRequests_SendPartialSent(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, g := newTestManagerWithStore(prng, 10, 2, nil, nil, t)
diff --git a/groupChat/send_test.go b/groupChat/send_test.go
index 1db5cec791e7790bf519ab98030a088f423b076e..395ffc3ee9e3b640b38e88e0b2e11331d9e8036d 100644
--- a/groupChat/send_test.go
+++ b/groupChat/send_test.go
@@ -29,7 +29,7 @@ func TestManager_Send(t *testing.T) {
 	message := []byte("Group chat message.")
 	sender := m.gs.GetUser().DeepCopy()
 
-	_, err := m.Send(g.ID, message)
+	_, _, err := m.Send(g.ID, message)
 	if err != nil {
 		t.Errorf("Send() returned an error: %+v", err)
 	}
@@ -109,7 +109,7 @@ func TestManager_Send_CmixMessageError(t *testing.T) {
 	expectedErr := strings.SplitN(newCmixMsgErr, "%", 2)[0]
 
 	// Send message
-	_, err := m.Send(g.ID, make([]byte, 400))
+	_, _, err := m.Send(g.ID, make([]byte, 400))
 	if err == nil || !strings.Contains(err.Error(), expectedErr) {
 		t.Errorf("Send() failed to return the expected error."+
 			"\nexpected: %s\nreceived: %+v", expectedErr, err)
@@ -124,7 +124,7 @@ func TestManager_Send_SendManyCMIXError(t *testing.T) {
 	expectedErr := strings.SplitN(sendManyCmixErr, "%", 2)[0]
 
 	// Send message
-	_, err := m.Send(g.ID, []byte("message"))
+	_, _, err := m.Send(g.ID, []byte("message"))
 	if err == nil || !strings.Contains(err.Error(), expectedErr) {
 		t.Errorf("Send() failed to return the expected error."+
 			"\nexpected: %s\nreceived: %+v", expectedErr, err)
@@ -136,14 +136,15 @@ func TestManager_Send_SendManyCMIXError(t *testing.T) {
 	}
 }
 
-// Tests that Manager.createMessages generates the messages for the correct group.
+// Tests that Manager.createMessages generates the messages for the correct
+// group.
 func TestManager_createMessages(t *testing.T) {
 	prng := rand.New(rand.NewSource(42))
 	m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
 
 	message := []byte("Test group message.")
 	sender := m.gs.GetUser()
-	messages, err := m.createMessages(g.ID, message)
+	messages, err := m.createMessages(g.ID, message, netTime.Now())
 	if err != nil {
 		t.Errorf("createMessages() returned an error: %+v", err)
 	}
@@ -203,7 +204,8 @@ func TestManager_createMessages_InvalidGroupIdError(t *testing.T) {
 	m, _ := newTestManagerWithStore(prng, 10, 0, nil, nil, t)
 
 	// Read message and make sure the error is expected
-	_, err := m.createMessages(id.NewIdFromString("invalidID", id.Group, t), nil)
+	_, err := m.createMessages(
+		id.NewIdFromString("invalidID", id.Group, t), nil, time.Time{})
 	if err == nil || !strings.Contains(err.Error(), expectedErr) {
 		t.Errorf("createMessages() did not return the expected error."+
 			"\nexpected: %s\nreceived: %+v", expectedErr, err)
@@ -373,7 +375,8 @@ func TestGroup_newCmixMsg_SaltReaderError(t *testing.T) {
 	expectedErr := strings.SplitN(saltReadErr, "%", 2)[0]
 	m := &Manager{store: storage.InitTestingSession(t)}
 
-	_, err := m.newCmixMsg(gs.Group{}, []byte{}, time.Time{}, group.Member{}, strings.NewReader(""))
+	_, err := m.newCmixMsg(gs.Group{},
+		[]byte{}, time.Time{}, group.Member{}, strings.NewReader(""))
 	if err == nil || !strings.Contains(err.Error(), expectedErr) {
 		t.Errorf("newCmixMsg() failed to return the expected error"+
 			"\nexpected: %s\nreceived: %+v", expectedErr, err)
diff --git a/groupChat/utils_test.go b/groupChat/utils_test.go
index d43ada0dc827fcff5fe253d9d89189abf588f778..069772587c34f8b8597b8b56951b383c0c7fe752 100644
--- a/groupChat/utils_test.go
+++ b/groupChat/utils_test.go
@@ -33,6 +33,7 @@ import (
 	"gitlab.com/xx_network/primitives/id"
 	"gitlab.com/xx_network/primitives/id/ephemeral"
 	"gitlab.com/xx_network/primitives/ndf"
+	"gitlab.com/xx_network/primitives/netTime"
 	"math/rand"
 	"sync"
 	"testing"
@@ -101,7 +102,8 @@ func newTestManagerWithStore(rng *rand.Rand, numGroups int, sendErr int,
 }
 
 // getMembership returns a Membership with random members for testing.
-func getMembership(size int, uid *id.ID, pubKey *cyclic.Int, grp *cyclic.Group, prng *rand.Rand, t *testing.T) group.Membership {
+func getMembership(size int, uid *id.ID, pubKey *cyclic.Int, grp *cyclic.Group,
+	prng *rand.Rand, t *testing.T) group.Membership {
 	contacts := make([]contact.Contact, size)
 	for i := range contacts {
 		randId, _ := id.NewRandomID(prng, id.User)
@@ -123,7 +125,8 @@ func getMembership(size int, uid *id.ID, pubKey *cyclic.Int, grp *cyclic.Group,
 }
 
 // newTestGroup generates a new group with random values for testing.
-func newTestGroup(grp *cyclic.Group, privKey *cyclic.Int, rng *rand.Rand, t *testing.T) gs.Group {
+func newTestGroup(grp *cyclic.Group, privKey *cyclic.Int, rng *rand.Rand,
+	t *testing.T) gs.Group {
 	// Generate name from base 64 encoded random data
 	nameBytes := make([]byte, 16)
 	rng.Read(nameBytes)
@@ -137,7 +140,8 @@ func newTestGroup(grp *cyclic.Group, privKey *cyclic.Int, rng *rand.Rand, t *tes
 	membership := getMembership(10, id.NewIdFromString("userID", id.User, t),
 		randCycInt(rng), grp, rng, t)
 
-	dkl := gs.GenerateDhKeyList(id.NewIdFromString("userID", id.User, t), privKey, membership, grp)
+	dkl := gs.GenerateDhKeyList(
+		id.NewIdFromString("userID", id.User, t), privKey, membership, grp)
 
 	idPreimage, err := group.NewIdPreimage(rng)
 	if err != nil {
@@ -153,7 +157,7 @@ func newTestGroup(grp *cyclic.Group, privKey *cyclic.Int, rng *rand.Rand, t *tes
 	groupKey := group.NewKey(keyPreimage, membership)
 
 	return gs.NewGroup(name, groupID, groupKey, idPreimage, keyPreimage, msg,
-		membership, dkl)
+		netTime.Now(), membership, dkl)
 }
 
 // newTestGroup generates a new group with random values for testing.
@@ -187,7 +191,7 @@ func newTestGroupWithUser(grp *cyclic.Group, uid *id.ID, pubKey,
 	groupKey := group.NewKey(keyPreimage, membership)
 
 	return gs.NewGroup(name, groupID, groupKey, idPreimage, keyPreimage, msg,
-		membership, dkl)
+		netTime.Now().Round(0), membership, dkl)
 }
 
 // randCycInt returns a random cyclic int.
@@ -241,7 +245,8 @@ func (tnm *testNetworkManager) GetE2eMsg(i int) message.Send {
 	return tnm.e2eMessages[i]
 }
 
-func (tnm *testNetworkManager) SendE2E(msg message.Send, _ params.E2E, _ *stoppable.Single) ([]id.Round, e2e.MessageID, time.Time, error) {
+func (tnm *testNetworkManager) SendE2E(msg message.Send, _ params.E2E,
+	_ *stoppable.Single) ([]id.Round, e2e.MessageID, time.Time, error) {
 	tnm.Lock()
 	defer tnm.Unlock()
 
@@ -269,7 +274,8 @@ func (tnm *testNetworkManager) SendCMIX(format.Message, *id.ID, params.CMIX) (id
 	return 0, ephemeral.Id{}, nil
 }
 
-func (tnm *testNetworkManager) SendManyCMIX(messages map[id.ID]format.Message, _ params.CMIX) (id.Round, []ephemeral.Id, error) {
+func (tnm *testNetworkManager) SendManyCMIX(messages map[id.ID]format.Message,
+	_ params.CMIX) (id.Round, []ephemeral.Id, error) {
 	if tnm.sendErr == 1 {
 		return 0, nil, errors.New("SendManyCMIX error")
 	}
@@ -284,8 +290,8 @@ func (tnm *testNetworkManager) SendManyCMIX(messages map[id.ID]format.Message, _
 
 type dummyEventMgr struct{}
 
-func (d *dummyEventMgr) Report(p int, a, b, c string) {}
-func (t *testNetworkManager) GetEventManager() interfaces.EventManager {
+func (d *dummyEventMgr) Report(int, string, string, string) {}
+func (tnm *testNetworkManager) GetEventManager() interfaces.EventManager {
 	return &dummyEventMgr{}
 }
 
diff --git a/interfaces/params/CMIX.go b/interfaces/params/CMIX.go
index e4142eb5e509dd8ffce8cfd6b3e7a8d33d4fdc46..ee735d1d96026b416c2790c7a7731e5285ca0801 100644
--- a/interfaces/params/CMIX.go
+++ b/interfaces/params/CMIX.go
@@ -17,6 +17,9 @@ type CMIX struct {
 	RoundTries uint
 	Timeout    time.Duration
 	RetryDelay time.Duration
+	// an alternate identity preimage to use on send. If not set, the default
+	// for the sending identity will be used
+	IdentityPreimage []byte
 }
 
 func GetDefaultCMIX() CMIX {
diff --git a/interfaces/preimage/generate.go b/interfaces/preimage/generate.go
new file mode 100644
index 0000000000000000000000000000000000000000..476e6b7157c8953ac82a572e119f725c0a081475
--- /dev/null
+++ b/interfaces/preimage/generate.go
@@ -0,0 +1,30 @@
+package preimage
+
+import (
+	"gitlab.com/xx_network/primitives/id"
+	"golang.org/x/crypto/blake2b"
+)
+
+func Generate(data []byte, t string) []byte {
+
+	if t==Default{
+		return data
+	}
+	// Hash fingerprints
+	h, _ := blake2b.New256(nil)
+	h.Write(data)
+	h.Write([]byte(t))
+
+	// Base 64 encode hash and truncate
+	return h.Sum(nil)
+}
+
+func GenerateRequest(recipient *id.ID) []byte {
+	// Hash fingerprints
+	h, _ := blake2b.New256(nil)
+	h.Write(recipient[:])
+	h.Write([]byte(Request))
+
+	// Base 64 encode hash and truncate
+	return h.Sum(nil)
+}
diff --git a/interfaces/preimage/request.go b/interfaces/preimage/request.go
new file mode 100644
index 0000000000000000000000000000000000000000..0eb0689362e92e1f3145572f2551a45448adef54
--- /dev/null
+++ b/interfaces/preimage/request.go
@@ -0,0 +1,20 @@
+package preimage
+
+import (
+	"gitlab.com/xx_network/primitives/id"
+	"golang.org/x/crypto/blake2b"
+)
+
+func MakeRequest(uid *id.ID) []byte {
+	h, _ := blake2b.New256(nil)
+	h.Write(uid[:])
+	h.Write([]byte(Request))
+
+	// Base 64 encode hash and truncate
+	return h.Sum(nil)
+}
+
+func MakeDefault(uid *id.ID) []byte {
+	// Base 64 encode hash and truncate
+	return uid[:]
+}
diff --git a/interfaces/preimage/types.go b/interfaces/preimage/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..3588b4f24ca24ecdb5cc6840d35ade097b5468d8
--- /dev/null
+++ b/interfaces/preimage/types.go
@@ -0,0 +1,10 @@
+package preimage
+
+const (
+	Default = "default"
+	Request = "request"
+	Confirm = "confirm"
+	Rekey   = "rekey"
+	E2e     = "e2e"
+	Group   = "group"
+)
diff --git a/interfaces/user/proto.go b/interfaces/user/proto.go
new file mode 100644
index 0000000000000000000000000000000000000000..6b9cf1e3a6b338a08abd386b92772314b5f499e3
--- /dev/null
+++ b/interfaces/user/proto.go
@@ -0,0 +1,33 @@
+package user
+
+import (
+	"gitlab.com/elixxir/crypto/cyclic"
+	"gitlab.com/xx_network/crypto/signature/rsa"
+	"gitlab.com/xx_network/primitives/id"
+)
+
+type Proto struct {
+	//General Identity
+	TransmissionID   *id.ID
+	TransmissionSalt []byte
+	TransmissionRSA  *rsa.PrivateKey
+	ReceptionID      *id.ID
+	ReceptionSalt    []byte
+	ReceptionRSA     *rsa.PrivateKey
+	Precanned        bool
+	// Timestamp in which user has registered with the network
+	RegistrationTimestamp int64
+
+	RegCode string
+
+	TransmissionRegValidationSig []byte
+	ReceptionRegValidationSig    []byte
+
+	//cmix Identity
+	CmixDhPrivateKey *cyclic.Int
+	CmixDhPublicKey  *cyclic.Int
+
+	//e2e Identity
+	E2eDhPrivateKey *cyclic.Int
+	E2eDhPublicKey  *cyclic.Int
+}
diff --git a/interfaces/user/user.go b/interfaces/user/user.go
index 8788856b494cf386dbd903510680935f2b320daa..56f260d03527ce2c605c5fe781f3be3935cc106c 100644
--- a/interfaces/user/user.go
+++ b/interfaces/user/user.go
@@ -13,7 +13,6 @@ import (
 	"gitlab.com/elixxir/primitives/fact"
 	"gitlab.com/xx_network/crypto/signature/rsa"
 	"gitlab.com/xx_network/primitives/id"
-	"time"
 )
 
 type User struct {
@@ -26,7 +25,7 @@ type User struct {
 	ReceptionRSA     *rsa.PrivateKey
 	Precanned        bool
 	// Timestamp in which user has registered with the network
-	RegistrationTimestamp time.Time
+	RegistrationTimestamp int64
 
 	//cmix Identity
 	CmixDhPrivateKey *cyclic.Int
@@ -44,3 +43,20 @@ func (u User) GetContact() contact.Contact {
 		Facts:    make([]fact.Fact, 0),
 	}
 }
+
+func NewUserFromProto(proto *Proto) User {
+	return User{
+		TransmissionID:        proto.TransmissionID,
+		TransmissionSalt:      proto.TransmissionSalt,
+		TransmissionRSA:       proto.TransmissionRSA,
+		ReceptionID:           proto.ReceptionID,
+		ReceptionSalt:         proto.ReceptionSalt,
+		ReceptionRSA:          proto.ReceptionRSA,
+		Precanned:             proto.Precanned,
+		RegistrationTimestamp: proto.RegistrationTimestamp,
+		CmixDhPrivateKey:      proto.CmixDhPrivateKey,
+		CmixDhPublicKey:       proto.CmixDhPublicKey,
+		E2eDhPrivateKey:       proto.E2eDhPrivateKey,
+		E2eDhPublicKey:        proto.E2eDhPublicKey,
+	}
+}
diff --git a/keyExchange/rekey.go b/keyExchange/rekey.go
index 5acc9ba71f383b5e8ae15966421ecd227ed7f6e7..f549d7a57a69674565618048828b3a8586a42717 100644
--- a/keyExchange/rekey.go
+++ b/keyExchange/rekey.go
@@ -62,8 +62,10 @@ func trigger(instance *network.Instance, sendE2E interfaces.SendE2E,
 			"negotiating status: %s", session, session.NegotiationStatus())
 	}
 
+	rekeyPreimage := manager.GetRekeyPreimage()
+
 	// send the rekey notification to the partner
-	err := negotiate(instance, sendE2E, sess, negotiatingSession, sendTimeout, stop)
+	err := negotiate(instance, sendE2E, sess, negotiatingSession, sendTimeout, rekeyPreimage, stop)
 	// if sending the negotiation fails, revert the state of the session to
 	// unconfirmed so it will be triggered in the future
 	if err != nil {
@@ -74,7 +76,7 @@ func trigger(instance *network.Instance, sendE2E interfaces.SendE2E,
 
 func negotiate(instance *network.Instance, sendE2E interfaces.SendE2E,
 	sess *storage.Session, session *e2e.Session, sendTimeout time.Duration,
-	stop *stoppable.Single) error {
+	rekeyPreimage []byte, stop *stoppable.Single) error {
 	e2eStore := sess.E2e()
 
 	//generate public key
@@ -103,6 +105,7 @@ func negotiate(instance *network.Instance, sendE2E interfaces.SendE2E,
 	//send the message under the key exchange
 	e2eParams := params.GetDefaultE2E()
 	e2eParams.Type = params.KeyExchange
+	e2eParams.IdentityPreimage = rekeyPreimage
 
 	rounds, _, _, err := sendE2E(m, e2eParams, stop)
 	// If the send fails, returns the error so it can be handled. The caller
diff --git a/keyExchange/trigger.go b/keyExchange/trigger.go
index 0bdcbcc6e6ca80a28b58f53bbf32f22a2b0d0d41..b4f6eb69c95707e1d330b255d10df6d3f6ae83e5 100644
--- a/keyExchange/trigger.go
+++ b/keyExchange/trigger.go
@@ -122,6 +122,7 @@ func handleTrigger(sess *storage.Session, net interfaces.NetworkManager,
 
 	//send the message under the key exchange
 	e2eParams := params.GetDefaultE2E()
+	e2eParams.IdentityPreimage = partner.GetRekeyPreimage()
 
 	// store in critical messages buffer first to ensure it is resent if the
 	// send fails
diff --git a/network/follow.go b/network/follow.go
index ca0634be7f36a44e00c487046e488ee57525a55b..50f98f0aca1371723361d838942a60b611ef575a 100644
--- a/network/follow.go
+++ b/network/follow.go
@@ -29,6 +29,7 @@ import (
 	"gitlab.com/elixxir/client/interfaces"
 	"gitlab.com/elixxir/client/network/rounds"
 	"gitlab.com/elixxir/client/stoppable"
+	rounds2 "gitlab.com/elixxir/client/storage/rounds"
 	pb "gitlab.com/elixxir/comms/mixmessages"
 	"gitlab.com/elixxir/primitives/knownRounds"
 	"gitlab.com/elixxir/primitives/states"
@@ -110,6 +111,16 @@ func (m *manager) follow(report interfaces.ClientErrorReport, rng csprng.Source,
 			"impossible: %+v", err)
 	}
 
+	// While polling with a fake identity, it is necessary to have
+	// populated earliestRound data. However, as with fake identities
+	// we want the values to be randomly generated rather than based on
+	// actual state.
+	if identity.Fake {
+		fakeEr := &rounds2.EarliestRound{}
+		fakeEr.Set(m.GetFakeEarliestRound())
+		identity.ER = fakeEr
+	}
+
 	atomic.AddUint64(m.tracker, 1)
 
 	// Get client version for poll
@@ -126,6 +137,7 @@ func (m *manager) follow(report interfaces.ClientErrorReport, rng csprng.Source,
 		EndTimestamp:   identity.EndValid.UnixNano(),
 		ClientVersion:  []byte(version.String()),
 		FastPolling:    m.param.FastPolling,
+		LastRound:      uint64(identity.ER.Get()),
 	}
 
 	result, err := m.GetSender().SendToAny(func(host *connect.Host) (interface{}, error) {
@@ -305,6 +317,7 @@ func (m *manager) follow(report interfaces.ClientErrorReport, rng csprng.Source,
 	// move the earliest unknown round tracker forward to the earliest
 	// tracked round if it is behind
 	earliestTrackedRound := id.Round(pollResp.EarliestRound)
+	m.SetFakeEarliestRound(earliestTrackedRound)
 	updated, old, _ := identity.ER.Set(earliestTrackedRound)
 	if old == 0 {
 		if gwRoundsState.GetLastChecked() > id.Round(m.param.KnownRoundsThreshold) {
@@ -321,11 +334,15 @@ func (m *manager) follow(report interfaces.ClientErrorReport, rng csprng.Source,
 	//threshold is the earliest round that will not be excluded from earliest remaining
 	earliestRemaining, roundsWithMessages, roundsUnknown := gwRoundsState.RangeUnchecked(updated,
 		m.param.KnownRoundsThreshold, roundChecker)
+	jww.DEBUG.Printf("Processed RangeUnchecked, Oldest: %d, firstUnchecked: %d, "+
+		"last Checked: %d, threshold: %d, NewEarliestRemaning: %d, NumWithMessages: %d, "+
+		"NumUnknown: %d", updated, gwRoundsState.GetFirstUnchecked(), gwRoundsState.GetLastChecked(),
+		m.param.KnownRoundsThreshold, earliestRemaining, len(roundsWithMessages), len(roundsUnknown))
 
 	_, _, changed := identity.ER.Set(earliestRemaining)
 	if changed {
 		jww.TRACE.Printf("External returns of RangeUnchecked: %d, %v, %v", earliestRemaining, roundsWithMessages, roundsUnknown)
-		jww.DEBUG.Printf("New Earliest Remaining: %d", earliestRemaining)
+		jww.DEBUG.Printf("New Earliest Remaining: %d, Gateways last checked: %d", earliestRemaining, gwRoundsState.GetLastChecked())
 	}
 
 	roundsWithMessages2 := identity.UR.Iterate(func(rid id.Round) bool {
diff --git a/network/gateway/sender.go b/network/gateway/sender.go
index 9d2f3d3ad9fa96f3dee1ea987ef4e5b9f55635dd..c3c7bff8259feb2f477d658791c39c42e079064c 100644
--- a/network/gateway/sender.go
+++ b/network/gateway/sender.go
@@ -53,7 +53,8 @@ func (s *Sender) SendToAny(sendFunc func(host *connect.Host) (interface{}, error
 			// Retry of the proxy could not communicate
 			jww.INFO.Printf("Unable to SendToAny via %s: non-fatal error received, retrying: %s",
 				proxies[proxy].GetId().String(), err)
-		} else if strings.Contains(err.Error(), "unable to connect to target host") {
+		} else if strings.Contains(err.Error(), "unable to connect to target host") ||
+			strings.Contains(err.Error(), "unable to find target host") {
 			// Retry of the proxy could not communicate
 			jww.WARN.Printf("Unable to SendToAny via %s: %s,"+
 				" proxy could not contact requested host",
@@ -94,7 +95,8 @@ func (s *Sender) SendToPreferred(targets []*id.ID,
 			// Retry of the proxy could not communicate
 			jww.INFO.Printf("Unable to to SendToPreferred first pass %s via %s: non-fatal error received, retrying: %s",
 				targets[i], targetHosts[i].GetId(), err)
-		} else if strings.Contains(err.Error(), "unable to connect to target host") {
+		} else if strings.Contains(err.Error(), "unable to connect to target host") ||
+			strings.Contains(err.Error(), "unable to find target host") {
 			// Retry of the proxy could not communicate
 			jww.WARN.Printf("Unable to SendToPreferred first pass %s via %s: %s, "+
 				"proxy could not contact requested host",
@@ -157,7 +159,8 @@ func (s *Sender) SendToPreferred(targets []*id.ID,
 				// Retry of the proxy could not communicate
 				jww.INFO.Printf("Unable to SendToPreferred second pass %s via %s: non-fatal error received, retrying: %s",
 					target, proxy, err)
-			} else if strings.Contains(err.Error(), "unable to connect to target host") {
+			} else if strings.Contains(err.Error(), "unable to connect to target host") ||
+				strings.Contains(err.Error(), "unable to find target host") {
 				// Retry of the proxy could not communicate
 				jww.WARN.Printf("Unable to SendToPreferred second pass %s via %s: %s,"+
 					" proxy could not contact requested host",
diff --git a/network/manager.go b/network/manager.go
index 35f3c668247586618ea3d52e442b3800bc2efd76..bb69e30a5154ec09e4d029d6c797171d088e01fb 100644
--- a/network/manager.go
+++ b/network/manager.go
@@ -11,8 +11,11 @@ package network
 // and intraclient state are accessible through the context object.
 
 import (
+	"crypto/rand"
+	"encoding/binary"
 	"fmt"
 	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/interfaces"
 	"gitlab.com/elixxir/client/interfaces/params"
 	"gitlab.com/elixxir/client/network/ephemeral"
@@ -28,11 +31,20 @@ import (
 	"gitlab.com/elixxir/comms/client"
 	"gitlab.com/elixxir/comms/network"
 	"gitlab.com/elixxir/crypto/fastRNG"
+	"gitlab.com/xx_network/crypto/csprng"
+	"gitlab.com/xx_network/primitives/id"
 	"gitlab.com/xx_network/primitives/ndf"
 	"math"
+	"sync/atomic"
 	"time"
 )
 
+// fakeIdentityRange indicates the range generated between
+// 0 (most current) and fakeIdentityRange rounds behind the earliest known
+// round that will be used as the earliest round when polling with a
+// fake identity.
+const fakeIdentityRange = 800
+
 // Manager implements the NetworkManager interface inside context. It
 // controls access to network resources and implements all the communications
 // functions used by the client.
@@ -49,6 +61,9 @@ type manager struct {
 	round   *rounds.Manager
 	message *message.Manager
 
+	// Earliest tracked round
+	earliestRound *uint64
+
 	//number of polls done in a period of time
 	tracker       *uint64
 	latencySum    uint64
@@ -81,13 +96,14 @@ func NewManager(session *storage.Session, switchboard *switchboard.Switchboard,
 	session.E2e().SetE2ESessionParams(params.E2EParams)
 
 	tracker := uint64(0)
-
+	earliest := uint64(0)
 	// create manager object
 	m := manager{
 		param:     params,
 		tracker:   &tracker,
 		addrSpace: ephemeral.NewAddressSpace(),
 		events:    events,
+		earliestRound: &earliest,
 	}
 
 	if params.VerboseRoundTracking {
@@ -241,3 +257,22 @@ func (m *manager) GetVerboseRounds() string {
 	}
 	return m.verboseRounds.String()
 }
+
+
+func (m *manager) SetFakeEarliestRound(rnd id.Round)   {
+	atomic.StoreUint64(m.earliestRound, uint64(rnd))
+}
+
+// GetFakeEarliestRound generates a random earliest round for a fake identity.
+func (m *manager) GetFakeEarliestRound() id.Round   {
+	b, err := csprng.Generate(8, rand.Reader)
+	if err != nil {
+		jww.FATAL.Panicf("Could not get random number: %v", err)
+	}
+
+	rangeVal :=  binary.LittleEndian.Uint64(b) % 800
+
+	earliestKnown := atomic.LoadUint64(m.earliestRound)
+
+	return id.Round(earliestKnown - rangeVal)
+}
diff --git a/network/message/handler.go b/network/message/handler.go
index ed6891a755a885f186cb7352b60c588b61eb6d13..838be7875a696a15f3074aee5fae07ce3b1f0679 100644
--- a/network/message/handler.go
+++ b/network/message/handler.go
@@ -11,7 +11,9 @@ import (
 	"fmt"
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/interfaces/message"
+	"gitlab.com/elixxir/client/interfaces/preimage"
 	"gitlab.com/elixxir/client/stoppable"
+	"gitlab.com/elixxir/client/storage/edge"
 	"gitlab.com/elixxir/crypto/e2e"
 	fingerprint2 "gitlab.com/elixxir/crypto/fingerprint"
 	"gitlab.com/elixxir/primitives/format"
@@ -21,6 +23,7 @@ import (
 )
 
 func (m *Manager) handleMessages(stop *stoppable.Single) {
+	preimageList := m.Session.GetEdge()
 	for {
 		select {
 		case <-stop.Quit():
@@ -28,7 +31,7 @@ func (m *Manager) handleMessages(stop *stoppable.Single) {
 			return
 		case bundle := <-m.messageReception:
 			for _, msg := range bundle.Messages {
-				m.handleMessage(msg, bundle)
+				m.handleMessage(msg, bundle, preimageList)
 			}
 			bundle.Finish()
 		}
@@ -36,7 +39,7 @@ func (m *Manager) handleMessages(stop *stoppable.Single) {
 
 }
 
-func (m *Manager) handleMessage(ecrMsg format.Message, bundle Bundle) {
+func (m *Manager) handleMessage(ecrMsg format.Message, bundle Bundle, edge *edge.Store) {
 	// We've done all the networking, now process the message
 	fingerprint := ecrMsg.GetKeyFP()
 	msgDigest := ecrMsg.Digest()
@@ -50,15 +53,21 @@ func (m *Manager) handleMessage(ecrMsg format.Message, bundle Bundle) {
 	var err error
 	var relationshipFingerprint []byte
 
-	//check if the identity fingerprint matches
-	forMe := fingerprint2.CheckIdentityFP(ecrMsg.GetIdentityFP(),
-		ecrMsg.GetContents(), identity.Source)
+	//if it exists, check against all in the list
+	has, forMe, _ := m.Session.GetEdge().Check(identity.Source, ecrMsg.GetIdentityFP(), ecrMsg.GetContents())
+	if !has {
+		jww.INFO.Printf("checking backup %v", preimage.MakeDefault(identity.Source))
+		//if it doesnt exist, check against the default fingerprint for the identity
+		forMe = fingerprint2.CheckIdentityFP(ecrMsg.GetIdentityFP(),
+			ecrMsg.GetContents(), preimage.MakeDefault(identity.Source))
+	}
+
 	if !forMe {
 		if jww.GetLogThreshold() == jww.LevelTrace {
 			expectedFP := fingerprint2.IdentityFP(ecrMsg.GetContents(),
-				identity.Source)
+				preimage.MakeDefault(identity.Source))
 			jww.TRACE.Printf("Message for %d (%s) failed identity "+
-				"check: %v (expected) vs %v (received)", identity.EphId,
+				"check: %v (expected-default) vs %v (received)", identity.EphId,
 				identity.Source, expectedFP, ecrMsg.GetIdentityFP())
 		}
 
diff --git a/network/message/sendCmix.go b/network/message/sendCmix.go
index bfb57a56903cb9f4f23e46cae2c59657c0d06da1..5847e4e15631424d0b4165d11d49e0ec980cf98d 100644
--- a/network/message/sendCmix.go
+++ b/network/message/sendCmix.go
@@ -135,7 +135,7 @@ func sendCmixHelper(sender *gateway.Sender, msg format.Message,
 		stream := rng.GetStream()
 
 		wrappedMsg, encMsg, ephID, err := buildSlotMessage(msg, recipient,
-			firstGateway, stream, senderId, bestRound, roundKeys)
+			firstGateway, stream, senderId, bestRound, roundKeys, cmixParams)
 		if err != nil {
 			stream.Close()
 			return 0, ephemeral.Id{}, err
diff --git a/network/message/sendCmixUtils.go b/network/message/sendCmixUtils.go
index 4e71f5731f775386d9ab3afa1d10316214d67b32..b889b0ad86a7a09e7df10ab1ea72bd03541a4057 100644
--- a/network/message/sendCmixUtils.go
+++ b/network/message/sendCmixUtils.go
@@ -10,6 +10,8 @@ package message
 import (
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/interfaces/params"
+	preimage2 "gitlab.com/elixxir/client/interfaces/preimage"
 	"gitlab.com/elixxir/client/storage"
 	"gitlab.com/elixxir/client/storage/cmix"
 	pb "gitlab.com/elixxir/comms/mixmessages"
@@ -119,7 +121,7 @@ func processRound(instance *network.Instance, session *storage.Session,
 // the recipient.
 func buildSlotMessage(msg format.Message, recipient *id.ID, target *id.ID,
 	stream *fastRNG.Stream, senderId *id.ID, bestRound *pb.RoundInfo,
-	roundKeys *cmix.RoundKeys) (*pb.GatewaySlot, format.Message, ephemeral.Id,
+	roundKeys *cmix.RoundKeys, param params.CMIX) (*pb.GatewaySlot, format.Message, ephemeral.Id,
 	error) {
 
 	// Set the ephemeral ID
@@ -139,11 +141,23 @@ func buildSlotMessage(msg format.Message, recipient *id.ID, target *id.ID,
 
 	msg.SetEphemeralRID(ephIdFilled[:])
 
+	// use the alternate identity preimage if it is set
+	var preimage []byte
+	if param.IdentityPreimage != nil {
+		preimage = param.IdentityPreimage
+		jww.INFO.Printf("Sending to %s with override preimage %v", recipient, preimage)
+	} else {
+		preimage = preimage2.MakeDefault(recipient)
+		jww.INFO.Printf("Sending to %s with default preimage %v", recipient, preimage)
+	}
+
 	// Set the identity fingerprint
-	ifp := fingerprint.IdentityFP(msg.GetContents(), recipient)
+	ifp := fingerprint.IdentityFP(msg.GetContents(), preimage)
 
 	msg.SetIdentityFP(ifp)
 
+	jww.INFO.Printf(" Sending to %s with preimage %v, ifp: %v, contents: %v", recipient, preimage, ifp, msg.GetContents())
+
 	// Encrypt the message
 	salt := make([]byte, 32)
 	_, err = stream.Read(salt)
diff --git a/network/message/sendE2E.go b/network/message/sendE2E.go
index 5f42729f651de44afa707d10b52d2d5c83caf428..52d202393cc8817f2bf9ffd8acace69fb9df72eb 100644
--- a/network/message/sendE2E.go
+++ b/network/message/sendE2E.go
@@ -93,6 +93,11 @@ func (m *Manager) SendE2E(msg message.Send, param params.E2E,
 		jww.INFO.Printf("E2E sending %d/%d to %s with msgDigest: %s, key fp: %s",
 			i+i, len(partitions), msg.Recipient, msgEnc.Digest(), key.Fingerprint())
 
+		//set the preimage to the default e2e one if it is not already set
+		if param.IdentityPreimage == nil {
+			param.IdentityPreimage = partner.GetE2EPreimage()
+		}
+
 		//send the cmix message, each partition in its own thread
 		wg.Add(1)
 		go func(i int) {
diff --git a/network/message/sendManyCmix.go b/network/message/sendManyCmix.go
index 3c212f745846c2b60747517d3759a0d019e5528d..31e9ef78ccc7baea47909d687a2b0671994511e4 100644
--- a/network/message/sendManyCmix.go
+++ b/network/message/sendManyCmix.go
@@ -103,6 +103,7 @@ func sendManyCmixHelper(sender *gateway.Sender, msgs map[id.ID]format.Message,
 		firstGateway, roundKeys, err := processRound(instance, session,
 			nodeRegistration, bestRound, recipientString, msgDigests)
 		if err != nil {
+			jww.INFO.Printf("error processing round: %v", err)
 			jww.WARN.Printf("SendManyCMIX failed to process round %d "+
 				"(will retry): %+v", bestRound.ID, err)
 			continue
@@ -115,8 +116,9 @@ func sendManyCmixHelper(sender *gateway.Sender, msgs map[id.ID]format.Message,
 		i := 0
 		for recipient, msg := range msgs {
 			slots[i], encMsgs[i], ephemeralIds[i], err = buildSlotMessage(
-				msg, &recipient, firstGateway, stream, senderId, bestRound, roundKeys)
+				msg, &recipient, firstGateway, stream, senderId, bestRound, roundKeys, param)
 			if err != nil {
+				jww.INFO.Printf("error building slot received: %v", err)
 				return 0, []ephemeral.Id{}, errors.Errorf("failed to build "+
 					"slot message for %s: %+v", recipient, err)
 			}
@@ -148,7 +150,8 @@ func sendManyCmixHelper(sender *gateway.Sender, msgs map[id.ID]format.Message,
 				err := handlePutMessageError(firstGateway, instance,
 					session, nodeRegistration, recipientString, bestRound, err)
 				return result, errors.WithMessagef(err,
-					"SendManyCMIX %s", unrecoverableError)
+					"SendManyCMIX %s (via %s): %s",
+					target, host, unrecoverableError)
 
 			}
 			return result, err
@@ -161,9 +164,10 @@ func sendManyCmixHelper(sender *gateway.Sender, msgs map[id.ID]format.Message,
 				jww.ERROR.Printf("SendManyCMIX failed to send to EphIDs [%s] "+
 					"(sources: %s) on round %d, trying a new round %+v",
 					ephemeralIdsString, recipientString, bestRound.ID, err)
+				jww.INFO.Printf("error received, continuing: %v", err)
 				continue
 			}
-
+			jww.INFO.Printf("error received: %v", err)
 			return 0, []ephemeral.Id{}, err
 		}
 
diff --git a/storage/e2e/manager.go b/storage/e2e/manager.go
index a978beeae77c2fffa723c15f495ebf1fa4925c5c..68119395dc81f3c3dff50dfcfdd5b5d8b6da9ea4 100644
--- a/storage/e2e/manager.go
+++ b/storage/e2e/manager.go
@@ -14,13 +14,13 @@ import (
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/interfaces/params"
+	"gitlab.com/elixxir/client/interfaces/preimage"
 	"gitlab.com/elixxir/client/storage/utility"
 	"gitlab.com/elixxir/client/storage/versioned"
 	"gitlab.com/elixxir/crypto/cyclic"
 	dh "gitlab.com/elixxir/crypto/diffieHellman"
 	"gitlab.com/xx_network/primitives/id"
 	"golang.org/x/crypto/blake2b"
-	"sort"
 )
 
 const managerPrefix = "Manager{partner:%s}"
@@ -228,10 +228,22 @@ const relationshipFpLength = 15
 // relationship. The fingerprint is a base 64 encoded hash of of the two
 // relationship fingerprints truncated to 15 characters.
 func (m *Manager) GetRelationshipFingerprint() string {
+
+	// Base 64 encode hash and truncate
+	return base64.StdEncoding.EncodeToString(m.GetRelationshipFingerprintBytes())[:relationshipFpLength]
+}
+
+// GetRelationshipFingerprintBytes returns a unique fingerprint for an E2E
+// relationship. used for the e2e preimage.
+func (m *Manager) GetRelationshipFingerprintBytes() []byte {
 	// Sort fingerprints
-	fps := [][]byte{m.receive.fingerprint, m.send.fingerprint}
-	less := func(i, j int) bool { return bytes.Compare(fps[i], fps[j]) == -1 }
-	sort.Slice(fps, less)
+	var fps [][]byte
+
+	if bytes.Compare(m.receive.fingerprint, m.send.fingerprint) == 1 {
+		fps = [][]byte{m.send.fingerprint, m.receive.fingerprint}
+	} else {
+		fps = [][]byte{m.receive.fingerprint, m.send.fingerprint}
+	}
 
 	// Hash fingerprints
 	h, _ := blake2b.New256(nil)
@@ -240,5 +252,17 @@ func (m *Manager) GetRelationshipFingerprint() string {
 	}
 
 	// Base 64 encode hash and truncate
-	return base64.StdEncoding.EncodeToString(h.Sum(nil))[:relationshipFpLength]
+	return h.Sum(nil)
+}
+
+// GetE2EPreimage returns a hash of the unique
+// fingerprint for an E2E relationship message.
+func (m *Manager) GetE2EPreimage() []byte {
+	return preimage.Generate(m.GetRelationshipFingerprintBytes(), preimage.E2e)
+}
+
+// GetRekeyPreimage returns a hash of the unique
+// fingerprint for an E2E rekey message.
+func (m *Manager) GetRekeyPreimage() []byte {
+	return preimage.Generate(m.GetRelationshipFingerprintBytes(), preimage.Rekey)
 }
diff --git a/storage/edge/edge.go b/storage/edge/edge.go
new file mode 100644
index 0000000000000000000000000000000000000000..a720055d73f95dcb8c70455cb16a40a3ba0f81b9
--- /dev/null
+++ b/storage/edge/edge.go
@@ -0,0 +1,266 @@
+package edge
+
+import (
+	"encoding/json"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/storage/versioned"
+	fingerprint2 "gitlab.com/elixxir/crypto/fingerprint"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/netTime"
+	"sync"
+)
+
+// This stores Preimages which can be used with the identity fingerprint system.
+
+const (
+	edgeStorePrefix  = "edgeStore"
+	edgeStoreKey     = "edgeStoreKey"
+	edgeStoreVersion = 0
+)
+
+type ListUpdateCallBack func(identity *id.ID, deleted bool)
+
+type Store struct {
+	kv        *versioned.KV
+	edge      map[id.ID]Preimages
+	callbacks map[id.ID][]ListUpdateCallBack
+	mux       sync.RWMutex
+}
+
+// NewStore creates a new edge store object and inserts the default Preimages
+// for the base identity.
+func NewStore(kv *versioned.KV, baseIdentity *id.ID) (*Store, error) {
+	kv = kv.Prefix(edgeStorePrefix)
+
+	s := &Store{
+		kv:        kv,
+		edge:      make(map[id.ID]Preimages),
+		callbacks: make(map[id.ID][]ListUpdateCallBack),
+	}
+
+	defaultPreimages := newPreimages(baseIdentity)
+	err := defaultPreimages.save(kv, baseIdentity)
+	if err != nil {
+		return nil, errors.WithMessage(err, "Failed to create preimage store, "+
+			"failed to create default Preimages")
+	}
+
+	s.edge[*baseIdentity] = defaultPreimages
+
+	return s, s.save()
+}
+
+// Add adds the Preimage to the list of the given identity and calls any
+// associated callbacks.
+func (s *Store) Add(preimage Preimage, identity *id.ID) {
+	s.mux.Lock()
+	defer s.mux.Unlock()
+
+	// Get the list to update, create if needed
+	preimages, exists := s.edge[*identity]
+	if !exists {
+		preimages = newPreimages(identity)
+	}
+
+	// Add to the list
+	if !preimages.add(preimage) {
+		return
+	}
+
+	// Store the updated list
+	if err := preimages.save(s.kv, identity); err != nil {
+		jww.FATAL.Panicf("Failed to store preimages list after adding "+
+			"preimage %v to identity %s: %+v", preimage.Data, identity, err)
+	}
+
+	// Update the map
+	s.edge[*identity] = preimages
+	if !exists {
+		err := s.save()
+		if err != nil {
+			jww.FATAL.Panicf("Failed to store edge store after adding "+
+				"preimage %v to identity %s: %+v", preimage.Data, identity, err)
+		}
+	}
+
+	// Call any callbacks to notify
+	for _, cb := range s.callbacks[*identity] {
+		go cb(identity, false)
+	}
+
+	return
+}
+
+// Remove deletes the preimage for the given identity and triggers the
+// associated callback. If the given preimage is the last in the Preimages list,
+// then the entire list is removed and the associated callback will be triggered
+// with the boolean indicating the list was deleted.
+func (s *Store) Remove(preimage Preimage, identity *id.ID) error {
+	s.mux.Lock()
+	defer s.mux.Unlock()
+
+	preimages, exists := s.edge[*identity]
+	if !exists {
+		return errors.Errorf("cannot delete preimage %v from identity %s; "+
+			"identity cannot be found", preimage.Data, identity)
+	}
+
+	preimages.remove(preimage.Data)
+
+	if len(preimages) == 0 {
+		delete(s.edge, *identity)
+		if err := s.save(); err != nil {
+			jww.FATAL.Panicf("Failed to store edge store after removing "+
+				"preimage %v to identity %s: %+v", preimage.Data, identity, err)
+		}
+
+		if err := preimages.delete(s.kv, identity); err != nil {
+			jww.FATAL.Panicf("Failed to delete preimage list store after "+
+				"removing preimage %v to identity %s: %+v", preimage.Data,
+				identity, err)
+		}
+
+		// Call any callbacks to notify
+		for i := range s.callbacks[*identity] {
+			cb := s.callbacks[*identity][i]
+			go cb(identity, true)
+		}
+
+		return nil
+	}
+
+	if err := preimages.save(s.kv, identity); err != nil {
+		jww.FATAL.Panicf("Failed to store preimage list store after removing "+
+			"preimage %v to identity %s: %+v", preimage.Data, identity, err)
+	}
+
+	s.edge[*identity] = preimages
+
+	// Call any callbacks to notify
+	for i := range s.callbacks[*identity] {
+		cb := s.callbacks[*identity][i]
+		go cb(identity, false)
+	}
+
+	return nil
+}
+
+// Get returns the Preimages list for the given identity.
+func (s *Store) Get(identity *id.ID) ([]Preimage, bool) {
+	s.mux.RLock()
+	defer s.mux.RUnlock()
+
+	preimages, exists := s.edge[*identity]
+	if !exists {
+		return nil, false
+	}
+
+	preiamgesSlice := make([]Preimage, 0, len(preimages))
+
+	for _, preimage := range preimages {
+		preiamgesSlice = append(preiamgesSlice, preimage)
+	}
+	return preiamgesSlice, exists
+}
+
+// Check looks checks if the identity fingerprint matches for any of
+// the stored preimages. It returns the preimage it hit with if it
+// finds one.
+func (s *Store) Check(identity *id.ID, identityFP []byte, messageContents []byte) (bool, bool, Preimage) {
+	s.mux.RLock()
+	defer s.mux.RUnlock()
+
+	preimages, exists := s.edge[*identity]
+	if !exists {
+		return false, false, Preimage{}
+	}
+
+	for _, preimage := range preimages {
+		jww.INFO.Printf("checking  ifp: %v, msg: %v, preimage %v", identityFP, messageContents, preimage)
+		if fingerprint2.CheckIdentityFP(identityFP, messageContents, preimage.Data) {
+			return true, true, preimage
+		}
+	}
+
+	return true, false, Preimage{}
+}
+
+// AddUpdateCallback adds the callback to be called for changes to the identity.
+func (s *Store) AddUpdateCallback(identity *id.ID, luCB ListUpdateCallBack) {
+	s.mux.Lock()
+	defer s.mux.Unlock()
+
+	s.callbacks[*identity] = append(s.callbacks[*identity], luCB)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Storage Functions                                                          //
+////////////////////////////////////////////////////////////////////////////////
+
+func LoadStore(kv *versioned.KV) (*Store, error) {
+	kv = kv.Prefix(edgeStorePrefix)
+
+	// Load the list of identities with preimage lists
+	obj, err := kv.Get(edgeStoreKey, preimageStoreVersion)
+	if err != nil {
+		return nil, errors.WithMessagef(err, "failed to load edge store")
+	}
+
+	identities := make([]id.ID, 0)
+
+	err = json.Unmarshal(obj.Data, &identities)
+	if err != nil {
+		return nil, errors.WithMessagef(err, "failed to unmarshal edge store")
+	}
+
+	s := &Store{
+		kv:        kv,
+		edge:      make(map[id.ID]Preimages),
+		callbacks: make(map[id.ID][]ListUpdateCallBack),
+	}
+
+	// Load the preimage lists for all identities
+	for i := range identities {
+		eid := &identities[i]
+
+		preimages, err := loadPreimages(kv, eid)
+		if err != nil {
+			return nil, err
+		}
+
+		s.edge[*eid] = preimages
+	}
+
+	return s, nil
+}
+
+func (s *Store) save() error {
+	identities := make([]id.ID, 0, len(s.edge))
+
+	for eid := range s.edge {
+		identities = append(identities, eid)
+	}
+
+	// JSON marshal
+	data, err := json.Marshal(&identities)
+	if err != nil {
+		return errors.WithMessagef(err, "Failed to marshal edge list for "+
+			"storage")
+	}
+
+	// Construct versioning object
+	obj := versioned.Object{
+		Version:   edgeStoreVersion,
+		Timestamp: netTime.Now(),
+		Data:      data,
+	}
+
+	// Save to storage
+	err = s.kv.Set(edgeStoreKey, preimageStoreVersion, &obj)
+	if err != nil {
+		return errors.WithMessagef(err, "Failed to store edge list")
+	}
+
+	return nil
+}
diff --git a/storage/edge/edge_test.go b/storage/edge/edge_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..c7823245f47a29d839622a63200ea46dfcba9185
--- /dev/null
+++ b/storage/edge/edge_test.go
@@ -0,0 +1,619 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package edge
+
+import (
+	"encoding/json"
+	"gitlab.com/elixxir/client/interfaces/preimage"
+	"gitlab.com/elixxir/client/storage/versioned"
+	fingerprint2 "gitlab.com/elixxir/crypto/fingerprint"
+	"gitlab.com/elixxir/ekv"
+	"gitlab.com/xx_network/primitives/id"
+	"math/rand"
+	"reflect"
+	"sync"
+	"testing"
+	"time"
+)
+
+// Tests that NewStore returns the expected new Store and that it can be loaded
+// from storage.
+func TestNewStore(t *testing.T) {
+	kv := versioned.NewKV(make(ekv.Memstore))
+	baseIdentity := id.NewIdFromString("baseIdentity", id.User, t)
+	expected := &Store{
+		kv:        kv.Prefix(edgeStorePrefix),
+		edge:      map[id.ID]Preimages{*baseIdentity: newPreimages(baseIdentity)},
+		callbacks: make(map[id.ID][]ListUpdateCallBack),
+	}
+
+	received, err := NewStore(kv, baseIdentity)
+	if err != nil {
+		t.Errorf("NewStore returned an error: %+v", err)
+	}
+
+	if !reflect.DeepEqual(expected, received) {
+		t.Errorf("New Store does not match expected."+
+			"\nexpected: %+v\nreceived: %+v", expected, received)
+	}
+
+	_, err = expected.kv.Get(preimagesKey(baseIdentity), preimageStoreVersion)
+	if err != nil {
+		t.Errorf("Failed to load Store from storage: %+v", err)
+	}
+}
+
+// Adds three Preimage to the store, two with the same identity. It checks that
+// Store.Add adds all three exist and that the length of the list is correct.
+// Also checks that the appropriate callbacks are called.
+func TestStore_Add(t *testing.T) {
+	s, _, _ := newTestStore(t)
+	identities := []*id.ID{
+		id.NewIdFromString("identity0", id.User, t),
+		id.NewIdFromString("identity1", id.User, t),
+	}
+	preimages := []Preimage{
+		{[]byte("ID0"), "default0", []byte("ID0")},
+		{[]byte("ID1"), "default1", []byte("ID1")},
+		{[]byte("ID2"), "default2", []byte("ID2")},
+	}
+
+	var wg sync.WaitGroup
+
+	id0Chan := make(chan struct {
+		identity *id.ID
+		deleted  bool
+	}, 2)
+	s.callbacks[*identities[0]] = []ListUpdateCallBack{
+		func(identity *id.ID, deleted bool) {
+			id0Chan <- struct {
+				identity *id.ID
+				deleted  bool
+			}{identity: identity, deleted: deleted}
+		}}
+
+	wg.Add(1)
+	wg.Add(1)
+	go func() {
+		for i := 0; i < 2; i++ {
+			select {
+			case <-time.NewTimer(50 * time.Millisecond).C:
+				t.Errorf("Timed out waiting for callback (%d).", i)
+			case r := <-id0Chan:
+				if !identities[0].Cmp(r.identity) {
+					t.Errorf("Received wrong identity (%d).\nexpected: %s"+
+						"\nreceived: %s", i, identities[0], r.identity)
+				} else if r.deleted == true {
+					t.Errorf("Received wrong value for deleted (%d)."+
+						"\nexpected: %t\nreceived: %t", i, true, r.deleted)
+				}
+			}
+			wg.Done()
+		}
+	}()
+
+	id1Chan := make(chan struct {
+		identity *id.ID
+		deleted  bool
+	})
+	s.callbacks[*identities[1]] = []ListUpdateCallBack{
+		func(identity *id.ID, deleted bool) {
+			id1Chan <- struct {
+				identity *id.ID
+				deleted  bool
+			}{identity: identity, deleted: deleted}
+		}}
+
+	wg.Add(1)
+	go func() {
+		select {
+		case <-time.NewTimer(10 * time.Millisecond).C:
+			t.Errorf("Timed out waiting for callback.")
+		case r := <-id1Chan:
+			if !identities[1].Cmp(r.identity) {
+				t.Errorf("Received wrong identity.\nexpected: %s\nreceived: %s",
+					identities[1], r.identity)
+			} else if r.deleted == true {
+				t.Errorf("Received wrong value for deleted."+
+					"\nexpected: %t\nreceived: %t", true, r.deleted)
+			}
+		}
+		wg.Done()
+	}()
+
+	s.Add(preimages[0], identities[0])
+	s.Add(preimages[1], identities[1])
+	s.Add(preimages[2], identities[0])
+
+	if len(s.edge) != 3 {
+		t.Errorf("Length of edge incorrect.\nexpected: %d\nreceived: %d",
+			3, len(s.edge))
+	}
+
+	pis := s.edge[*identities[0]]
+
+	if len(pis) != 3 {
+		t.Errorf("Length of preimages for identity %s inocrrect."+
+			"\nexpected: %d\nreceived: %d", identities[0], 3, len(pis))
+	}
+
+	expected := Preimage{preimage.MakeDefault(identities[0]), preimage.Default, identities[0].Bytes()}
+	if !reflect.DeepEqual(pis[expected.key()], expected) {
+		t.Errorf("First Preimage of first Preimages does not match expected."+
+			"\nexpected: %+v\nreceived: %+v", expected, pis[expected.key()])
+	}
+
+	expected = preimages[0]
+	if !reflect.DeepEqual(pis[expected.key()], expected) {
+		t.Errorf("Second Preimage of first Preimages does not match expected."+
+			"\nexpected: %+v\nreceived: %+v", expected, pis[expected.key()])
+	}
+
+	expected = preimages[2]
+	if !reflect.DeepEqual(pis[expected.key()], expected) {
+		t.Errorf("Third Preimage of first Preimages does not match expected."+
+			"\nexpected: %+v\nreceived: %+v", expected, pis[expected.key()])
+	}
+
+	pis = s.edge[*identities[1]]
+
+	if len(pis) != 2 {
+		t.Errorf("Length of preimages for identity %s inocrrect."+
+			"\nexpected: %d\nreceived: %d", identities[1], 2, len(pis))
+	}
+
+	expected = Preimage{preimage.MakeDefault(identities[1]), preimage.Default, identities[1].Bytes()}
+	if !reflect.DeepEqual(pis[expected.key()], expected) {
+		t.Errorf("First Preimage of second Preimages does not match expected."+
+			"\nexpected: %+v\nreceived: %+v", expected, pis[expected.key()])
+	}
+
+	expected = preimages[1]
+	if !reflect.DeepEqual(pis[expected.key()], expected) {
+		t.Errorf("Second Preimage of second Preimages does not match expected."+
+			"\nexpected: %+v\nreceived: %+v", expected, pis[expected.key()])
+	}
+
+	wg.Wait()
+}
+
+// Adds three Preimage to two identities and tests that Store.Remove removes all
+// three blue the default preimage for the second identity and checks that all
+// Preimage have been deleted, that the Preimages for the second identity has
+// been deleted and that the callbacks are called with the expected values.
+func TestStore_Remove(t *testing.T) {
+	s, _, _ := newTestStore(t)
+	identities := []*id.ID{
+		id.NewIdFromString("identity0", id.User, t),
+		id.NewIdFromString("identity1", id.User, t),
+	}
+	preimages := []Preimage{
+		{[]byte("ID0"), "default0", []byte("ID0")},
+		{[]byte("ID1"), "default1", []byte("ID1")},
+		{[]byte("ID2"), "default2", []byte("ID2")},
+	}
+
+	s.Add(preimages[0], identities[0])
+	s.Add(preimages[1], identities[1])
+	s.Add(preimages[2], identities[0])
+
+	var wg sync.WaitGroup
+
+	id0Chan := make(chan struct {
+		identity *id.ID
+		deleted  bool
+	}, 2)
+	s.callbacks[*identities[0]] = []ListUpdateCallBack{
+		func(identity *id.ID, deleted bool) {
+			id0Chan <- struct {
+				identity *id.ID
+				deleted  bool
+			}{identity: identity, deleted: deleted}
+		}}
+
+	wg.Add(1)
+	wg.Add(1)
+	go func() {
+		for i := 0; i < 2; i++ {
+			select {
+			case <-time.NewTimer(50 * time.Millisecond).C:
+				t.Errorf("Timed out waiting for callback (%d).", i)
+			case r := <-id0Chan:
+				if !identities[0].Cmp(r.identity) {
+					t.Errorf("Received wrong identity (%d).\nexpected: %s"+
+						"\nreceived: %s", i, identities[0], r.identity)
+				} else if r.deleted == true {
+					t.Errorf("Received wrong value for deleted (%d)."+
+						"\nexpected: %t\nreceived: %t", i, true, r.deleted)
+				}
+			}
+			wg.Done()
+		}
+	}()
+
+	id1Chan := make(chan struct {
+		identity *id.ID
+		deleted  bool
+	})
+	s.callbacks[*identities[1]] = []ListUpdateCallBack{
+		func(identity *id.ID, deleted bool) {
+			id1Chan <- struct {
+				identity *id.ID
+				deleted  bool
+			}{identity: identity, deleted: deleted}
+		}}
+
+	wg.Add(1)
+	wg.Add(1)
+	go func() {
+		for i := 0; i < 2; i++ {
+			select {
+			case <-time.NewTimer(50 * time.Millisecond).C:
+				t.Errorf("Timed out waiting for callback (%d).", i)
+			case r := <-id1Chan:
+				if !identities[1].Cmp(r.identity) {
+					t.Errorf("Received wrong identity (%d).\nexpected: %s"+
+						"\nreceived: %s", i, identities[1], r.identity)
+				}
+			}
+			wg.Done()
+		}
+	}()
+
+	err := s.Remove(preimages[0], identities[0])
+	if err != nil {
+		t.Errorf("Remove returned an error: %+v", err)
+	}
+
+	err = s.Remove(preimages[1], identities[1])
+	if err != nil {
+		t.Errorf("Remove returned an error: %+v", err)
+	}
+
+	err = s.Remove(Preimage{Data: identities[1].Bytes()}, identities[1])
+	if err != nil {
+		t.Errorf("Remove returned an error: %+v", err)
+	}
+
+	err = s.Remove(preimages[2], identities[0])
+	if err != nil {
+		t.Errorf("Remove returned an error: %+v", err)
+	}
+
+	if len(s.edge) != 2 {
+		t.Errorf("Length of edge incorrect.\nexpected: %d\nreceived: %d",
+			2, len(s.edge))
+	}
+
+	pis := s.edge[*identities[0]]
+
+	if len(pis) != 1 {
+		t.Errorf("Length of preimages for identity %s inocrrect."+
+			"\nexpected: %d\nreceived: %d", identities[0], 1, len(pis))
+	}
+
+	expected := preimages[0]
+	if _, exists := pis[expected.key()]; exists {
+		t.Errorf("Second Preimage of first Preimages exists when it should " +
+			"have been deleted.")
+	}
+
+	expected = preimages[2]
+	if _, exists := pis[expected.key()]; exists {
+		t.Errorf("Third Preimage of first Preimages exists when it should " +
+			"have been deleted.")
+	}
+
+	pis = s.edge[*identities[1]]
+
+	if len(pis) != 0 {
+		t.Errorf("Length of preimages for identity %s inocrrect."+
+			"\nexpected: %d\nreceived: %d", identities[1], 0, len(pis))
+	}
+
+	wg.Wait()
+}
+
+// Tests that Store.Get returns the expected Preimages.
+func TestStore_Get(t *testing.T) {
+	s, _, _ := newTestStore(t)
+	identities := []*id.ID{
+		id.NewIdFromString("identity0", id.User, t),
+		id.NewIdFromString("identity1", id.User, t),
+	}
+	preimages := []Preimage{
+		{[]byte("ID0"), "default0", []byte("ID0")},
+		{[]byte("ID1"), "default1", []byte("ID1")},
+		{[]byte("ID2"), "default2", []byte("ID2")},
+	}
+
+	s.Add(preimages[0], identities[0])
+	s.Add(preimages[1], identities[1])
+	s.Add(preimages[2], identities[0])
+
+	pis, exists := s.Get(identities[0])
+	if !exists {
+		t.Errorf("No Preimages found for identity %s.", identities[0])
+	}
+
+	expected := []Preimage{
+		{preimage.MakeDefault(identities[0]), preimage.Default, identities[0].Bytes()},
+		preimages[0],
+		preimages[2],
+	}
+
+	if len(expected) != len(pis) {
+		t.Errorf("First Preimages for identity %s does not match expected, difrent lengths of %d and %d"+
+			"\nexpected: %+v\nreceived: %+v", identities[0], len(expected), len(pis), expected, pis)
+	}
+
+top:
+	for i, lookup := range expected {
+		for _, checked := range pis {
+			if reflect.DeepEqual(lookup, checked) {
+				continue top
+			}
+		}
+		t.Errorf("Entree %d in expected %v not found in received %v", i, lookup, pis)
+	}
+
+	pis, exists = s.Get(identities[1])
+	if !exists {
+		t.Errorf("No Preimages found for identity %s.", identities[1])
+	}
+
+	expected = []Preimage{
+		{preimage.MakeDefault(identities[1]), preimage.Default, identities[1].Bytes()},
+		preimages[1],
+	}
+
+	if len(expected) != len(pis) {
+		t.Errorf("First Preimages for identity %s does not match expected, difrent lengths of %d and %d"+
+			"\nexpected: %+v\nreceived: %+v", identities[0], len(expected), len(pis), expected, pis)
+	}
+
+top2:
+	for i, lookup := range expected {
+		for _, checked := range pis {
+			if reflect.DeepEqual(lookup, checked) {
+				continue top2
+			}
+		}
+		t.Errorf("Entree %d in expected %v not found in received %v", i, lookup, pis)
+	}
+}
+
+// Tests that Store.AddUpdateCallback adds all the appropriate callbacks for
+// each identity by calling each callback and checking if the received identity
+// is correct.
+func TestStore_AddUpdateCallback(t *testing.T) {
+	s, _, _ := newTestStore(t)
+	// Create list of n identities, each with one more callback than the last
+	// with the first having one
+	n := 3
+	chans := make(map[id.ID][]chan *id.ID, n)
+	for i := 0; i < n; i++ {
+		identity := id.NewIdFromUInt(uint64(i), id.User, t)
+		chans[*identity] = make([]chan *id.ID, i+1)
+		for j := range chans[*identity] {
+			cbChan := make(chan *id.ID, 2)
+			cb := func(cbIdentity *id.ID, _ bool) { cbChan <- cbIdentity }
+			chans[*identity][j] = cbChan
+			s.AddUpdateCallback(identity, cb)
+		}
+	}
+
+	var wg sync.WaitGroup
+	for identity, chanList := range chans {
+		for i := range chanList {
+			wg.Add(1)
+			go func(identity *id.ID, i int) {
+				select {
+				case <-time.NewTimer(150 * time.Millisecond).C:
+					t.Errorf("Timed out waiting on callback %d/%d for "+
+						"identity %s.", i+1, len(chans[*identity]), identity)
+				case r := <-chans[*identity][i]:
+					if !identity.Cmp(r) {
+						t.Errorf("Identity received from callback %d/%d does "+
+							"not match expected.\nexpected: %s\nreceived: %s",
+							i+1, len(chans[*identity]), identity, r)
+					}
+				}
+				wg.Done()
+			}(identity.DeepCopy(), i)
+		}
+	}
+
+	for identity, cbs := range chans {
+		for i := range cbs {
+			go s.callbacks[identity][i](identity.DeepCopy(), false)
+		}
+	}
+
+	wg.Wait()
+}
+
+func TestLoadStore(t *testing.T) {
+	// Initialize store
+	s, kv, _ := newTestStore(t)
+	identities := []*id.ID{
+		id.NewIdFromString("identity0", id.User, t),
+		id.NewIdFromString("identity1", id.User, t),
+	}
+	preimages := []Preimage{
+		{[]byte("ID0"), "default0", []byte("ID0")},
+		{[]byte("ID1"), "default1", []byte("ID1")},
+		{[]byte("ID2"), "default2", []byte("ID2")},
+	}
+
+	// Add preimages
+	s.Add(preimages[0], identities[0])
+	s.Add(preimages[1], identities[1])
+	s.Add(preimages[2], identities[0])
+
+	err := s.save()
+	if err != nil {
+		t.Fatalf("save error: %v", err)
+	}
+
+	receivedStore, err := LoadStore(kv)
+	if err != nil {
+		t.Fatalf("LoadStore error: %v", err)
+	}
+
+	expectedPis := [][]Preimage{
+		{
+			Preimage{preimage.MakeDefault(identities[0]), preimage.Default, identities[0].Bytes()},
+			preimages[0],
+			preimages[2],
+		},
+		{
+			Preimage{preimage.MakeDefault(identities[1]), preimage.Default, identities[1].Bytes()},
+			preimages[1],
+		},
+	}
+
+	for i, identity := range identities {
+		pis, exists := receivedStore.Get(identity)
+		if !exists {
+			t.Errorf("Identity %s does not exist in loaded store", identity)
+		}
+
+		if len(expectedPis[i]) != len(pis) {
+			t.Errorf("First Preimages for identity %s does not match expected, difrent lengths of %d and %d"+
+				"\nexpected: %+v\nreceived: %+v", identities[0], len(expectedPis[i]), len(pis), expectedPis[i], pis)
+		}
+
+	top:
+		for idx, lookup := range expectedPis[i] {
+			for _, checked := range pis {
+				if reflect.DeepEqual(lookup, checked) {
+					continue top
+				}
+			}
+			t.Errorf("Entree %d in expected %v not found in received %v", idx, lookup, pis)
+		}
+
+	}
+}
+
+func TestStore_Check(t *testing.T) {
+	// Initialize store
+	s, _, _ := newTestStore(t)
+	identities := []*id.ID{
+		id.NewIdFromString("identity0", id.User, t),
+		id.NewIdFromString("identity1", id.User, t),
+	}
+	preimages := []Preimage{
+		{[]byte("ID0"), "default0", []byte("ID0")},
+		{[]byte("ID1"), "default1", []byte("ID1")},
+		{[]byte("ID2"), "default2", []byte("ID2")},
+	}
+
+	// Add preimages
+	s.Add(preimages[0], identities[0])
+	s.Add(preimages[1], identities[1])
+	s.Add(preimages[2], identities[0])
+
+	testMsg := []byte("test message 123")
+	preImageData := preimages[0].Data
+	testFp := fingerprint2.IdentityFP(testMsg, preImageData)
+
+	has, forMe, receivedPreImage := s.Check(identities[0], testFp, testMsg)
+
+	if !has || !forMe || !reflect.DeepEqual(receivedPreImage, preimages[0]) {
+		t.Errorf("Unexpected result from Check()."+
+			"\nExpected results: (has: %v) "+
+			"\n\t(forMe: %v)"+
+			"\n\t(Preimage: %v)"+
+			"\nReceived results: (has: %v) "+
+			"\n\t(forME: %v)"+
+			"\n\t(Preimage: %v)", true, true, preimages[0],
+			has, forMe, receivedPreImage)
+	}
+
+	// Check with wrong identity (has should be true, for me false)
+	has, forMe, _ = s.Check(identities[1], testFp, testMsg)
+	if !has || forMe {
+		t.Errorf("Unexpected results from check."+
+			"\nExpected results: (has: %v)"+
+			"\n\t(ForMe %v)"+
+			"\nReceived results: "+
+			"has: %v"+
+			"\n\t(ForMe: %v)", true, false, has, forMe)
+	}
+
+}
+
+func TestStore_save(t *testing.T) {
+	// Initialize store
+	s, _, _ := newTestStore(t)
+	identities := []*id.ID{
+		id.NewIdFromString("identity0", id.User, t),
+		id.NewIdFromString("identity1", id.User, t),
+	}
+	preimages := []Preimage{
+		{[]byte("ID0"), "default0", []byte("ID0")},
+		{[]byte("ID1"), "default1", []byte("ID1")},
+		{[]byte("ID2"), "default2", []byte("ID2")},
+	}
+
+	s.Add(preimages[0], identities[0])
+	s.Add(preimages[1], identities[1])
+
+	// Save data to KV
+	err := s.save()
+	if err != nil {
+		t.Fatalf("save error: %v", err)
+	}
+
+	// Manually pull from KV
+	vo, err := s.kv.Get(edgeStoreKey, preimageStoreVersion)
+	if err != nil {
+		t.Fatalf("Failed to retrieve from KV: %v", err)
+	}
+
+	receivedIdentities := make([]id.ID, 0)
+	err = json.Unmarshal(vo.Data, &receivedIdentities)
+	if err != nil {
+		t.Fatalf("JSON unmarshal error: %v", err)
+	}
+
+	for _, receivedId := range receivedIdentities {
+		_, exists := s.Get(&receivedId)
+		if !exists {
+			t.Fatalf("Identity retrieved from store does not match " +
+				"identity stored in")
+		}
+	}
+}
+
+// newTestStore creates a new Store with a random base identity. Returns the
+// Store, KV, and base identity.
+func newTestStore(t *testing.T) (*Store, *versioned.KV, *id.ID) {
+	kv := versioned.NewKV(make(ekv.Memstore))
+	baseIdentity, err := id.NewRandomID(
+		rand.New(rand.NewSource(time.Now().Unix())), id.User)
+	if err != nil {
+		t.Fatalf("Failed to generate random base identity: %+v", err)
+	}
+
+	s, err := NewStore(kv, baseIdentity)
+	if err != nil {
+		t.Fatalf("Failed to create new test Store: %+v", err)
+	}
+
+	return s, kv, baseIdentity
+}
diff --git a/storage/edge/preimage.go b/storage/edge/preimage.go
new file mode 100644
index 0000000000000000000000000000000000000000..a8e4c89432dddc25596495fe69b59e7786ceab45
--- /dev/null
+++ b/storage/edge/preimage.go
@@ -0,0 +1,128 @@
+package edge
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"github.com/pkg/errors"
+	"gitlab.com/elixxir/client/interfaces/preimage"
+	"gitlab.com/elixxir/client/storage/versioned"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/netTime"
+)
+
+const (
+	preimageStoreKey     = "preimageStoreKey"
+	preimageStoreVersion = 0
+)
+
+type Preimage struct {
+	Data   []byte
+	Type   string
+	Source []byte
+}
+
+// key returns the key used to identify the Preimage in a map.
+func (pi Preimage) key() string {
+	return base64.StdEncoding.EncodeToString(pi.Data)
+}
+
+// Preimages is a map of unique Preimage keyed on their Data.
+type Preimages map[string]Preimage
+
+// newPreimages makes a Preimages object for the given identity and populates
+// it with the default preimage for the identity. Does not store to disk.
+func newPreimages(identity *id.ID) Preimages {
+	defaultPreimage := Preimage{
+		Data:   preimage.MakeDefault(identity),
+		Type:   preimage.Default,
+		Source: identity[:],
+	}
+	pis := Preimages{
+		defaultPreimage.key(): defaultPreimage,
+	}
+
+	return pis
+}
+
+// add adds the preimage to the list.
+func (pis Preimages) add(preimage Preimage) bool {
+	if _, exists := pis[preimage.key()]; exists {
+		return false
+	}
+
+	pis[preimage.key()] = preimage
+
+	return true
+}
+
+// remove deletes the Preimage with the matching data from the list.
+func (pis Preimages) remove(data []byte) {
+	key := base64.StdEncoding.EncodeToString(data)
+	delete(pis, key)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Storage Functions                                                          //
+////////////////////////////////////////////////////////////////////////////////
+
+// loadPreimages loads a Preimages object for the given identity.
+func loadPreimages(kv *versioned.KV, identity *id.ID) (Preimages, error) {
+
+	// Get the data from storage
+	obj, err := kv.Get(preimagesKey(identity), preimageStoreVersion)
+	if err != nil {
+		return nil, errors.WithMessagef(err, "Failed to load edge Preimages "+
+			"for identity %s", identity)
+	}
+
+	var preimageList Preimages
+	err = json.Unmarshal(obj.Data, &preimageList)
+	if err != nil {
+		return nil, errors.WithMessagef(err, "failed to unmarshal edge "+
+			"Preimages for identity %s", identity)
+	}
+
+	return preimageList, nil
+}
+
+// save stores the preimage list to disk.
+func (pis Preimages) save(kv *versioned.KV, identity *id.ID) error {
+	// JSON marshal
+	data, err := json.Marshal(&pis)
+	if err != nil {
+		return errors.WithMessagef(err, "Failed to marshal Preimages list "+
+			"for stroage for identity %s", identity)
+	}
+
+	// Construct versioning object
+	obj := versioned.Object{
+		Version:   preimageStoreVersion,
+		Timestamp: netTime.Now(),
+		Data:      data,
+	}
+
+	// Save to storage
+	err = kv.Set(preimagesKey(identity), preimageStoreVersion, &obj)
+	if err != nil {
+		return errors.WithMessagef(err, "Failed to store Preimages list for "+
+			"identity %s", identity)
+	}
+
+	return nil
+}
+
+// delete removes the Preimages from storage.
+func (pis Preimages) delete(kv *versioned.KV, identity *id.ID) error {
+	err := kv.Delete(preimagesKey(identity), preimageStoreVersion)
+	if err != nil {
+		return errors.WithMessagef(err, "Failed to delete Preimages list for "+
+			"identity %s", identity)
+	}
+
+	return nil
+}
+
+// preimagesKey generates the key for saving a Preimages to storage.
+func preimagesKey(identity *id.ID) string {
+	return preimageStoreKey + ":" + identity.String()
+}
diff --git a/storage/edge/preimage_test.go b/storage/edge/preimage_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..b31cc4274075344ad705b5308e7ac30e93bac5d0
--- /dev/null
+++ b/storage/edge/preimage_test.go
@@ -0,0 +1,246 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package edge
+
+import (
+	"bytes"
+	"encoding/json"
+	"gitlab.com/elixxir/client/interfaces/preimage"
+	"gitlab.com/elixxir/client/storage/versioned"
+	"gitlab.com/elixxir/ekv"
+	"gitlab.com/xx_network/primitives/id"
+	"reflect"
+	"testing"
+)
+
+// Tests that newPreimages returns the expected new Preimages.
+func Test_newPreimages(t *testing.T) {
+	identity := id.NewIdFromString("identity", id.User, t)
+	pimg := Preimage{
+		Data:   preimage.MakeDefault(identity),
+		Type:   "default",
+		Source: identity.Bytes(),
+	}
+	expected := Preimages{
+		pimg.key(): pimg,
+	}
+
+	received := newPreimages(identity)
+
+	if !reflect.DeepEqual(expected, received) {
+		t.Errorf("New Preimages does not match expected."+
+			"\nexpected: %+v\nreceived: %+v", expected, received)
+	}
+}
+
+// Tests that Preimages.add adds the expected Preimage to the list.
+func TestPreimages_add(t *testing.T) {
+	identity0 := id.NewIdFromString("identity0", id.User, t)
+	identity1 := id.NewIdFromString("identity1", id.User, t)
+	identity2 := id.NewIdFromString("identity3", id.User, t)
+	expected := Preimages{
+		identity0.String(): {preimage.Generate(identity0.Bytes(), preimage.Default), preimage.Default, preimage.MakeDefault(identity0)},
+		identity1.String(): {preimage.Generate(identity1.Bytes(), preimage.Group), preimage.Group, identity1.Bytes()},
+		identity2.String(): {preimage.Generate(identity2.Bytes(), preimage.Default), preimage.Default, identity2.Bytes()},
+	}
+
+	pis := newPreimages(identity0)
+	preimageOne := Preimage{preimage.Generate(identity1.Bytes(), preimage.Group), preimage.Group, identity1.Bytes()}
+	exists := pis.add(preimageOne)
+	if !exists {
+		t.Errorf("Failed to add idenetity.")
+	}
+
+	preimageTwo := Preimage{preimage.Generate(identity2.Bytes(), preimage.Default), preimage.Default, identity2.Bytes()}
+	exists = pis.add(preimageTwo)
+	if !exists {
+		t.Errorf("Failed to add idenetity.")
+	}
+
+	for identity, pimg := range expected {
+		if _, exists = pis[pimg.key()]; !exists {
+			t.Errorf("Identity %s could not be found", identity)
+		}
+	}
+
+	expectedPreimageIdentityTwo := Preimage{
+		Data:   preimage.Generate(identity2.Bytes(), preimage.Default),
+		Type:   preimage.Default,
+		Source: identity2.Bytes(),
+	}
+	// Test that nothing happens when a Preimage with the same data exists
+	exists = pis.add(Preimage{preimage.Generate(identity2.Bytes(), preimage.Default), "test", identity2.Bytes()})
+	if exists {
+		t.Errorf("Add idenetity that shoudl already exist.")
+	}
+
+	receivedPreimageIdentityTwo := pis[preimageTwo.key()]
+
+	if !reflect.DeepEqual(expectedPreimageIdentityTwo, receivedPreimageIdentityTwo) {
+		t.Errorf("Unexpected overwritting of existing identity")
+	}
+
+}
+
+// Tests that Preimages.remove removes all the correct Preimage from the list.
+func TestPreimages_remove(t *testing.T) {
+	pis := make(Preimages)
+	var identities [][]byte
+
+	// Add 10 Preimage to the list
+	for i := 0; i < 10; i++ {
+		identity := id.NewIdFromUInt(uint64(i), id.User, t)
+		pisType := preimage.Default
+		if i%2 == 0 {
+			pisType = preimage.Group
+		}
+
+		exists := pis.add(Preimage{identity.Bytes(), pisType, identity.Bytes()})
+		if !exists {
+			t.Errorf("Failed to add idenetity.")
+		}
+		identities = append(identities, identity.Bytes())
+	}
+
+	// Remove each Preimage, check if the length of the list has changed, and
+	// check that the correct Preimage was removed
+	for i, identity := range identities {
+		pis.remove(identity)
+
+		if len(pis) != len(identities)-(i+1) {
+			t.Errorf("Length of Preimages incorrect after removing %d Premiages."+
+				"\nexpected: %d\nreceived: %d", i, len(identities)-(i+1),
+				len(pis))
+		}
+
+		// Check if the correct Preimage was deleted
+		for _, pimg := range pis {
+			if bytes.Equal(pimg.Data, identity) {
+				t.Errorf("Failed to delete Preimage #%d: %+v", i, pimg)
+			}
+		}
+	}
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Storage Functions                                                          //
+////////////////////////////////////////////////////////////////////////////////
+
+// Tests that the Preimages loaded via loadPreimages matches the original saved
+// to storage.
+func Test_loadPreimages(t *testing.T) {
+	kv := versioned.NewKV(make(ekv.Memstore))
+	identity := id.NewIdFromString("identity", id.User, t)
+	pis := Preimages{
+		"a": {[]byte("identity0"), "default", []byte("identity0")},
+		"b": {[]byte("identity0"), "group", []byte("identity0")},
+		"c": {[]byte("identity1"), "default", []byte("identity1")},
+	}
+
+	err := pis.save(kv, identity)
+	if err != nil {
+		t.Errorf("Failed to save Preimages to storage: %+v", err)
+	}
+
+	loaded, err := loadPreimages(kv, identity)
+	if err != nil {
+		t.Errorf("loadPreimages returned an error: %+v", err)
+	}
+
+	if !reflect.DeepEqual(pis, loaded) {
+		t.Errorf("Loaded Preimages do not match original."+
+			"\nexpected: %+v\nreceived: %+v", pis, loaded)
+	}
+}
+
+// Tests that the data saved to storage via Preimages.save can be loaded and
+// unmarshalled and that it matches the original.
+func TestPreimages_save(t *testing.T) {
+	kv := versioned.NewKV(make(ekv.Memstore))
+	identity := id.NewIdFromString("identity", id.User, t)
+	pis := Preimages{
+		"a": {[]byte("identity0"), "default", []byte("identity0")},
+		"b": {[]byte("identity0"), "group", []byte("identity0")},
+		"c": {[]byte("identity1"), "default", []byte("identity1")},
+	}
+
+	err := pis.save(kv, identity)
+	if err != nil {
+		t.Errorf("save returned an error: %+v", err)
+	}
+
+	obj, err := kv.Get(preimagesKey(identity), preimageStoreVersion)
+	if err != nil {
+		t.Errorf("Failed to load Preimages from storage: %+v", err)
+	}
+
+	var loaded Preimages
+	err = json.Unmarshal(obj.Data, &loaded)
+	if err != nil {
+		t.Errorf("Failed to unmarshal Preimages loaded from storage: %+v", err)
+	}
+
+	if !reflect.DeepEqual(pis, loaded) {
+		t.Errorf("Loaded Preimages do not match original."+
+			"\nexpected: %+v\nreceived: %+v", pis, loaded)
+	}
+}
+
+// Tests that Preimages.delete deletes the Preimages saved to storage by
+// attempting to load them.
+func TestPreimages_delete(t *testing.T) {
+	kv := versioned.NewKV(make(ekv.Memstore))
+	identity := id.NewIdFromString("identity", id.User, t)
+	pis := Preimages{
+		"a": {[]byte("identity0"), "default", []byte("identity0")},
+		"b": {[]byte("identity0"), "group", []byte("identity0")},
+		"c": {[]byte("identity1"), "default", []byte("identity1")},
+	}
+
+	err := pis.save(kv, identity)
+	if err != nil {
+		t.Errorf("Failed to save Preimages to storage: %+v", err)
+	}
+
+	err = pis.delete(kv, identity)
+	if err != nil {
+		t.Errorf("delete returned an error: %+v", err)
+	}
+
+	loaded, err := loadPreimages(kv, identity)
+	if err == nil {
+		t.Errorf("loadPreimages loaded a Preimages from storage when it "+
+			"should have been deleted: %+v", loaded)
+	}
+}
+
+// Consistency test: tests that preimagesKey returned the expected output for a
+// set input.
+func Test_preimagesKey(t *testing.T) {
+	expectedKeys := []string{
+		"preimageStoreKey:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
+		"preimageStoreKey:ACOG8m/BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
+		"preimageStoreKey:AEcN5N+CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
+		"preimageStoreKey:AGqU109DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
+		"preimageStoreKey:AI4byb8EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
+		"preimageStoreKey:ALGivC7FAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
+		"preimageStoreKey:ANUprp6GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
+		"preimageStoreKey:APiwoQ5HAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
+		"preimageStoreKey:ARw3k34IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
+		"preimageStoreKey:AT++he3JAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
+	}
+
+	for i, expected := range expectedKeys {
+		identity := id.NewIdFromUInt(uint64(i)*1e16, id.User, t)
+		key := preimagesKey(identity)
+		if key != expected {
+			t.Errorf("Key #%d does not match expected."+
+				"\nexpected: %q\nreceived: %q", i, expected, key)
+		}
+	}
+}
diff --git a/storage/session.go b/storage/session.go
index fd98a85c22e1d1c3aea073ad3d1d9d6a709c6e1c..8ee2b43d3119b222612389685360d53b26bc9abf 100644
--- a/storage/session.go
+++ b/storage/session.go
@@ -10,6 +10,7 @@
 package storage
 
 import (
+	"gitlab.com/elixxir/client/storage/edge"
 	"gitlab.com/elixxir/client/storage/hostList"
 	"gitlab.com/elixxir/client/storage/rounds"
 	"sync"
@@ -68,6 +69,7 @@ type Session struct {
 	clientVersion       *clientVersion.Store
 	uncheckedRounds     *rounds.UncheckedRoundStore
 	hostList            *hostList.Store
+	edgeCheck           *edge.Store
 }
 
 // Initialize a new Session object
@@ -92,7 +94,7 @@ func New(baseDir, password string, u userInterface.User, currentVersion version.
 
 	s, err := initStore(baseDir, password)
 	if err != nil {
-		return nil, errors.WithMessage(err, "Failed to create session")
+		return nil, errors.WithMessagef(err, "Failed to create session for %s", baseDir)
 	}
 
 	err = s.newRegStatus()
@@ -101,7 +103,8 @@ func New(baseDir, password string, u userInterface.User, currentVersion version.
 			"Create new session")
 	}
 
-	s.user, err = user.NewUser(s.kv, u.TransmissionID, u.ReceptionID, u.TransmissionSalt, u.ReceptionSalt, u.TransmissionRSA, u.ReceptionRSA, u.Precanned)
+	s.user, err = user.NewUser(s.kv, u.TransmissionID, u.ReceptionID, u.TransmissionSalt,
+		u.ReceptionSalt, u.TransmissionRSA, u.ReceptionRSA, u.Precanned)
 	if err != nil {
 		return nil, errors.WithMessage(err, "Failed to create user")
 	}
@@ -154,6 +157,10 @@ func New(baseDir, password string, u userInterface.User, currentVersion version.
 
 	s.hostList = hostList.NewStore(s.kv)
 
+	s.edgeCheck, err = edge.NewStore(s.kv, u.ReceptionID)
+	if err != nil {
+		return nil, errors.WithMessage(err, "Failed to edge check store")
+	}
 	return s, nil
 }
 
@@ -232,6 +239,11 @@ func Load(baseDir, password string, currentVersion version.Version,
 
 	s.hostList = hostList.NewStore(s.kv)
 
+	s.edgeCheck, err = edge.LoadStore(s.kv)
+	if err != nil {
+		return nil, errors.WithMessage(err, "Failed to load edge check store")
+	}
+
 	return s, nil
 }
 
@@ -314,6 +326,13 @@ func (s *Session) HostList() *hostList.Store {
 	return s.hostList
 }
 
+// GetEdge returns the edge preimage store.
+func (s *Session) GetEdge() *edge.Store {
+	s.mux.RLock()
+	defer s.mux.RUnlock()
+	return s.edgeCheck
+}
+
 // Get an object from the session
 func (s *Session) Get(key string) (*versioned.Object, error) {
 	return s.kv.Get(key, currentSessionVersion)
@@ -417,5 +436,10 @@ func InitTestingSession(i interface{}) *Session {
 
 	s.hostList = hostList.NewStore(s.kv)
 
+	s.edgeCheck, err = edge.NewStore(s.kv, uid)
+	if err != nil {
+		jww.FATAL.Panicf("Failed to create new edge Store: %+v", err)
+	}
+
 	return s
 }
diff --git a/storage/user.go b/storage/user.go
index 975b574a2e9678a31eff7f0598b396ab6427a6af..313741471ee5e3fefa12349d2e9b441352b983e1 100644
--- a/storage/user.go
+++ b/storage/user.go
@@ -16,9 +16,9 @@ func (s *Session) GetUser() user.User {
 	return user.User{
 		TransmissionID:        ci.GetTransmissionID().DeepCopy(),
 		TransmissionSalt:      copySlice(ci.GetTransmissionSalt()),
-		TransmissionRSA:       ci.GetReceptionRSA(),
+		TransmissionRSA:       ci.GetTransmissionRSA(),
 		ReceptionID:           ci.GetReceptionID().DeepCopy(),
-		RegistrationTimestamp: s.user.GetRegistrationTimestamp(),
+		RegistrationTimestamp: s.user.GetRegistrationTimestamp().UnixNano(),
 		ReceptionSalt:         copySlice(ci.GetReceptionSalt()),
 		ReceptionRSA:          ci.GetReceptionRSA(),
 		Precanned:             ci.IsPrecanned(),
diff --git a/ud/lookup.go b/ud/lookup.go
index c5d8b4dd96757ec69b52e629ad69c131b92ed7aa..2dc2dd2a8724227423df2ff028809746aa49cc8f 100644
--- a/ud/lookup.go
+++ b/ud/lookup.go
@@ -23,9 +23,6 @@ type lookupCallback func(contact.Contact, error)
 // system or returns by the timeout.
 func (m *Manager) Lookup(uid *id.ID, callback lookupCallback, timeout time.Duration) error {
 	jww.INFO.Printf("ud.Lookup(%s, %s)", uid, timeout)
-	if !m.IsRegistered() {
-		return errors.New("Failed to lookup: client is not registered.")
-	}
 
 	// Build the request and marshal it
 	request := &LookupSend{UserID: uid.Marshal()}
diff --git a/ud/search.go b/ud/search.go
index 83f33901ca97938c4d74966f11d3909fdd227061..e8db5749ace91b445f56b7f71ace9c8b5165b886 100644
--- a/ud/search.go
+++ b/ud/search.go
@@ -28,9 +28,6 @@ type searchCallback func([]contact.Contact, error)
 // of information is known.
 func (m *Manager) Search(list fact.FactList, callback searchCallback, timeout time.Duration) error {
 	jww.INFO.Printf("ud.Search(%s, %s)", list.Stringify(), timeout)
-	if !m.IsRegistered() {
-		return errors.New("Failed to search: client is not registered.")
-	}
 
 	factHashes, factMap := hashFactList(list)