diff --git a/README.md b/README.md
index 1c169b50dd2d0a5d491a2137b2591a4d838b08f0..dc58f697ff36740f76504073c92ae3d96676b16d 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,8 @@
 # xx network Client
 
-[Repository](https://git.xx.network/elixxir/client) | [Go Doc](https://pkg.go.dev/gitlab.com/elixxir/client/xxdk) | [Examples](https://git.xx.network/elixxir/xxdk-examples/-/tree/master)
-
+[Repository](https://git.xx.network/elixxir/client)
+| [Go Doc](https://pkg.go.dev/gitlab.com/elixxir/client/xxdk)
+| [Examples](https://git.xx.network/elixxir/xxdk-examples/-/tree/master)
 
 The client is a library and related command-line tool that facilitates making full-featured xx clients for all
 platforms. It interfaces with the cMix system, enabling access to all xx network messaging features, including
@@ -11,7 +12,8 @@ This repository contains everything necessary to implement the xx network messag
 contains features to extend the base messaging protocols.
 
 The command-line tool accompanying the client library can be built for any platform supported by Go. The libraries are
-built for iOS and Android using [gomobile](https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile).
+built for iOS and Android using [gomobile](https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile). The libraries are built
+for web assembly using the repository [xxdk-wasm](https://git.xx.network/elixxir/xxdk-wasm).
 
 For library writers, the client requires a writable folder to store data, functions for receiving and approving requests
 for creating secure end-to-end messaging channels, discovering users, and receiving different types of messages.
@@ -48,7 +50,7 @@ $ GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o client.dar
 
 ### Fetching an NDF
 
-All actions performed with the client require a current 
+All actions performed with the client require a current
 [network definition file (NDF)](https://xxdk-dev.xx.network/technical-glossary/#network-definition-file-ndf). The NDF is
 downloadable from the command line or
 [via an access point](https://xxdk-dev.xx.network/quick-reference#func-downloadandverifysignedndfwithurl) in the Client
@@ -78,16 +80,17 @@ $ ./client getndf --env mainnet | jq . > ndf.json
 ```
 
 Sample content of `ndf.json`:
+
 ```json
 {
-  "Timestamp": "2021-01-29T01:19:49.227246827Z",
-  "Gateways": [
-    {
-      "Id": "BRM+Iotl6ujIGhjRddZMBdauapS7Z6jL0FJGq7IkUdYB",
-      "Address": ":8440",
-      "Tls_certificate": "-----BEGIN CERTIFICATE-----\nMIIDbDCCAlSgAwIBAgIJAOUNtZneIYECMA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNV\nBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQx\nGzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJp\ncDAeFw0xOTAzMDUxODM1NDNaFw0yOTAzMDIxODM1NDNaMGgxCzAJBgNVBAYTAlVT\nMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQxGzAZBgNV\nBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJpcDCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPP0WyVkfZA/CEd2DgKpcudn0oDh\nDwsjmx8LBDWsUgQzyLrFiVigfUmUefknUH3dTJjmiJtGqLsayCnWdqWLHPJYvFfs\nWYW0IGF93UG/4N5UAWO4okC3CYgKSi4ekpfw2zgZq0gmbzTnXcHF9gfmQ7jJUKSE\ntJPSNzXq+PZeJTC9zJAb4Lj8QzH18rDM8DaL2y1ns0Y2Hu0edBFn/OqavBJKb/uA\nm3AEjqeOhC7EQUjVamWlTBPt40+B/6aFJX5BYm2JFkRsGBIyBVL46MvC02MgzTT9\nbJIJfwqmBaTruwemNgzGu7Jk03hqqS1TUEvSI6/x8bVoba3orcKkf9HsDjECAwEA\nAaMZMBcwFQYDVR0RBA4wDIIKKi5jbWl4LnJpcDANBgkqhkiG9w0BAQUFAAOCAQEA\nneUocN4AbcQAC1+b3To8u5UGdaGxhcGyZBlAoenRVdjXK3lTjsMdMWb4QctgNfIf\nU/zuUn2mxTmF/ekP0gCCgtleZr9+DYKU5hlXk8K10uKxGD6EvoiXZzlfeUuotgp2\nqvI3ysOm/hvCfyEkqhfHtbxjV7j7v7eQFPbvNaXbLa0yr4C4vMK/Z09Ui9JrZ/Z4\ncyIkxfC6/rOqAirSdIp09EGiw7GM8guHyggE4IiZrDslT8V3xIl985cbCxSxeW1R\ntgH4rdEXuVe9+31oJhmXOE9ux2jCop9tEJMgWg7HStrJ5plPbb+HmjoX3nBO04E5\n6m52PyzMNV+2N21IPppKwA==\n-----END CERTIFICATE-----\n"
-    }
-  ]
+	"Timestamp": "2021-01-29T01:19:49.227246827Z",
+	"Gateways": [
+		{
+			"Id": "BRM+IoTl6ujIGhjRddZMBdaUapS7Z6jL0FJGq7IkUdYB",
+			"Address": ":8440",
+			"Tls_certificate": "-----BEGIN CERTIFICATE-----\nMIIDbDCCAlSgAwIBAgIJA8UNtZneIYE2MA0GCSqGSIb3DQE3BQU8MGgxCzAJBgNV\nBaYTAlVTmRMwEQYDvQQiDApDYWxpZm9ybmLhMRIwEAYfVQqHDAlDbGFyZW1vbnQx\nGzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjETMBeGA1UEAwwKKi5jbWl4LnJp\ncDAeFw0xOTAzMDUxODM1NDNaFw0yOTAzMDIxODM1NDNaMGgxCzAJBgNVBAYTAlVT\nMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQxGzAZBgNV\nBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJpcDCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPP0WyVkfZA/CEd2DgKpcudn0oDh\nDwsjmx8LBDWsUgQzyLrFiVigfUmUefknUH3dTJjmiJtGqLsayCnWdqWLHPJYvFfs\nWYW0IGF93UG/4N5UAWO4okC3CYgKSi4ekpfw2zgZq0gmbzTnXcHF9gfmQ7jJUKSE\ntJPSNzXq+PZeJTC9zJAb4Lj8QzH18rDM8DaL2y1ns0Y2Hu0edBFn/OqavBJKb/uA\nm3AEjqeOhC7EQUjVamWlTBPt40+B/6aFJX5BYm2JFkRsGBIyBVL46MvC02MgzTT9\nbJIJfwqmBaTruwemNgzGu7Jk03hqqS1TUEvSI6/x8bVoba3orcKkf9HsDjECAwEA\nAaMZMBcwFQYDVR0RBA4wDIIKKi5jbWl4LnJpcDANBgkqhkiG9w0BAQUFAAOCAQEA\nneUocN4AbcQAC1+b3To8u5UGdaGxhcGyZBlAoenRVdjXK3lTjsMdMWb4QctgNfIf\nU/zuUn2mxTmF/ekP0gCCgtleZr9+DYKU5hlXk8K10uKxGD6EvoiXZzlfeUuotgp2\nqvI3ysOm/hvCfyEkqhfHtbxjV7j7v7eQFPbvNaXbLa0yr4C4vMK/Z09Ui9JrZ/Z4\ncyIkxfC6/rOqAirSdIp09EGiw7GM8guHyggE4IiZrDslT8V3xIl985cbCxSxeW1R\ntgH4rdEXuVe9+31oJhmXOE9ux2jCop9tEJMgWg7HStrJ5plPbb+HmjoX3nBO04E5\n6m52PyzMNV+2N21IPppKwA==\n-----END CERTIFICATE-----\n"
+		}
+	]
 }
 ```
 
@@ -99,9 +102,9 @@ $ ./client getndf --help
 
 ### Sending Safe Messages Between Two (2) Users
 
-> 💡 **Note:** For information on receiving messages and troubleshooting authenticated channel requests,
-see [Receiving Messages](#receiving-messages) and
-> [Confirming authenticated channel requests](#confirming-authenticated-channel-requests).
+> 💡 **Note:** For information on receiving messages and troubleshooting authenticated channel requests, see
+> [Receiving Messages](#receiving-messages)
+> and [Confirming authenticated channel requests](#confirming-authenticated-channel-requests).
 
 To send messages with end-to-end encryption, you must first establish a connection
 or [authenticated channel](https://xxdk-dev.xx.network/technical-glossary#authenticated-channel) with the other user.
@@ -148,7 +151,7 @@ Received 0
 * `-s`: The storage directory for client session data.
 * `--writeContact`: Output the user's contact information to this file.
 * `--destfile` is used to specify the recipient. You can also use `--destid b64:...` using the user's base64 ID, which
-is printed in the logs.
+  is printed in the logs.
 * `--unsafe`: Send message without encryption (necessary whenever you have not already established an e2e channel).
 * `--unsafe-channel-creation` Auto-create and auto-accept channel requests.
 * `-m`: The message to send.
@@ -158,7 +161,7 @@ This is why we've used the `--unsafe` flag when creating the user contact files.
 However, when sending between users, the flag is dropped in exchange for `--unsafe-channel-creation`.
 
 For the authenticated channel creation to be considered "safe", the user should be prompted. You can do this by
-explicitly accepting the channel creation when sending a request with `--send-auth-request` (while excluding the 
+explicitly accepting the channel creation when sending a request with `--send-auth-request` (while excluding the
 `--unsafe-channel-creation` flag) or explicitly accepting a request with `--accept-channel`:
 
 ```shell
@@ -189,8 +192,8 @@ See [Sending Safe Messages Between Two (2) Users](#sending-safe-messages-between
 Setting up an authenticated channel between clients is a back-and-forth process that happens in sequence. One client
 sends a request and waits for the other to accept it.
 
-See the previous section, [Sending safe messages between 2 users](#sending-safe-messages-between-two-2-users), for example,
-commands showing how to set up an end-to-end connection between clients before sending messages.
+See the previous section, [Sending safe messages between 2 users](#sending-safe-messages-between-two-2-users), for
+example, commands showing how to set up an end-to-end connection between clients before sending messages.
 
 As with received messages, there is no command for checking for authenticated channel requests; you'll be notified of
 any pending requests whenever the client is run.
@@ -226,7 +229,7 @@ request. This means your client has not received the request. If one has been se
 
 Full usage of the client can be found with `client --help`:
 
-```shell
+```text
 $ ./client --help
 Runs a client for cMix anonymous communication platform
 
@@ -296,15 +299,19 @@ Flags:
 Use "client [command] --help" for more information about a command.
 ```
 
->💡 **Note:**  The client cannot be used on the xx network with pre-canned user IDs.
+> 💡 **Note:**  The client cannot be used on the xx network with pre-canned user IDs.
 
 ## Library Overview
 
-The xx client is designed to be a go library (and, by extension, a C library).
+The xx client is designed to be a Go library (and, by extension, a C library).
 
 Support is also present for Go mobile to build Android and iOS libraries. In addition, we bind all exported symbols from
 the bindings package for use on mobile platforms.
 
+This library is also supported by WebAssembly through the [xxdk-wasm](https://git.xx.network/elixxir/xxdk-wasm)
+repository. xxdk-wasm wraps the bindings package in this repository so that they can be used by Javascript when compiled
+for WebAssembly.
+
 ### Implementation Notes
 
 Clients must perform the same actions *in the same order* as shown in `cmd/root.go`. Specifically, certain handlers need
@@ -323,6 +330,10 @@ and functions exposed by the Client API.
 The main entry point for developing with the client is `xxdk/cmix` (or `bindings/cmix`). We recommend using the
 [documentation in the Go package directory](https://pkg.go.dev/gitlab.com/elixxir/client/xxdk).
 
+If you are developing with the client through the browser and Javascript, refer to the
+[xxdk-wasm](https://git.xx.network/elixxir/xxdk-wasm) repository, which wraps the `bindings/cmix` package. You may also
+want to refer to the [Go documentation](https://pkg.go.dev/gitlab.com/elixxir/xxdk-wasm/wasm).
+
 Looking at the API will, for example, show you there is a `RoundEvents` callback registration function, which lets your
 client see round events.
 
@@ -373,6 +384,7 @@ gomobile for binding should include a shell script that creates the bindings. Fo
 machine with Xcode installed.
 
 Important reference info:
+
 1. [Setting up gomobile and subcommands](https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile)
 2. [Reference cycles, type restrictions](https://pkg.go.dev/golang.org/x/mobile/cmd/gobind)
 
diff --git a/auth/confirm.go b/auth/confirm.go
index bc87fa182aace4b7c1b3a9db9544b8532ef2a963..0b7278c6e1f08b8535384d729b95c46eba650b04 100644
--- a/auth/confirm.go
+++ b/auth/confirm.go
@@ -132,7 +132,7 @@ func (s *state) confirm(partner contact.Contact, serviceTag string) (
 
 			/*send message*/
 			if err = s.store.StoreConfirmation(partner.ID, baseFmt.Marshal(),
-				mac, fp); err == nil {
+				mac, fp); err != nil {
 				jww.WARN.Printf("Failed to store confirmation for replay "+
 					"for relationship between %s and %s, cannot be replayed: %+v",
 					partner.ID, s.e2e.GetReceptionID(), err)
@@ -171,8 +171,8 @@ func sendAuthConfirm(net cmixClient, partner *id.ID,
 	}
 
 	em := fmt.Sprintf("Confirm Request with %s (msgDigest: %s) sent on round %d",
-		partner, format.DigestContents(payload), sentRound)
+		partner, format.DigestContents(payload), sentRound.ID)
 	jww.INFO.Print(em)
 	event.Report(1, "Auth", "SendConfirm", em)
-	return sentRound, nil
+	return sentRound.ID, nil
 }
diff --git a/auth/interface.go b/auth/interface.go
index b0eb8fcea491a9e48e82b6045099eda02f9f5200..c515973886b9387d3442ee6f134aa47a428bb0ea 100644
--- a/auth/interface.go
+++ b/auth/interface.go
@@ -141,7 +141,7 @@ type cmixClient interface {
 	DeleteFingerprint(identity *id.ID, fingerprint format.Fingerprint)
 	Send(recipient *id.ID, fingerprint format.Fingerprint,
 		service message.Service, payload, mac []byte, cmixParams cmix.CMIXParams) (
-		id.Round, ephemeral.Id, error)
+		rounds.Round, ephemeral.Id, error)
 }
 
 // e2eHandler is a sub-interface of e2e.Handler containing
diff --git a/auth/request.go b/auth/request.go
index 91641307430d69f9e51097124680cd4697468285..17db1b2f64572d73a7601e606d72fa1d2fb5c2b1 100644
--- a/auth/request.go
+++ b/auth/request.go
@@ -164,10 +164,10 @@ func (s *state) request(partner contact.Contact, myfacts fact.FactList,
 	}
 
 	em := fmt.Sprintf("Auth Request with %s (msgDigest: %s) sent"+
-		" on round %d", partner.ID, format.DigestContents(contents), round)
+		" on round %d", partner.ID, format.DigestContents(contents), round.ID)
 	jww.INFO.Print(em)
 	s.event.Report(1, "Auth", "RequestSent", em)
-	return round, nil
+	return round.ID, nil
 
 }
 
diff --git a/auth/state_test.go b/auth/state_test.go
index 31a85612297e22467a837ad6f9899a4c8489dd60..5dc12aca17520550e4857647e40c32bed212fcc4 100644
--- a/auth/state_test.go
+++ b/auth/state_test.go
@@ -69,8 +69,8 @@ func (mnm *mockNetManager) AddFingerprint(identity *id.ID, fingerprint format.Fi
 func (mnm *mockNetManager) DeleteFingerprint(identity *id.ID, fingerprint format.Fingerprint) {}
 func (mnm *mockNetManager) Send(recipient *id.ID, fingerprint format.Fingerprint,
 	service message.Service, payload, mac []byte, cmixParams cmix.CMIXParams) (
-	id.Round, ephemeral.Id, error) {
-	return id.Round(5), ephemeral.Id{}, nil
+	rounds.Round, ephemeral.Id, error) {
+	return rounds.Round{ID: 5}, ephemeral.Id{}, nil
 }
 
 type mockE2E struct {
diff --git a/bindings/broadcast.go b/bindings/broadcast.go
deleted file mode 100644
index 8d5255fa054ec1361a36fc10753bb8a6ada15271..0000000000000000000000000000000000000000
--- a/bindings/broadcast.go
+++ /dev/null
@@ -1,216 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-package bindings
-
-import (
-	"encoding/json"
-	"github.com/pkg/errors"
-	"gitlab.com/elixxir/client/broadcast"
-	"gitlab.com/elixxir/client/cmix"
-	"gitlab.com/elixxir/client/cmix/identity/receptionID"
-	"gitlab.com/elixxir/client/cmix/rounds"
-	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
-	"gitlab.com/xx_network/crypto/signature/rsa"
-	"gitlab.com/xx_network/primitives/id/ephemeral"
-)
-
-// Channel is a bindings-level struct encapsulating the broadcast.Channel client
-// object.
-type Channel struct {
-	ch broadcast.Channel
-}
-
-// ChannelDef is the bindings representation of an elixxir/crypto
-// broadcast.Channel object.
-//
-// Example JSON:
-//  {
-//    "Name": "My broadcast channel",
-//    "Description": "A broadcast channel for me to test things",
-//    "Salt": "gpUqW7N22sffMXsvPLE7BA==",
-//    "PubKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1DZ0NJUUN2YkZVckJKRFpqT3Y0Y0MvUHZZdXNvQkFtUTFkb3Znb044aHRuUjA2T3F3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0="
-//  }
-type ChannelDef struct {
-	Name        string
-	Description string
-	Salt        []byte
-	PubKey      []byte
-}
-
-// BroadcastMessage is the bindings representation of a broadcast message.
-//
-// BroadcastMessage Example JSON:
-//  {
-// 	 "RoundID":42,
-//   "EphID":[0,0,0,0,0,0,24,61],
-//   "RoundURL":"https://dashboard.xx.network/rounds/25?xxmessenger=true",
-//   "Payload":"SGVsbG8sIGJyb2FkY2FzdCBmcmllbmRzIQ=="
-//  }
-type BroadcastMessage struct {
-	BroadcastReport
-	Payload []byte
-}
-
-// BroadcastReport is the bindings representation of the info on how a broadcast
-// message was sent
-//
-// BroadcastReport Example JSON:
-//  {
-//	 "Rounds": [25, 26, 29],
-//   "EphID":[0,0,0,0,0,0,24,61],
-//   "RoundURL":"https://dashboard.xx.network/rounds/25?xxmessenger=true"
-//  }
-type BroadcastReport struct {
-	RoundsList
-	RoundURL string
-	EphID    ephemeral.Id
-}
-
-// BroadcastListener is the public function type bindings can use to listen for
-// broadcast messages.
-//
-// Parameters:
-//  - []byte - the JSON marshalled bytes of the BroadcastMessage object, which
-//    can be passed into WaitForRoundResult to see if the broadcast succeeded.
-type BroadcastListener interface {
-	Callback([]byte, error)
-}
-
-// NewBroadcastChannel creates a bindings-layer broadcast channel and starts
-// listening for new messages.
-//
-// Parameters:
-//  - cmixId - internal ID of cmix
-//  - channelDefinition - JSON marshalled ChannelDef object
-func NewBroadcastChannel(cmixId int, channelDefinition []byte) (*Channel, error) {
-	c, err := cmixTrackerSingleton.get(cmixId)
-	if err != nil {
-		return nil, err
-	}
-
-	def := &ChannelDef{}
-	err = json.Unmarshal(channelDefinition, def)
-	if err != nil {
-		return nil, errors.WithMessage(err, "Failed to unmarshal underlying channel definition")
-	}
-
-	channelID, err := cryptoBroadcast.NewChannelID(def.Name, def.Description, def.Salt, def.PubKey)
-	if err != nil {
-		return nil, errors.WithMessage(err, "Failed to generate channel ID")
-	}
-	chanPubLoaded, err := rsa.LoadPublicKeyFromPem(def.PubKey)
-	if err != nil {
-		return nil, errors.WithMessage(err, "Failed to load public key")
-	}
-
-	ch, err := broadcast.NewBroadcastChannel(cryptoBroadcast.Channel{
-		ReceptionID: channelID,
-		Name:        def.Name,
-		Description: def.Description,
-		Salt:        def.Salt,
-		RsaPubKey:   chanPubLoaded,
-	}, c.api.GetCmix(), c.api.GetRng())
-	if err != nil {
-		return nil, errors.WithMessage(err, "Failed to create broadcast channel client")
-	}
-
-	return &Channel{ch: ch}, nil
-}
-
-// Listen registers a BroadcastListener for a given method. This allows users to
-// handle incoming broadcast messages.
-//
-// Parameters:
-//  - l - BroadcastListener object
-//  - method - int corresponding to broadcast.Method constant, 0 for symmetric
-//    or 1 for asymmetric
-func (c *Channel) Listen(l BroadcastListener, method int) error {
-	broadcastMethod := broadcast.Method(method)
-	listen := func(payload []byte,
-		receptionID receptionID.EphemeralIdentity, round rounds.Round) {
-		l.Callback(json.Marshal(&BroadcastMessage{
-			BroadcastReport: BroadcastReport{
-				RoundsList: makeRoundsList(round.ID),
-				RoundURL:   getRoundURL(round.ID),
-				EphID:      receptionID.EphId,
-			},
-			Payload: payload,
-		}))
-	}
-	return c.ch.RegisterListener(listen, broadcastMethod)
-}
-
-// Broadcast sends a given payload over the broadcast channel using symmetric
-// broadcast.
-//
-// Returns:
-//  - []byte - the JSON marshalled bytes of the BroadcastReport object, which
-//    can be passed into Cmix.WaitForRoundResult to see if the broadcast
-//    succeeded.
-func (c *Channel) Broadcast(payload []byte) ([]byte, error) {
-	rid, eid, err := c.ch.Broadcast(payload, cmix.GetDefaultCMIXParams())
-	if err != nil {
-		return nil, err
-	}
-	return json.Marshal(BroadcastReport{
-		RoundsList: makeRoundsList(rid),
-		RoundURL:   getRoundURL(rid),
-		EphID:      eid,
-	})
-}
-
-// BroadcastAsymmetric sends a given payload over the broadcast channel using
-// asymmetric broadcast. This mode of encryption requires a private key.
-//
-// Returns:
-//  - []byte - the JSON marshalled bytes of the BroadcastReport object, which
-//    can be passed into WaitForRoundResult to see if the broadcast succeeded.
-func (c *Channel) BroadcastAsymmetric(payload, pk []byte) ([]byte, error) {
-	pkLoaded, err := rsa.LoadPrivateKeyFromPem(pk)
-	if err != nil {
-		return nil, err
-	}
-	rid, eid, err := c.ch.BroadcastAsymmetric(pkLoaded, payload, cmix.GetDefaultCMIXParams())
-	if err != nil {
-		return nil, err
-	}
-	return json.Marshal(BroadcastReport{
-		RoundsList: makeRoundsList(rid),
-		RoundURL:   getRoundURL(rid),
-		EphID:      eid,
-	})
-}
-
-// MaxPayloadSize returns the maximum possible payload size which can be
-// broadcast.
-func (c *Channel) MaxPayloadSize() int {
-	return c.ch.MaxPayloadSize()
-}
-
-// MaxAsymmetricPayloadSize returns the maximum possible payload size which can
-// be broadcast.
-func (c *Channel) MaxAsymmetricPayloadSize() int {
-	return c.ch.MaxAsymmetricPayloadSize()
-}
-
-// Get returns the result of calling json.Marshal on a ChannelDef based on the
-// underlying crypto broadcast.Channel.
-func (c *Channel) Get() ([]byte, error) {
-	def := c.ch.Get()
-	return json.Marshal(&ChannelDef{
-		Name:        def.Name,
-		Description: def.Description,
-		Salt:        def.Salt,
-		PubKey:      rsa.CreatePublicKeyPem(def.RsaPubKey),
-	})
-}
-
-// Stop stops the channel from listening for more messages.
-func (c *Channel) Stop() {
-	c.ch.Stop()
-}
diff --git a/bindings/broadcast_test.go b/bindings/broadcast_test.go
deleted file mode 100644
index bb7d93d52431b545d536b53742f49ee75ba786f4..0000000000000000000000000000000000000000
--- a/bindings/broadcast_test.go
+++ /dev/null
@@ -1,75 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-package bindings
-
-import (
-	"encoding/json"
-	"gitlab.com/elixxir/crypto/cmix"
-	"gitlab.com/xx_network/crypto/csprng"
-	"gitlab.com/xx_network/crypto/signature/rsa"
-	"gitlab.com/xx_network/primitives/id"
-	"gitlab.com/xx_network/primitives/id/ephemeral"
-	"testing"
-	"time"
-)
-
-func TestChannelDef_JSON(t *testing.T) {
-	rng := csprng.NewSystemRNG()
-	rng.SetSeed([]byte("rng"))
-	pk, _ := rsa.GenerateKey(rng, 256)
-	cd := ChannelDef{
-		Name:        "My broadcast channel",
-		Description: "A broadcast channel for me to test things",
-		Salt:        cmix.NewSalt(rng, 16),
-		PubKey:      rsa.CreatePublicKeyPem(pk.GetPublic()),
-	}
-
-	cdJson, err := json.Marshal(cd)
-	if err != nil {
-		t.Errorf("Failed to marshal channel def: %+v", err)
-	}
-	t.Log(string(cdJson))
-}
-
-func TestBroadcastMessage_JSON(t *testing.T) {
-	uid := id.NewIdFromString("zezima", id.User, t)
-	eid, _, _, err := ephemeral.GetId(uid, 16, time.Now().UnixNano())
-	if err != nil {
-		t.Errorf("Failed to form ephemeral ID: %+v", err)
-	}
-	bm := BroadcastMessage{
-		BroadcastReport: BroadcastReport{
-			RoundsList: makeRoundsList(42),
-			EphID:      eid,
-		},
-		Payload: []byte("Hello, broadcast friends!"),
-	}
-	bmJson, err := json.Marshal(bm)
-	if err != nil {
-		t.Errorf("Failed to marshal broadcast message: %+v", err)
-	}
-	t.Log(string(bmJson))
-}
-
-func TestBroadcastReport_JSON(t *testing.T) {
-	uid := id.NewIdFromString("zezima", id.User, t)
-	eid, _, _, err := ephemeral.GetId(uid, 16, time.Now().UnixNano())
-	if err != nil {
-		t.Errorf("Failed to form ephemeral ID: %+v", err)
-	}
-	br := BroadcastReport{
-		RoundsList: makeRoundsList(42),
-		EphID:      eid,
-	}
-
-	brJson, err := json.Marshal(br)
-	if err != nil {
-		t.Errorf("Failed to marshal broadcast report: %+v", err)
-	}
-	t.Log(string(brJson))
-}
diff --git a/bindings/channels.go b/bindings/channels.go
new file mode 100644
index 0000000000000000000000000000000000000000..0a6014332ec077516bc7e57d7ae36a2c58bc2f23
--- /dev/null
+++ b/bindings/channels.go
@@ -0,0 +1,1603 @@
+///////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx network SEZC                                          //
+//                                                                           //
+// Use of this source code is governed by a license that can be found in the //
+// LICENSE file                                                              //
+///////////////////////////////////////////////////////////////////////////////
+
+package bindings
+
+import (
+	"crypto/ed25519"
+	"encoding/base64"
+	"encoding/json"
+	"sync"
+	"time"
+
+	"github.com/pkg/errors"
+	"gitlab.com/elixxir/client/channels"
+	"gitlab.com/elixxir/client/cmix/rounds"
+	"gitlab.com/elixxir/client/storage/utility"
+	"gitlab.com/elixxir/client/storage/versioned"
+	"gitlab.com/elixxir/client/xxdk"
+	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/rsa"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/id/ephemeral"
+	"gitlab.com/xx_network/primitives/netTime"
+)
+
+////////////////////////////////////////////////////////////////////////////////
+// Singleton Tracker                                                          //
+////////////////////////////////////////////////////////////////////////////////
+
+// channelManagerTrackerSingleton is used to track ChannelsManager objects
+// so that they can be referenced by ID back over the bindings.
+var channelManagerTrackerSingleton = &channelManagerTracker{
+	tracked: make(map[int]*ChannelsManager),
+	count:   0,
+}
+
+// channelManagerTracker is a singleton used to keep track of extant
+// ChannelsManager objects, preventing race conditions created by passing it
+// over the bindings.
+type channelManagerTracker struct {
+	tracked map[int]*ChannelsManager
+	count   int
+	mux     sync.RWMutex
+}
+
+// make create a ChannelsManager from an [channels.Manager], assigns it a unique
+// ID, and adds it to the channelManagerTracker.
+func (cmt *channelManagerTracker) make(c channels.Manager) *ChannelsManager {
+	cmt.mux.Lock()
+	defer cmt.mux.Unlock()
+
+	chID := cmt.count
+	cmt.count++
+
+	cmt.tracked[chID] = &ChannelsManager{
+		api: c,
+		id:  chID,
+	}
+
+	return cmt.tracked[chID]
+}
+
+// get an ChannelsManager from the channelManagerTracker given its ID.
+func (cmt *channelManagerTracker) get(id int) (*ChannelsManager, error) {
+	cmt.mux.RLock()
+	defer cmt.mux.RUnlock()
+
+	c, exist := cmt.tracked[id]
+	if !exist {
+		return nil, errors.Errorf(
+			"Cannot get ChannelsManager for ID %d, does not exist", id)
+	}
+
+	return c, nil
+}
+
+// delete removes a ChannelsManager from the channelManagerTracker.
+func (cmt *channelManagerTracker) delete(id int) {
+	cmt.mux.Lock()
+	defer cmt.mux.Unlock()
+
+	delete(cmt.tracked, id)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Basic Channel API                                                          //
+////////////////////////////////////////////////////////////////////////////////
+
+// ChannelsManager is a bindings-layer struct that wraps a [channels.Manager]
+// interface.
+type ChannelsManager struct {
+	api channels.Manager
+	id  int
+}
+
+// GetID returns the channelManagerTracker ID for the ChannelsManager object.
+func (cm *ChannelsManager) GetID() int {
+	return cm.id
+}
+
+// GenerateChannelIdentity creates a new private channel identity
+// ([channel.PrivateIdentity]). The public component can be retrieved as JSON
+// via [GetPublicChannelIdentityFromPrivate].
+//
+// Parameters:
+//  - cmixID - The tracked cmix object ID. This can be retrieved using
+//    [Cmix.GetID].
+//
+// Returns:
+//  - Marshalled bytes of [channel.PrivateIdentity].
+func GenerateChannelIdentity(cmixID int) ([]byte, error) {
+	// Get user from singleton
+	user, err := cmixTrackerSingleton.get(cmixID)
+	if err != nil {
+		return nil, err
+	}
+
+	rng := user.api.GetRng().GetStream()
+	defer rng.Close()
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		return nil, err
+	}
+	return pi.Marshal(), nil
+}
+
+// ConstructIdentity constructs a [channel.Identity] from a user's public key
+// and codeset version.
+//
+// Parameters:
+//  - pubKey - The Ed25519 public key.
+//  - codesetVersion - The version of the codeset used to generate the identity.
+//
+// Returns:
+//  - JSON of [channel.Identity].
+func ConstructIdentity(pubKey []byte, codesetVersion int) ([]byte, error) {
+	identity, err := cryptoChannel.ConstructIdentity(
+		pubKey, uint8(codesetVersion))
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(identity)
+}
+
+// ImportPrivateIdentity generates a new [channel.PrivateIdentity] from exported
+// data.
+//
+// Parameters:
+//  - password - The password used to encrypt the identity.
+//  - data - The encrypted data.
+//
+// Returns:
+//  - JSON of [channel.PrivateIdentity].
+func ImportPrivateIdentity(password string, data []byte) ([]byte, error) {
+	pi, err := cryptoChannel.ImportPrivateIdentity(password, data)
+	if err != nil {
+		return nil, err
+	}
+	return pi.Marshal(), nil
+}
+
+// GetPublicChannelIdentity constructs a public identity ([channel.Identity])
+// from a bytes version and returns it JSON marshaled.
+//
+// Parameters:
+//  - marshaledPublic - Bytes of the public identity ([channel.Identity]).
+//
+// Returns:
+//  - JSON of the constructed [channel.Identity].
+func GetPublicChannelIdentity(marshaledPublic []byte) ([]byte, error) {
+	i, err := cryptoChannel.UnmarshalIdentity(marshaledPublic)
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(&i)
+}
+
+// GetPublicChannelIdentityFromPrivate returns the public identity
+// ([channel.Identity]) contained in the given private identity
+// ([channel.PrivateIdentity]).
+//
+// Parameters:
+//  - marshaledPrivate - Bytes of the private identity
+//    (channel.PrivateIdentity]).
+//
+// Returns:
+//  - JSON of the public [channel.Identity].
+func GetPublicChannelIdentityFromPrivate(marshaledPrivate []byte) ([]byte, error) {
+	pi, err := cryptoChannel.UnmarshalPrivateIdentity(marshaledPrivate)
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(&pi.Identity)
+}
+
+// NewChannelsManagerGoEventModel creates a new [ChannelsManager] from a new
+// private identity ([channel.PrivateIdentity]). This is not compatible with
+// GoMobile Bindings because it receives the go event model.
+//
+// This is for creating a manager for an identity for the first time. For
+// generating a new one channel identity, use [GenerateChannelIdentity]. To
+// reload this channel manager, use [LoadChannelsManagerGoEventModel], passing
+// in the storage tag retrieved by [ChannelsManager.GetStorageTag].
+//
+// Parameters:
+//  - cmixID - The tracked Cmix object ID. This can be retrieved using
+//    [Cmix.GetID].
+//  - privateIdentity - Bytes of a private identity ([channel.PrivateIdentity])
+//    that is generated by [GenerateChannelIdentity].
+//  - goEvent - A function that initialises and returns the event model that is
+//    not compatible with GoMobile bindings.
+func NewChannelsManagerGoEventModel(cmixID int, privateIdentity []byte,
+	goEventBuilder channels.EventModelBuilder) (*ChannelsManager, error) {
+	pi, err := cryptoChannel.UnmarshalPrivateIdentity(privateIdentity)
+	if err != nil {
+		return nil, err
+	}
+
+	// Get user from singleton
+	user, err := cmixTrackerSingleton.get(cmixID)
+	if err != nil {
+		return nil, err
+	}
+
+	// Construct new channels manager
+	m, err := channels.NewManager(pi, user.api.GetStorage().GetKV(),
+		user.api.GetCmix(), user.api.GetRng(), goEventBuilder)
+	if err != nil {
+		return nil, err
+	}
+
+	// Add channel to singleton and return
+	return channelManagerTrackerSingleton.make(m), nil
+}
+
+// LoadChannelsManagerGoEventModel loads an existing ChannelsManager. This is not
+// compatible with GoMobile Bindings because it receives the go event model.
+// This is for creating a manager for an identity for the first time.
+// The channel manager should have first been created with
+// NewChannelsManagerGoEventModel and then the storage tag can be retrieved
+// with ChannelsManager.GetStorageTag
+//
+// Parameters:
+//  - cmixID - The tracked cmix object ID. This can be retrieved using
+//    [Cmix.GetID].
+//  - storageTag - retrieved with ChannelsManager.GetStorageTag
+//  - goEvent - A function that initialises and returns the event model that is
+//    not compatible with GoMobile bindings.
+func LoadChannelsManagerGoEventModel(cmixID int, storageTag string,
+	goEventBuilder channels.EventModelBuilder) (*ChannelsManager, error) {
+
+	// Get user from singleton
+	user, err := cmixTrackerSingleton.get(cmixID)
+	if err != nil {
+		return nil, err
+	}
+
+	// Construct new channels manager
+	m, err := channels.LoadManager(storageTag, user.api.GetStorage().GetKV(),
+		user.api.GetCmix(), user.api.GetRng(), goEventBuilder)
+	if err != nil {
+		return nil, err
+	}
+
+	// Add channel to singleton and return
+	return channelManagerTrackerSingleton.make(m), nil
+}
+
+// NewChannelsManager creates a new [ChannelsManager] from a new private
+// identity ([channel.PrivateIdentity]).
+//
+// This is for creating a manager for an identity for the first time. For
+// generating a new one channel identity, use [GenerateChannelIdentity]. To
+// reload this channel manager, use [LoadChannelsManager], passing in the
+// storage tag retrieved by [ChannelsManager.GetStorageTag].
+//
+// Parameters:
+//  - cmixID - The tracked Cmix object ID. This can be retrieved using
+//    [Cmix.GetID].
+//  - privateIdentity - Bytes of a private identity ([channel.PrivateIdentity])
+//    that is generated by [GenerateChannelIdentity].
+//  - event -  An interface that contains a function that initialises and returns
+//    the event model that is bindings-compatible.
+func NewChannelsManager(cmixID int, privateIdentity []byte,
+	eventBuilder EventModelBuilder) (*ChannelsManager, error) {
+	pi, err := cryptoChannel.UnmarshalPrivateIdentity(privateIdentity)
+	if err != nil {
+		return nil, err
+	}
+
+	// Get user from singleton
+	user, err := cmixTrackerSingleton.get(cmixID)
+	if err != nil {
+		return nil, err
+	}
+
+	eb := func(path string) (channels.EventModel, error) {
+		return NewEventModel(eventBuilder.Build(path)), nil
+	}
+
+	// Construct new channels manager
+	m, err := channels.NewManager(pi, user.api.GetStorage().GetKV(),
+		user.api.GetCmix(), user.api.GetRng(), eb)
+	if err != nil {
+		return nil, err
+	}
+
+	// Add channel to singleton and return
+	return channelManagerTrackerSingleton.make(m), nil
+}
+
+// LoadChannelsManager loads an existing [ChannelsManager].
+//
+// This is for loading a manager for an identity that has already been created.
+// The channel manager should have previously been created with
+// [NewChannelsManager] and the storage is retrievable with
+// [ChannelsManager.GetStorageTag].
+//
+// Parameters:
+//  - cmixID - The tracked cmix object ID. This can be retrieved using
+//    [Cmix.GetID].
+//  - storageTag - The storage tag associated with the previously created
+//    channel manager and retrieved with [ChannelsManager.GetStorageTag].
+//  - event - An interface that contains a function that initialises and returns
+//    the event model that is bindings-compatible.
+func LoadChannelsManager(cmixID int, storageTag string,
+	eventBuilder EventModelBuilder) (*ChannelsManager, error) {
+
+	// Get user from singleton
+	user, err := cmixTrackerSingleton.get(cmixID)
+	if err != nil {
+		return nil, err
+	}
+
+	eb := func(path string) (channels.EventModel, error) {
+		return NewEventModel(eventBuilder.Build(path)), nil
+	}
+
+	// Construct new channels manager
+	m, err := channels.LoadManager(storageTag, user.api.GetStorage().GetKV(),
+		user.api.GetCmix(), user.api.GetRng(), eb)
+	if err != nil {
+		return nil, err
+	}
+
+	// Add channel to singleton and return
+	return channelManagerTrackerSingleton.make(m), nil
+}
+
+// ChannelGeneration contains information about a newly generated channel. It
+// contains the public channel info formatted in pretty print and the private
+// key for the channel in PEM format.
+//
+// Example JSON:
+//  {
+//    "Channel": "\u003cSpeakeasy-v2:Test_Channel|description:Channel description.|level:Public|secrets:8AS3SczFvAYZftWuj4ZkOM9muFPIwq/0HuVCUJgTK8w=|GpPl1510/G07J4RfdYX9J5plTX3WNLVm+uuGmCwgFeU=|5|1|mRfdUGM6WxWjjCuLzO+2+zc3BQh2zMT2CHD8ZnBwpVI=\u003e",
+//    "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\nMDECAQACBgDMIU9LpQIDAQABAgYAvCd9ewECAw0tzQIDD305AgMFu4UCAwd+kQID\nAQxc\n-----END RSA PRIVATE KEY-----"
+//  }
+type ChannelGeneration struct {
+	Channel    string
+	PrivateKey string
+}
+
+// GenerateChannel is used to create a channel a new channel of which you are
+// the admin. It is only for making new channels, not joining existing ones.
+//
+// It returns a pretty print of the channel and the private key.
+//
+// Parameters:
+//  - cmixID - The tracked cmix object ID. This can be retrieved using
+//    [Cmix.GetID].
+//  - name - The name of the new channel. The name must be between 3 and 24
+//    characters inclusive. It can only include upper and lowercase unicode
+//    letters, digits 0 through 9, and underscores (_). It cannot be changed
+//    once a channel is created.
+//  - description - The description of a channel. The description is optional
+//    but cannot be longer than 144 characters and can include all unicode
+//    characters. It cannot be changed once a channel is created.
+//  - privacyLevel - The broadcast.PrivacyLevel of the channel. 0 = public,
+//    1 = private, and 2 = secret. Refer to the comment below for more
+//    information.
+//
+// Returns:
+//  - []byte - [ChannelGeneration] describes a generated channel. It contains
+//    both the public channel info and the private key for the channel in PEM
+//    format.
+//
+// The [broadcast.PrivacyLevel] of a channel indicates the level of channel
+// information revealed when sharing it via URL. For any channel besides public
+// channels, the secret information is encrypted and a password is required to
+// share and join a channel.
+//  - A privacy level of [broadcast.Public] reveals all the information
+//    including the name, description, privacy level, public key and salt.
+//  - A privacy level of [broadcast.Private] reveals only the name and
+//    description.
+//  - A privacy level of [broadcast.Secret] reveals nothing.
+func GenerateChannel(cmixID int, name, description string, privacyLevel int) ([]byte, error) {
+	// Get cmix from singleton so its rng can be used
+	cmix, err := cmixTrackerSingleton.get(cmixID)
+	if err != nil {
+		return nil, err
+	}
+
+	stream := cmix.api.GetRng().GetStream()
+	defer stream.Close()
+	level := cryptoBroadcast.PrivacyLevel(privacyLevel)
+	c, pk, err := cryptoBroadcast.NewChannel(name, description, level,
+		cmix.api.GetCmix().GetMaxMessageLength(), stream)
+	if err != nil {
+		return nil, err
+	}
+
+	gen := ChannelGeneration{
+		Channel:    c.PrettyPrint(),
+		PrivateKey: string(pk.MarshalPem()),
+	}
+
+	err = saveChannelPrivateKey(cmix, c.ReceptionID, pk)
+	if err != nil {
+		return nil, err
+	}
+
+	return json.Marshal(&gen)
+}
+
+const (
+	channelPrivateKeyStoreVersion = 0
+	channelPrivateKeyStoreKey     = "channelPrivateKey"
+)
+
+func saveChannelPrivateKey(cmix *Cmix, channelID *id.ID, pk rsa.PrivateKey) error {
+	return cmix.api.GetStorage().Set(
+		makeChannelPrivateKeyStoreKey(channelID),
+		&versioned.Object{
+			Version:   channelPrivateKeyStoreVersion,
+			Timestamp: netTime.Now(),
+			Data:      pk.MarshalPem(),
+		})
+}
+
+// GetSavedChannelPrivateKeyUNSAFE loads the private key from storage for the
+// given channel ID.
+//
+// NOTE: This function is unsafe and only for debugging purposes only.
+//
+// Parameters:
+//  - cmixID - ID of [Cmix] object in tracker.
+//  - channelIdBase64 - The [id.ID] of the channel in base 64 encoding.
+//
+// Returns:
+//  - The PEM file of the private key.
+func GetSavedChannelPrivateKeyUNSAFE(cmixID int, channelIdBase64 string) (string, error) {
+	cmix, err := cmixTrackerSingleton.get(cmixID)
+	if err != nil {
+		return "", err
+	}
+
+	channelIdBytes, err := base64.StdEncoding.DecodeString(channelIdBase64)
+	if err != nil {
+		return "", errors.Errorf("failed to decode channel ID: %+v", err)
+	}
+
+	channelID, err := id.Unmarshal(channelIdBytes)
+	if err != nil {
+		return "", errors.Errorf("invalid channel ID: %+v", err)
+	}
+
+	privKey, err := loadChannelPrivateKey(cmix, channelID)
+	if err != nil {
+		return "", errors.Errorf(
+			"failed to load private key from storage: %+v", err)
+	}
+
+	return string(privKey.MarshalPem()), nil
+}
+
+func loadChannelPrivateKey(cmix *Cmix, channelID *id.ID) (rsa.PrivateKey, error) {
+	obj, err := cmix.api.GetStorage().Get(
+		makeChannelPrivateKeyStoreKey(channelID))
+	if err != nil {
+		return nil, err
+	}
+
+	return rsa.GetScheme().UnmarshalPrivateKeyPEM(obj.Data)
+}
+
+func makeChannelPrivateKeyStoreKey(channelID *id.ID) string {
+	return channelPrivateKeyStoreKey + "/" + channelID.String()
+}
+
+// DecodePublicURL decodes the channel URL into a channel pretty print. This
+// function can only be used for public channel URLs. To get the privacy level
+// of a channel URL, use [GetShareUrlType].
+//
+// Parameters:
+//  - url - The channel's share URL. Should be received from another user or
+//    generated via [GetShareURL].
+//
+// Returns:
+//  - The channel pretty print.
+func DecodePublicURL(url string) (string, error) {
+	c, err := cryptoBroadcast.DecodeShareURL(url, "")
+	if err != nil {
+		return "", err
+	}
+
+	return c.PrettyPrint(), nil
+}
+
+// DecodePrivateURL decodes the channel URL, using the password, into a channel
+// pretty print. This function can only be used for private or secret channel
+// URLs. To get the privacy level of a channel URL, use [GetShareUrlType].
+//
+// Parameters:
+//  - url - The channel's share URL. Should be received from another user or
+//    generated via [GetShareURL].
+//  - password - The password needed to decrypt the secret data in the URL.
+//
+// Returns:
+//  - The channel pretty print.
+func DecodePrivateURL(url, password string) (string, error) {
+	c, err := cryptoBroadcast.DecodeShareURL(url, password)
+	if err != nil {
+		return "", err
+	}
+
+	return c.PrettyPrint(), nil
+}
+
+// GetChannelJSON returns the JSON of the channel for the given pretty print.
+//
+// Parameters:
+//  - prettyPrint - The pretty print of the channel.
+//
+// Returns:
+//  - JSON of the [broadcast.Channel] object.
+//
+// Example JSON of [broadcast.Channel]:
+//  {
+//    "ReceptionID": "Ja/+Jh+1IXZYUOn+IzE3Fw/VqHOscomD0Q35p4Ai//kD",
+//    "Name": "My_Channel",
+//    "Description": "Here is information about my channel.",
+//    "Salt": "+tlrU/htO6rrV3UFDfpQALUiuelFZ+Cw9eZCwqRHk+g=",
+//    "RsaPubKeyHash": "PViT1mYkGBj6AYmE803O2RpA7BX24EjgBdldu3pIm4o=",
+//    "RsaPubKeyLength": 5,
+//    "RSASubPayloads": 1,
+//    "Secret": "JxZt/wPx2luoPdHY6jwbXqNlKnixVU/oa9DgypZOuyI=",
+//    "Level": 0
+//  }
+func GetChannelJSON(prettyPrint string) ([]byte, error) {
+	c, err := cryptoBroadcast.NewChannelFromPrettyPrint(prettyPrint)
+	if err != nil {
+		return nil, nil
+	}
+
+	return json.Marshal(c)
+}
+
+// ChannelInfo contains information about a channel.
+//
+// Example of ChannelInfo JSON:
+//  {
+//    "Name": "Test Channel",
+//    "Description": "This is a test channel",
+//    "ChannelID": "RRnpRhmvXtW9ugS1nILJ3WfttdctDvC2jeuH43E0g/0D",
+//  }
+type ChannelInfo struct {
+	Name        string
+	Description string
+	ChannelID   string
+}
+
+// GetChannelInfo returns the info about a channel from its public description.
+//
+// Parameters:
+//  - prettyPrint - The pretty print of the channel.
+//
+// The pretty print will be of the format:
+//  <Speakeasy-v2:Test_Channel|description:Channel description.|level:Public|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=>
+//
+// Returns:
+//  - []byte - JSON of [ChannelInfo], which describes all relevant channel info.
+func GetChannelInfo(prettyPrint string) ([]byte, error) {
+	_, bytes, err := getChannelInfo(prettyPrint)
+	return bytes, err
+}
+
+func getChannelInfo(prettyPrint string) (*cryptoBroadcast.Channel, []byte, error) {
+	c, err := cryptoBroadcast.NewChannelFromPrettyPrint(prettyPrint)
+	if err != nil {
+		return nil, nil, err
+	}
+	ci := &ChannelInfo{
+		Name:        c.Name,
+		Description: c.Description,
+		ChannelID:   c.ReceptionID.String(),
+	}
+	bytes, err := json.Marshal(ci)
+	if err != nil {
+		return nil, nil, err
+	}
+	return c, bytes, nil
+}
+
+// JoinChannel joins the given channel. It will fail if the channel has already
+// been joined.
+//
+// Parameters:
+//  - channelPretty - A portable channel string. Should be received from
+//    another user or generated via GenerateChannel.
+//
+// The pretty print will be of the format:
+//  <Speakeasy-v2:Test_Channel|description:Channel description.|level:Public|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=>
+//
+// Returns:
+//  - []byte - JSON of [ChannelInfo], which describes all relevant channel info.
+func (cm *ChannelsManager) JoinChannel(channelPretty string) ([]byte, error) {
+	c, info, err := getChannelInfo(channelPretty)
+	if err != nil {
+		return nil, err
+	}
+
+	// Join the channel using the API
+	err = cm.api.JoinChannel(c)
+
+	return info, err
+}
+
+// GetChannels returns the IDs of all channels that have been joined.
+//
+// Returns:
+//  - []byte - A JSON marshalled list of IDs.
+//
+// JSON Example:
+//  {
+//    "U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID",
+//    "15tNdkKbYXoMn58NO6VbDMDWFEyIhTWEGsvgcJsHWAgD"
+//  }
+func (cm *ChannelsManager) GetChannels() ([]byte, error) {
+	channelIds := cm.api.GetChannels()
+	return json.Marshal(channelIds)
+}
+
+// LeaveChannel leaves the given channel. It will return an error if the
+// channel was not previously joined.
+//
+// Parameters:
+//  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+func (cm *ChannelsManager) LeaveChannel(marshalledChanId []byte) error {
+	// Unmarshal channel ID
+	channelId, err := id.Unmarshal(marshalledChanId)
+	if err != nil {
+		return err
+	}
+
+	// Leave the channel
+	return cm.api.LeaveChannel(channelId)
+}
+
+// ReplayChannel replays all messages from the channel within the network's
+// memory (~3 weeks) over the event model.
+//
+// Parameters:
+//  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+func (cm *ChannelsManager) ReplayChannel(marshalledChanId []byte) error {
+
+	// Unmarshal channel ID
+	chanId, err := id.Unmarshal(marshalledChanId)
+	if err != nil {
+		return err
+	}
+
+	// Replay channel
+	return cm.api.ReplayChannel(chanId)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Channel Share URL                                                          //
+////////////////////////////////////////////////////////////////////////////////
+
+// ShareURL is returned from ChannelsManager.GetShareURL. It includes the
+// channel's share URL and password, if it needs one.
+//
+// JSON example for a public channel:
+//  {
+//    "url": "https://internet.speakeasy.tech/?0Name=My_Channel&Description=Here+is+information+about+my+channel.&2Level=Public&e=3CCvzK8diF%2B6vUZetyZkcyemoiI8uFLGSh%2B%2F9%2Bh5YQE%3D&k=zBakjn1Snay7AMr2CZ%2BCoWCHbe9TrtQqAVftIDi9Fjs%3D&l=5&m=0&p=1&s=Seyvx%2F5%2FOVTj5LClUG42AuLamDnqrbtMOoLymyIpFqY%3D&v=0",
+//    "password": ""
+//  }
+//
+// JSON example for a private channel:
+//  {
+//    "url": "https://internet.speakeasy.tech/?0Name=My_Channel&1Description=Here+is+information+about+my+channel.&d=i%2FwBAK6i89YT3LjPYb5%2BMmog5Gjk2unYzYt25y%2BmZH3%2Bo08oUIHHEoC7JYjk50Q2%2BMcSj6fQh%2BW3LBvWv02f1g60PLXZ1H8OS2rqoxBhwHvTpNgXRdUIErbk6q3ljIdjtSqJtWIzAx5no%2F96jaIBsob0U9jDE1jgsU8XNGxDz3TeKcdTOFiUpnh4R%2BALcys%3D&m=1&v=0",
+//    "password": "easter boaster musket catalyze unproven vendetta plated grinning"
+//  }
+//
+// JSON example for a secret channel:
+//  {
+//    "url": "https://internet.speakeasy.tech/?d=xRORDN8lt%2BI2SAn%2F21ZpOzj50J3HOV1GkMsPkhtgoYyQUqpPBZhhKpewzuDI%2B3wTQlpANLDMtFVL4J7y2lBpvIz9LQ5%2F6CoRdVkoXbG7uRqv6wscYdwWPYZBARC2cJSyeVad6RbxnoZ65Z0dtEVEff328ri3ZpaMBlP%2BpUH928pcVHibALW7Bw04Rkmh%2FWx6wJGw%2FU0gTHo02UlYFHh4G9CC%2BIU1x13BmEuW6Hyk6Ty9BlHt29QbsQ7uU30RwzQOyg8%3D&m=2&v=0",
+//    "password": "florist angled valid snarl discharge endearing harbor hazy"
+//  }
+type ShareURL struct {
+	URL      string `json:"url"`
+	Password string `json:"password"`
+}
+
+// GetShareURL generates a URL that can be used to share this channel with
+// others on the given host.
+//
+// A URL comes in one of three forms based on the privacy level set when
+// generating the channel. Each privacy level hides more information than the
+// last with the lowest level revealing everything and the highest level
+// revealing nothing. For any level above the lowest, a password is returned,
+// which will be required when decoding the URL.
+//
+// The maxUses is the maximum number of times this URL can be used to join a
+// channel. If it is set to 0, then it can be shared unlimited times. The max
+// uses is set as a URL parameter using the key [broadcast.MaxUsesKey]. Note
+// that this number is also encoded in the secret data for private and secret
+// URLs, so if the number is changed in the URL, is will be verified when
+// calling [ChannelsManager.JoinChannelFromURL]. There is no enforcement for
+// public URLs.
+//
+// Parameters:
+//  - cmixID - The tracked Cmix object ID.
+//  - host - The URL to append the channel info to.
+//  - maxUses - The maximum number of uses the link can be used (0 for
+//    unlimited).
+//  - marshalledChanId - A marshalled channel ID ([id.ID]).
+//
+// Returns:
+//  - JSON of ShareURL.
+func (cm *ChannelsManager) GetShareURL(cmixID int, host string, maxUses int,
+	marshalledChanId []byte) ([]byte, error) {
+
+	// Unmarshal channel ID
+	chanId, err := id.Unmarshal(marshalledChanId)
+	if err != nil {
+		return nil, err
+	}
+
+	// Get the channel from the ID
+	ch, err := cm.api.GetChannel(chanId)
+	if err != nil {
+		return nil, err
+	}
+
+	// Get user from singleton
+	user, err := cmixTrackerSingleton.get(cmixID)
+	if err != nil {
+		return nil, err
+	}
+
+	// Generate share URL and password
+	rng := user.api.GetRng().GetStream()
+	url, password, err := ch.ShareURL(host, maxUses, rng)
+	rng.Close()
+	if err != nil {
+		return nil, err
+	}
+
+	su := ShareURL{
+		URL:      url,
+		Password: password,
+	}
+
+	return json.Marshal(su)
+}
+
+// GetShareUrlType determines the [broadcast.PrivacyLevel] of the channel URL.
+// If the URL is an invalid channel URL, an error is returned.
+//
+// Parameters:
+//  - url - The channel share URL.
+//
+// Returns:
+//  - An int that corresponds to the [broadcast.PrivacyLevel] as outlined below.
+//
+// Possible returns:
+//  0 = public channel
+//  1 = private channel
+//  2 = secret channel
+func GetShareUrlType(url string) (int, error) {
+	level, err := cryptoBroadcast.GetShareUrlType(url)
+	return int(level), err
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Channel Sending Methods & Reports                                          //
+////////////////////////////////////////////////////////////////////////////////
+
+// ChannelSendReport is the bindings' representation of the return values of
+// ChannelsManager's Send operations.
+//
+// JSON Example:
+//  {
+//    "MessageId": "0kitNxoFdsF4q1VMSI/xPzfCnGB2l+ln2+7CTHjHbJw=",
+//    "Rounds":[1,5,9],
+//    "EphId": 0
+//  }
+type ChannelSendReport struct {
+	MessageId []byte
+	RoundsList
+	EphId int64
+}
+
+// SendGeneric is used to send a raw message over a channel. In general, it
+// should be wrapped in a function that defines the wire protocol. If the final
+// message, before being sent over the wire, is too long, this will return an
+// error. Due to the underlying encoding using compression, it isn't possible to
+// define the largest payload that can be sent, but it will always be possible
+// to send a payload of 802 bytes at minimum. The meaning of validUntil depends
+// on the use case.
+//
+// Parameters:
+//  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+//  - messageType - The message type of the message. This will be a valid
+//    [channels.MessageType].
+//  - message - The contents of the message. This need not be of data type
+//    string, as the message could be a specified format that the channel may
+//    recognize.
+//  - leaseTimeMS - The lease of the message. This will be how long the message
+//    is valid until, in milliseconds. As per the channels.Manager
+//    documentation, this has different meanings depending on the use case.
+//    These use cases may be generic enough that they will not be enumerated
+//    here.
+//  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
+//    and GetDefaultCMixParams will be used internally.
+//
+// Returns:
+//  - []byte - A JSON marshalled ChannelSendReport.
+func (cm *ChannelsManager) SendGeneric(marshalledChanId []byte,
+	messageType int, message []byte, leaseTimeMS int64,
+	cmixParamsJSON []byte) ([]byte, error) {
+
+	// Unmarshal channel ID and parameters
+	chanId, params, err := parseChannelsParameters(
+		marshalledChanId, cmixParamsJSON)
+	if err != nil {
+		return nil, err
+	}
+
+	msgTy := channels.MessageType(messageType)
+
+	// Send message
+	chanMsgId, rnd, ephId, err := cm.api.SendGeneric(chanId,
+		msgTy, message, time.Duration(leaseTimeMS),
+		params.CMIX)
+	if err != nil {
+		return nil, err
+	}
+
+	// Construct send report
+	return constructChannelSendReport(chanMsgId, rnd.ID, ephId)
+}
+
+// SendAdminGeneric is used to send a raw message over a channel encrypted with
+// admin keys, identifying it as sent by the admin. In general, it should be
+// wrapped in a function that defines the wire protocol. If the final message,
+// before being sent over the wire, is too long, this will return an error. The
+// message must be at most 510 bytes long.
+//
+// Parameters:
+//  - adminPrivateKey - The PEM-encoded admin RSA private key.
+//  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+//  - messageType - The message type of the message. This will be a valid
+//    [channels.MessageType].
+//  - message - The contents of the message. The message should be at most 510
+//    bytes. This need not be of data type string, as the message could be a
+//    specified format that the channel may recognize.
+//  - leaseTimeMS - The lease of the message. This will be how long the message
+//    is valid until, in milliseconds. As per the channels.Manager
+//    documentation, this has different meanings depending on the use case.
+//    These use cases may be generic enough that they will not be enumerated
+//    here.
+//  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
+//    and GetDefaultCMixParams will be used internally.
+//
+// Returns:
+//  - []byte - A JSON marshalled ChannelSendReport.
+func (cm *ChannelsManager) SendAdminGeneric(adminPrivateKey,
+	marshalledChanId []byte,
+	messageType int, message []byte, leaseTimeMS int64,
+	cmixParamsJSON []byte) ([]byte, error) {
+
+	// Load private key from file
+	rsaPrivKey, err := rsa.GetScheme().UnmarshalPrivateKeyPEM(adminPrivateKey)
+	if err != nil {
+		return nil, err
+	}
+
+	// Unmarshal channel ID and parameters
+	chanId, params, err := parseChannelsParameters(
+		marshalledChanId, cmixParamsJSON)
+	if err != nil {
+		return nil, err
+	}
+
+	msgTy := channels.MessageType(messageType)
+
+	// Send admin message
+	chanMsgId, rnd, ephId, err := cm.api.SendAdminGeneric(rsaPrivKey,
+		chanId, msgTy, message, time.Duration(leaseTimeMS),
+		params.CMIX)
+
+	// Construct send report
+	return constructChannelSendReport(chanMsgId, rnd.ID, ephId)
+}
+
+// SendMessage is used to send a formatted message over a channel.
+// Due to the underlying encoding using compression, it isn't possible to define
+// the largest payload that can be sent, but it will always be possible to send
+// a payload of 798 bytes at minimum.
+//
+// The message will auto delete validUntil after the round it is sent in,
+// lasting forever if [channels.ValidForever] is used.
+//
+// Parameters:
+//  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+//  - message - The contents of the message. The message should be at most 510
+//    bytes. This is expected to be Unicode, and thus a string data type is
+//    expected
+//  - leaseTimeMS - The lease of the message. This will be how long the message
+//    is valid until, in milliseconds. As per the channels.Manager
+//    documentation, this has different meanings depending on the use case.
+//    These use cases may be generic enough that they will not be enumerated
+//    here.
+//  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be
+//    empty, and GetDefaultCMixParams will be used internally.
+//
+// Returns:
+//  - []byte - A JSON marshalled ChannelSendReport
+func (cm *ChannelsManager) SendMessage(marshalledChanId []byte,
+	message string, leaseTimeMS int64, cmixParamsJSON []byte) ([]byte, error) {
+
+	// Unmarshal channel ID and parameters
+	chanId, params, err := parseChannelsParameters(
+		marshalledChanId, cmixParamsJSON)
+	if err != nil {
+		return nil, err
+	}
+
+	// Send message
+	chanMsgId, rnd, ephId, err := cm.api.SendMessage(chanId, message,
+		time.Duration(leaseTimeMS), params.CMIX)
+	if err != nil {
+		return nil, err
+	}
+
+	// Construct send report
+	return constructChannelSendReport(chanMsgId, rnd.ID, ephId)
+}
+
+// SendReply is used to send a formatted message over a channel. Due to the
+// underlying encoding using compression, it isn't possible to define the
+// largest payload that can be sent, but it will always be possible to send a
+// payload of 766 bytes at minimum.
+//
+// If the message ID the reply is sent to is nonexistent, the other side will
+// post the message as a normal message and not a reply. The message will auto
+// delete validUntil after the round it is sent in, lasting forever if
+// [channels.ValidForever] is used.
+//
+// Parameters:
+//  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+//  - message - The contents of the message. The message should be at most 510
+//    bytes. This is expected to be Unicode, and thus a string data type is
+//    expected.
+//  - messageToReactTo - The marshalled [channel.MessageID] of the message you
+//    wish to reply to. This may be found in the ChannelSendReport if replying
+//    to your own. Alternatively, if reacting to another user's message, you may
+//    retrieve it via the ChannelMessageReceptionCallback registered using
+//    RegisterReceiveHandler.
+//  - leaseTimeMS - The lease of the message. This will be how long the message
+//    is valid until, in milliseconds. As per the channels.Manager
+//    documentation, this has different meanings depending on the use case.
+//    These use cases may be generic enough that they will not be enumerated
+//    here.
+//  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
+//    and GetDefaultCMixParams will be used internally.
+//
+// Returns:
+//  - []byte - A JSON marshalled ChannelSendReport
+func (cm *ChannelsManager) SendReply(marshalledChanId []byte,
+	message string, messageToReactTo []byte, leaseTimeMS int64,
+	cmixParamsJSON []byte) ([]byte, error) {
+
+	// Unmarshal channel ID and parameters
+	chanId, params, err := parseChannelsParameters(
+		marshalledChanId, cmixParamsJSON)
+	if err != nil {
+		return nil, err
+	}
+
+	// Unmarshal message ID
+	msgId := cryptoChannel.MessageID{}
+	copy(msgId[:], messageToReactTo)
+
+	// Send Reply
+	chanMsgId, rnd, ephId, err := cm.api.SendReply(chanId, message,
+		msgId, time.Duration(leaseTimeMS), params.CMIX)
+	if err != nil {
+		return nil, err
+	}
+
+	// Construct send report
+	return constructChannelSendReport(chanMsgId, rnd.ID, ephId)
+}
+
+// SendReaction is used to send a reaction to a message over a channel.
+// The reaction must be a single emoji with no other characters, and will
+// be rejected otherwise.
+// Users will drop the reaction if they do not recognize the reactTo message.
+//
+// Parameters:
+//  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+//  - reaction - The user's reaction. This should be a single emoji with no
+//    other characters. As such, a Unicode string is expected.
+//  - messageToReactTo - The marshalled [channel.MessageID] of the message you
+//    wish to reply to. This may be found in the ChannelSendReport if replying
+//    to your own. Alternatively, if reacting to another user's message, you may
+//    retrieve it via the ChannelMessageReceptionCallback registered using
+//    RegisterReceiveHandler.
+//  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
+//  and GetDefaultCMixParams will be used internally.
+//
+// Returns:
+//  - []byte - A JSON marshalled ChannelSendReport.
+func (cm *ChannelsManager) SendReaction(marshalledChanId []byte,
+	reaction string, messageToReactTo []byte,
+	cmixParamsJSON []byte) ([]byte, error) {
+
+	// Unmarshal channel ID and parameters
+	chanId, params, err := parseChannelsParameters(
+		marshalledChanId, cmixParamsJSON)
+	if err != nil {
+		return nil, err
+	}
+
+	// Unmarshal message ID
+	msgId := cryptoChannel.MessageID{}
+	copy(msgId[:], messageToReactTo)
+
+	// Send reaction
+	chanMsgId, rnd, ephId, err := cm.api.SendReaction(chanId,
+		reaction, msgId, params.CMIX)
+	if err != nil {
+		return nil, err
+	}
+
+	// Construct send report
+	return constructChannelSendReport(chanMsgId, rnd.ID, ephId)
+}
+
+// GetIdentity returns the marshaled public identity ([channel.Identity]) that
+// the channel is using.
+func (cm *ChannelsManager) GetIdentity() ([]byte, error) {
+	i := cm.api.GetIdentity()
+	return json.Marshal(&i)
+}
+
+// ExportPrivateIdentity encrypts and exports the private identity to a portable
+// string.
+func (cm *ChannelsManager) ExportPrivateIdentity(password string) ([]byte, error) {
+	return cm.api.ExportPrivateIdentity(password)
+}
+
+// GetStorageTag returns the storage tag needed to reload the manager.
+func (cm *ChannelsManager) GetStorageTag() string {
+	return cm.api.GetStorageTag()
+}
+
+// SetNickname sets the nickname for a given channel. The nickname must be valid
+// according to [IsNicknameValid].
+func (cm *ChannelsManager) SetNickname(newNick string, ch []byte) error {
+	chid, err := id.Unmarshal(ch)
+	if err != nil {
+		return err
+	}
+	return cm.api.SetNickname(newNick, chid)
+}
+
+// DeleteNickname deletes the nickname for a given channel.
+func (cm *ChannelsManager) DeleteNickname(ch []byte) error {
+	chid, err := id.Unmarshal(ch)
+	if err != nil {
+		return err
+	}
+	return cm.api.DeleteNickname(chid)
+}
+
+// GetNickname returns the nickname set for a given channel. Returns an error if
+// there is no nickname set.
+func (cm *ChannelsManager) GetNickname(ch []byte) (string, error) {
+	chid, err := id.Unmarshal(ch)
+	if err != nil {
+		return "", err
+	}
+	nick, exists := cm.api.GetNickname(chid)
+	if !exists {
+		return "", errors.New("no nickname found for the given channel")
+	}
+
+	return nick, nil
+}
+
+// IsNicknameValid checks if a nickname is valid.
+//
+// Rules:
+//  1. A nickname must not be longer than 24 characters.
+//  2. A nickname must not be shorter than 1 character.
+func IsNicknameValid(nick string) error {
+	return channels.IsNicknameValid(nick)
+}
+
+// parseChannelsParameters is a helper function for the Send functions. It
+// parses the channel ID and the passed in parameters into their respective
+// objects. These objects are passed into the API via the internal send
+// functions.
+func parseChannelsParameters(marshalledChanId, cmixParamsJSON []byte) (
+	*id.ID, xxdk.CMIXParams, error) {
+	// Unmarshal channel ID
+	chanId, err := id.Unmarshal(marshalledChanId)
+	if err != nil {
+		return nil, xxdk.CMIXParams{}, err
+	}
+
+	// Unmarshal cmix params
+	params, err := parseCMixParams(cmixParamsJSON)
+	if err != nil {
+		return nil, xxdk.CMIXParams{}, err
+	}
+
+	return chanId, params, nil
+}
+
+// constructChannelSendReport is a helper function which returns a JSON
+// marshalled ChannelSendReport.
+func constructChannelSendReport(channelMessageId cryptoChannel.MessageID,
+	roundId id.Round, ephId ephemeral.Id) ([]byte, error) {
+	// Construct send report
+	chanSendReport := ChannelSendReport{
+		MessageId:  channelMessageId.Bytes(),
+		RoundsList: makeRoundsList(roundId),
+		EphId:      ephId.Int64(),
+	}
+
+	// Marshal send report
+	return json.Marshal(chanSendReport)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Channel Receiving Logic and Callback Registration                          //
+////////////////////////////////////////////////////////////////////////////////
+
+// ReceivedChannelMessageReport is a report structure returned via the
+// ChannelMessageReceptionCallback. This report gives the context for the
+// channel the message was sent to and the message itself. This is returned via
+// the callback as JSON marshalled bytes.
+//
+// JSON Example:
+//  {
+//    "ChannelId": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+//    "MessageId": "3S6DiVjWH9mLmjy1oaam/3x45bJQzOW6u2KgeUn59wA=",
+//    "ReplyTo":"cxMyGUFJ+Ff1Xp2X+XkIpOnNAQEZmv8SNP5eYH4tCik=",
+//    "MessageType": 42,
+//    "SenderUsername": "hunter2",
+//    "Content": "YmFuX2JhZFVTZXI=",
+//    "Timestamp": 1662502150335283000,
+//    "Lease": 25,
+//    "Rounds": [ 1, 4, 9],
+//  }
+type ReceivedChannelMessageReport struct {
+	ChannelId   []byte
+	MessageId   []byte
+	MessageType int
+	Nickname    string
+	PubKey      []byte
+	Codeset     int
+	Content     []byte
+	Timestamp   int64
+	Lease       int64
+	RoundsList
+}
+
+// ChannelMessageReceptionCallback is the callback that returns the context for
+// a channel message via the Callback.
+// It must return a unique UUID for the message by which it can be referenced
+// later
+type ChannelMessageReceptionCallback interface {
+	Callback(receivedChannelMessageReport []byte, err error) int
+}
+
+// RegisterReceiveHandler is used to register handlers for non-default message
+// types. They can be processed by modules. It is important that such modules
+// sync up with the event model implementation.
+//
+// There can only be one handler per [channels.MessageType], and this will
+// return an error on any re-registration.
+//
+// Parameters:
+//  - messageType - represents the [channels.MessageType] which will have a
+//    registered listener.
+//  - listenerCb - the callback which will be executed when a channel message
+//    of messageType is received.
+func (cm *ChannelsManager) RegisterReceiveHandler(messageType int,
+	listenerCb ChannelMessageReceptionCallback) error {
+
+	// Wrap callback around backend interface
+	cb := channels.MessageTypeReceiveMessage(
+		func(channelID *id.ID,
+			messageID cryptoChannel.MessageID, messageType channels.MessageType,
+			nickname string, content []byte, pubKey ed25519.PublicKey,
+			codeset uint8, timestamp time.Time, lease time.Duration,
+			round rounds.Round, status channels.SentStatus) uint64 {
+
+			rcm := ReceivedChannelMessageReport{
+				ChannelId:   channelID.Marshal(),
+				MessageId:   messageID.Bytes(),
+				MessageType: int(messageType),
+				Nickname:    nickname,
+				PubKey:      pubKey,
+				Codeset:     int(codeset),
+				Content:     content,
+				Timestamp:   timestamp.UnixNano(),
+				Lease:       int64(lease),
+				RoundsList:  makeRoundsList(round.ID),
+			}
+
+			return uint64(listenerCb.Callback(json.Marshal(rcm)))
+		})
+
+	// Register handler
+	return cm.api.RegisterReceiveHandler(channels.MessageType(messageType), cb)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Event Model Logic                                                          //
+////////////////////////////////////////////////////////////////////////////////
+
+// EventModelBuilder builds an event model
+type EventModelBuilder interface {
+	Build(path string) EventModel
+}
+
+// EventModel is an interface which an external party which uses the channels
+// system passed an object which adheres to in order to get events on the
+// channel.
+type EventModel interface {
+	// JoinChannel is called whenever a channel is joined locally.
+	//
+	// Parameters:
+	//  - channel - Returns the pretty print representation of a channel.
+	JoinChannel(channel string)
+
+	// LeaveChannel is called whenever a channel is left locally.
+	//
+	// Parameters:
+	//  - ChannelId - The marshalled channel [id.ID].
+	LeaveChannel(channelID []byte)
+
+	// ReceiveMessage is called whenever a message is received on a given
+	// channel. It may be called multiple times on the same message. It is
+	// incumbent on the user of the API to filter such called by message ID.
+	//
+	// Parameters:
+	//  - channelID - The marshalled channel [id.ID].
+	//  - messageID - The bytes of the [channel.MessageID] of the received
+	//    message.
+	//  - nickname - The nickname of the sender of the message.
+	//  - text - The content of the message.
+	//  - timestamp - Time the message was received; represented as nanoseconds
+	//    since unix epoch.
+	//  - pubKey - The sender's Ed25519 public key.
+	//  - codeset - The codeset version.
+	//  - lease - The number of nanoseconds that the message is valid for.
+	//  - roundId - The ID of the round that the message was received on.
+	//  - mType - the type of the message, always 1 for this call
+	//  - status - the [channels.SentStatus] of the message.
+	//
+	// Statuses will be enumerated as such:
+	//  Sent      =  0
+	//  Delivered =  1
+	//  Failed    =  2
+	//
+	// Returns a non-negative unique UUID for the message that it can be
+	// referenced by later with [EventModel.UpdateSentStatus].
+	ReceiveMessage(channelID, messageID []byte, nickname, text string,
+		pubKey []byte, codeset int, timestamp, lease, roundId, mType,
+		status int64) int64
+
+	// ReceiveReply is called whenever a message is received that is a reply on
+	// a given channel. It may be called multiple times on the same message. It
+	// is incumbent on the user of the API to filter such called by message ID.
+	//
+	// Messages may arrive our of order, so a reply in theory can arrive before
+	// the initial message. As a result, it may be important to buffer replies.
+	//
+	// Parameters:
+	//  - channelID - The marshalled channel [id.ID].
+	//  - messageID - The bytes of the [channel.MessageID] of the received
+	//    message.
+	//  - reactionTo - The [channel.MessageID] for the message that received a
+	//    reply.
+	//  - nickname - The nickname of the sender of the message.
+	//  - text - The content of the message.
+	//  - pubKey - The sender's Ed25519 public key.
+	//  - codeset - The codeset version.
+	//  - timestamp - Time the message was received; represented as nanoseconds
+	//    since unix epoch.
+	//  - lease - The number of nanoseconds that the message is valid for.
+	//  - roundId - The ID of the round that the message was received on.
+	//  - mType - the type of the message, always 1 for this call
+	//  - status - the [channels.SentStatus] of the message.
+	//
+	// Statuses will be enumerated as such:
+	//  Sent      =  0
+	//  Delivered =  1
+	//  Failed    =  2
+	//
+	// Returns a non-negative unique UUID for the message that it can be
+	// referenced by later with [EventModel.UpdateSentStatus].
+	ReceiveReply(channelID, messageID, reactionTo []byte, nickname, text string,
+		pubKey []byte, codeset int, timestamp, lease, roundId, mType,
+		status int64) int64
+
+	// ReceiveReaction is called whenever a reaction to a message is received
+	// on a given channel. It may be called multiple times on the same reaction.
+	// It is incumbent on the user of the API to filter such called by message
+	// ID.
+	//
+	// Messages may arrive our of order, so a reply in theory can arrive before
+	// the initial message. As a result, it may be important to buffer
+	// reactions.
+	//
+	// Parameters:
+	//  - channelID - The marshalled channel [id.ID].
+	//  - messageID - The bytes of the [channel.MessageID] of the received
+	//    message.
+	//  - reactionTo - The [channel.MessageID] for the message that received a
+	//    reply.
+	//  - nickname - The nickname of the sender of the message.
+	//  - reaction - The contents of the reaction message.
+	//  - pubKey - The sender's Ed25519 public key.
+	//  - codeset - The codeset version.
+	//  - timestamp - Time the message was received; represented as nanoseconds
+	//    since unix epoch.
+	//  - lease - The number of nanoseconds that the message is valid for.
+	//  - roundId - The ID of the round that the message was received on.
+	//  - mType - the type of the message, always 1 for this call
+	//  - status - the [channels.SentStatus] of the message.
+	//
+	// Statuses will be enumerated as such:
+	//  Sent      =  0
+	//  Delivered =  1
+	//  Failed    =  2
+	//
+	// Returns a non-negative unique uuid for the message by which it can be
+	// referenced later with UpdateSentStatus
+	ReceiveReaction(channelID, messageID, reactionTo []byte, nickname,
+		reaction string, pubKey []byte, codeset int, timestamp, lease, roundId,
+		mType, status int64) int64
+
+	// UpdateSentStatus is called whenever the sent status of a message has
+	// changed.
+	//
+	// Parameters:
+	//  - messageID - The bytes of the [channel.MessageID] of the received
+	//    message.
+	//  - status - the [channels.SentStatus] of the message.
+	//
+	// Statuses will be enumerated as such:
+	//  Sent      =  0
+	//  Delivered =  1
+	//  Failed    =  2
+	UpdateSentStatus(
+		uuid int64, messageID []byte, timestamp, roundID, status int64)
+
+	// unimplemented
+	// IgnoreMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID)
+	// UnIgnoreMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID)
+	// PinMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID, end time.Time)
+	// UnPinMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID)
+}
+
+// toEventModel is a wrapper which wraps an existing channels.EventModel object.
+type toEventModel struct {
+	em EventModel
+}
+
+// NewEventModel is a constructor for a toEventModel. This will take in an
+// EventModel and wraps it around the toEventModel.
+func NewEventModel(em EventModel) channels.EventModel {
+	return &toEventModel{em: em}
+}
+
+// JoinChannel is called whenever a channel is joined locally.
+func (tem *toEventModel) JoinChannel(channel *cryptoBroadcast.Channel) {
+	tem.em.JoinChannel(channel.PrettyPrint())
+}
+
+// LeaveChannel is called whenever a channel is left locally.
+func (tem *toEventModel) LeaveChannel(channelID *id.ID) {
+	tem.em.LeaveChannel(channelID[:])
+}
+
+// ReceiveMessage is called whenever a message is received on a given channel.
+// It may be called multiple times on the same message. It is incumbent on the
+// user of the API to filter such called by message ID.
+func (tem *toEventModel) ReceiveMessage(channelID *id.ID,
+	messageID cryptoChannel.MessageID, nickname, text string,
+	pubKey ed25519.PublicKey, codeset uint8, timestamp time.Time,
+	lease time.Duration, round rounds.Round, mType channels.MessageType,
+	status channels.SentStatus) uint64 {
+
+	return uint64(tem.em.ReceiveMessage(channelID[:], messageID[:], nickname,
+		text, pubKey, int(codeset), timestamp.UnixNano(), int64(lease),
+		int64(round.ID), int64(mType), int64(status)))
+}
+
+// ReceiveReply is called whenever a message is received that is a reply on a
+// given channel. It may be called multiple times on the same message. It is
+// incumbent on the user of the API to filter such called by message ID.
+//
+// Messages may arrive our of order, so a reply in theory can arrive before the
+// initial message. As a result, it may be important to buffer replies.
+func (tem *toEventModel) ReceiveReply(channelID *id.ID,
+	messageID cryptoChannel.MessageID, reactionTo cryptoChannel.MessageID,
+	nickname, text string, pubKey ed25519.PublicKey, codeset uint8,
+	timestamp time.Time, lease time.Duration, round rounds.Round,
+	mType channels.MessageType, status channels.SentStatus) uint64 {
+
+	return uint64(tem.em.ReceiveReply(channelID[:], messageID[:], reactionTo[:],
+		nickname, text, pubKey, int(codeset), timestamp.UnixNano(),
+		int64(lease), int64(round.ID), int64(mType), int64(status)))
+
+}
+
+// ReceiveReaction is called whenever a reaction to a message is received on a
+// given channel. It may be called multiple times on the same reaction. It is
+// incumbent on the user of the API to filter such called by message ID.
+//
+// Messages may arrive our of order, so a reply in theory can arrive before the
+// initial message. As a result, it may be important to buffer reactions.
+func (tem *toEventModel) ReceiveReaction(channelID *id.ID, messageID cryptoChannel.MessageID,
+	reactionTo cryptoChannel.MessageID, nickname, reaction string,
+	pubKey ed25519.PublicKey, codeset uint8, timestamp time.Time,
+	lease time.Duration, round rounds.Round, mType channels.MessageType,
+	status channels.SentStatus) uint64 {
+
+	return uint64(tem.em.ReceiveReaction(channelID[:], messageID[:],
+		reactionTo[:], nickname, reaction, pubKey, int(codeset),
+		timestamp.UnixNano(), int64(lease), int64(round.ID), int64(mType),
+		int64(status)))
+}
+
+// UpdateSentStatus is called whenever the sent status of a message has changed.
+func (tem *toEventModel) UpdateSentStatus(uuid uint64,
+	messageID cryptoChannel.MessageID, timestamp time.Time, round rounds.Round,
+	status channels.SentStatus) {
+	tem.em.UpdateSentStatus(int64(uuid), messageID[:], timestamp.UnixNano(),
+		int64(round.ID), int64(status))
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Channel ChannelDbCipher                                                             //
+////////////////////////////////////////////////////////////////////////////////
+
+// ChannelDbCipher is the bindings layer representation of the [channel.Cipher].
+type ChannelDbCipher struct {
+	api  cryptoChannel.Cipher
+	salt []byte
+	id   int
+}
+
+// channelDbCipherTrackerSingleton is used to track ChannelDbCipher objects
+// so that they can be referenced by ID back over the bindings.
+var channelDbCipherTrackerSingleton = &channelDbCipherTracker{
+	tracked: make(map[int]*ChannelDbCipher),
+	count:   0,
+}
+
+// channelDbCipherTracker is a singleton used to keep track of extant
+// ChannelDbCipher objects, preventing race conditions created by passing it
+// over the bindings.
+type channelDbCipherTracker struct {
+	tracked map[int]*ChannelDbCipher
+	count   int
+	mux     sync.RWMutex
+}
+
+// create creates a ChannelDbCipher from a [channel.Cipher], assigns it a unique
+// ID, and adds it to the channelDbCipherTracker.
+func (ct *channelDbCipherTracker) create(c cryptoChannel.Cipher) *ChannelDbCipher {
+	ct.mux.Lock()
+	defer ct.mux.Unlock()
+
+	chID := ct.count
+	ct.count++
+
+	ct.tracked[chID] = &ChannelDbCipher{
+		api: c,
+		id:  chID,
+	}
+
+	return ct.tracked[chID]
+}
+
+// get an ChannelDbCipher from the channelDbCipherTracker given its ID.
+func (ct *channelDbCipherTracker) get(id int) (*ChannelDbCipher, error) {
+	ct.mux.RLock()
+	defer ct.mux.RUnlock()
+
+	c, exist := ct.tracked[id]
+	if !exist {
+		return nil, errors.Errorf(
+			"Cannot get ChannelDbCipher for ID %d, does not exist", id)
+	}
+
+	return c, nil
+}
+
+// delete removes a ChannelDbCipher from the channelDbCipherTracker.
+func (ct *channelDbCipherTracker) delete(id int) {
+	ct.mux.Lock()
+	defer ct.mux.Unlock()
+
+	delete(ct.tracked, id)
+}
+
+// GetChannelDbCipherTrackerFromID returns the ChannelDbCipher with the
+// corresponding ID in the tracker.
+func GetChannelDbCipherTrackerFromID(id int) (*ChannelDbCipher, error) {
+	return channelDbCipherTrackerSingleton.get(id)
+}
+
+// NewChannelsDatabaseCipher constructs a ChannelDbCipher object.
+//
+// Parameters:
+//  - cmixID - The tracked [Cmix] object ID.
+//  - password - The password for storage. This should be the same password
+//    passed into [NewCmix].
+//  - plaintTextBlockSize - The maximum size of a payload to be encrypted.
+//    A payload passed into [ChannelDbCipher.Encrypt] that is larger than
+//    plaintTextBlockSize will result in an error.
+func NewChannelsDatabaseCipher(cmixID int, password []byte,
+	plaintTextBlockSize int) (*ChannelDbCipher, error) {
+	// Get user from singleton
+	user, err := cmixTrackerSingleton.get(cmixID)
+	if err != nil {
+		return nil, err
+	}
+
+	// Generate RNG
+	stream := user.api.GetRng().GetStream()
+
+	// Load or generate a salt
+	salt, err := utility.NewOrLoadSalt(
+		user.api.GetStorage().GetKV(), stream)
+	if err != nil {
+		return nil, err
+	}
+
+	// Construct a cipher
+	c, err := cryptoChannel.NewCipher(password, salt,
+		plaintTextBlockSize, stream)
+	if err != nil {
+		return nil, err
+	}
+
+	// Return a cipher
+	return channelDbCipherTrackerSingleton.create(c), nil
+}
+
+// GetID returns the ID for this ChannelDbCipher in the channelDbCipherTracker.
+func (c *ChannelDbCipher) GetID() int {
+	return c.id
+}
+
+// Encrypt will encrypt the raw data. It will return a ciphertext. Padding is
+// done on the plaintext so all encrypted data looks uniform at rest.
+//
+// Parameters:
+//  - plaintext - The data to be encrypted. This must be smaller than the block
+//    size passed into [NewChannelsDatabaseCipher]. If it is larger, this will
+//    return an error.
+func (c *ChannelDbCipher) Encrypt(plaintext []byte) ([]byte, error) {
+	return c.api.Encrypt(plaintext)
+}
+
+// Decrypt will decrypt the passed in encrypted value. The plaintext will
+// be returned by this function. Any padding will be discarded within
+// this function.
+//
+// Parameters:
+//  - ciphertext - the encrypted data returned by [ChannelDbCipher.Encrypt].
+func (c *ChannelDbCipher) Decrypt(ciphertext []byte) ([]byte, error) {
+	return c.api.Decrypt(ciphertext)
+}
diff --git a/bindings/cmix.go b/bindings/cmix.go
index 422740df88391fcdd91e54848520d38dfeb28a8d..5b5683f8aedb3e0e57f087de749c5e090823f0c0 100644
--- a/bindings/cmix.go
+++ b/bindings/cmix.go
@@ -60,10 +60,6 @@ func NewCmix(ndfJSON, storageDir string, password []byte, registrationCode strin
 // subprocesses to perform network operations.
 func LoadCmix(storageDir string, password []byte, cmixParamsJSON []byte) (*Cmix,
 	error) {
-	if len(cmixParamsJSON) == 0 {
-		jww.WARN.Printf("cMix params not specified, using defaults...")
-		cmixParamsJSON = GetDefaultCMixParams()
-	}
 
 	params, err := parseCMixParams(cmixParamsJSON)
 	if err != nil {
diff --git a/bindings/delivery.go b/bindings/delivery.go
index 6420196d4e16f34edae4510cb7d1d228f7e0b2d2..a645e9412dbbc99e49272843241f0dc22058c838 100644
--- a/bindings/delivery.go
+++ b/bindings/delivery.go
@@ -22,9 +22,9 @@ import (
 // This should be used by any type of send report's GetRoundURL method.
 var dashboardBaseURL = "https://dashboard.xx.network"
 
-// SetDashboardURL is a function which modifies the base dashboard URL
-// that is returned as part of any send report. Internally, this is defaulted
-// to "https://dashboard.xx.network". This should only be called if the user
+// SetDashboardURL is a function which modifies the base dashboard URL that is
+// returned as part of any send report. Internally, this is defaulted to
+// "https://dashboard.xx.network". This should only be called if the user
 // explicitly wants to modify the dashboard URL. This function is not
 // thread-safe, and as such should only be called on setup.
 //
@@ -50,7 +50,7 @@ type RoundsList struct {
 	Rounds []uint64
 }
 
-// makeRoundsList converts a list of id.Round into a binding-compatable
+// makeRoundsList converts a list of id.Round into a binding-compatible
 // RoundsList.
 func makeRoundsList(rounds ...id.Round) RoundsList {
 	rl := RoundsList{make([]uint64, len(rounds))}
@@ -140,7 +140,7 @@ func (c *Cmix) WaitForRoundResult(
 
 	timeout := time.Duration(timeoutMS) * time.Millisecond
 
-	err = c.api.GetCmix().GetRoundResults(timeout, f, rl...)
+	c.api.GetCmix().GetRoundResults(timeout, f, rl...)
 
-	return err
+	return nil
 }
diff --git a/bindings/e2eHandler.go b/bindings/e2eHandler.go
index 5ed379257b3fe30089f2643a8c3616526112ceaf..4b63407520f8e1aa756f2e749e4b0971fc72fdd2 100644
--- a/bindings/e2eHandler.go
+++ b/bindings/e2eHandler.go
@@ -47,6 +47,19 @@ func (e *E2e) GetReceptionID() []byte {
 	return e.api.GetE2E().GetReceptionID().Marshal()
 }
 
+// DeleteContact removes a partner from E2e's storage.
+//
+// Parameters:
+//  - partnerID - the marshalled bytes of id.ID.
+func (e *E2e) DeleteContact(partnerID []byte) error {
+	partner, err := id.Unmarshal(partnerID)
+	if err != nil {
+		return err
+	}
+
+	return e.api.DeleteContact(partner)
+}
+
 // GetAllPartnerIDs returns a list of all partner IDs that the user has an E2E
 // relationship with.
 //
diff --git a/bindings/fileTransfer.go b/bindings/fileTransfer.go
index dd3f01e92fb0004d0a73a0c7139e9a95a130e97d..4b8b5adde300191ba85b524fdd1499c3319b1998 100644
--- a/bindings/fileTransfer.go
+++ b/bindings/fileTransfer.go
@@ -33,12 +33,12 @@ type FileTransfer struct {
 //
 // Example JSON:
 //  {
-//   "TransferID":"B4Z9cwU18beRoGbk5xBjbcd5Ryi9ZUFA2UBvi8FOHWo=",
-//   "SenderID":"emV6aW1hAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
-//   "Preview":"aXQncyBtZSBhIHByZXZpZXc=",
-//   "Name":"testfile.txt",
-//   "Type":"text file",
-//   "Size":2048
+//    "TransferID":"B4Z9cwU18beRoGbk5xBjbcd5Ryi9ZUFA2UBvi8FOHWo=",
+//    "SenderID":"emV6aW1hAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD",
+//    "Preview":"aXQncyBtZSBhIHByZXZpZXc=",
+//    "Name":"testfile.txt",
+//    "Type":"text file",
+//    "Size":2048
 //  }
 type ReceivedFile struct {
 	TransferID []byte // ID of the file transfer
@@ -52,10 +52,10 @@ type ReceivedFile struct {
 // FileSend is a public struct that contains the file contents and its name,
 // type, and preview.
 //  {
-//   "Name":"testfile.txt",
-//   "Type":"text file",
-//   "Preview":"aXQncyBtZSBhIHByZXZpZXc=",
-//   "Contents":"VGhpcyBpcyB0aGUgZnVsbCBjb250ZW50cyBvZiB0aGUgZmlsZSBpbiBieXRlcw=="
+//    "Name":"testfile.txt",
+//    "Type":"text file",
+//    "Preview":"aXQncyBtZSBhIHByZXZpZXc=",
+//    "Contents":"VGhpcyBpcyB0aGUgZnVsbCBjb250ZW50cyBvZiB0aGUgZmlsZSBpbiBieXRlcw=="
 //  }
 type FileSend struct {
 	Name     string // Name of the file
@@ -69,10 +69,10 @@ type FileSend struct {
 //
 // Example JSON:
 //  {
-//   "Completed":false,
-//   "Transmitted":128,
-//   "Total":2048,
-//   "Err":null
+//    "Completed":false,
+//    "Transmitted":128,
+//    "Total":2048,
+//    "Err":null
 //  }
 type Progress struct {
 	Completed   bool  // Status of transfer (true if done)
@@ -183,8 +183,8 @@ func InitFileTransfer(e2eID int, receiveFileCallback ReceiveFileCallback,
 //  - retry - number of retries allowed
 //  - callback - callback that reports file sending progress
 //  - period - Duration (in ms) to wait between progress callbacks triggering.
-//             This value should depend on how frequently you want to receive
-//             updates, and should be tuned to your implementation.
+//    This value should depend on how frequently you want to receive updates,
+//    and should be tuned to your implementation.
 //
 // Returns:
 //  - []byte - unique file transfer ID
@@ -271,8 +271,8 @@ func (f *FileTransfer) CloseSend(tidBytes []byte) error {
 //  - tidBytes - file transfer ID
 //  - callback - callback that reports file reception progress
 //  - period - Duration (in ms) to wait between progress callbacks triggering.
-//             This value should depend on how frequently you want to receive
-//             updates, and should be tuned to your implementation.
+//    This value should depend on how frequently you want to receive updates,
+//    and should be tuned to your implementation.
 func (f *FileTransfer) RegisterSentProgressCallback(tidBytes []byte,
 	callback FileTransferSentProgressCallback, period int) error {
 	cb := func(completed bool, arrived, total uint16,
@@ -301,8 +301,8 @@ func (f *FileTransfer) RegisterSentProgressCallback(tidBytes []byte,
 //  - tidBytes - file transfer ID
 //  - callback - callback that reports file reception progress
 //  - period - Duration (in ms) to wait between progress callbacks triggering.
-//             This value should depend on how frequently you want to receive
-//             updates, and should be tuned to your implementation.
+//    This value should depend on how frequently you want to receive updates,
+//    and should be tuned to your implementation.
 func (f *FileTransfer) RegisterReceivedProgressCallback(tidBytes []byte,
 	callback FileTransferReceiveProgressCallback, period int) error {
 	cb := func(completed bool, received, total uint16,
diff --git a/bindings/follow.go b/bindings/follow.go
index 362ace98f2c8c66f53717b405153c261f75b2597..6f892e4151356b6bfa0906f2f746542aaae4c99e 100644
--- a/bindings/follow.go
+++ b/bindings/follow.go
@@ -10,6 +10,7 @@ package bindings
 import (
 	"encoding/json"
 	"fmt"
+	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/cmix/message"
 	"time"
 
@@ -82,6 +83,29 @@ func (c *Cmix) WaitForNetwork(timeoutMS int) bool {
 	return false
 }
 
+// ReadyToSend determines if the network is ready to send messages on. It
+// returns true if the network is healthy and if the client has registered with
+// at least 70% of the nodes. Returns false otherwise.
+func (c *Cmix) ReadyToSend() bool {
+	// Check if the network is currently healthy
+	if !c.api.GetCmix().IsHealthy() {
+		return false
+	}
+
+	// If the network is healthy, then check the number of nodes that the client
+	// is currently registered with
+	numReg, total, err := c.api.GetNodeRegistrationStatus()
+	if err != nil {
+		jww.FATAL.Panicf("Failed to get node registration status: %+v", err)
+	}
+
+	// FIXME: This is a fix put in place because not all nodes in the NDF are
+	//  online. This should be fixed.
+	total = 340
+
+	return numReg >= total*7/10
+}
+
 // NetworkFollowerStatus gets the state of the network follower. It returns a
 // status with the following values:
 //  Stopped  - 0
@@ -145,8 +169,8 @@ func (c *Cmix) IsHealthy() bool {
 //
 // JSON Example:
 //  {
-//   "FileTransfer{BatchBuilderThread, FilePartSendingThread#0, FilePartSendingThread#1, FilePartSendingThread#2, FilePartSendingThread#3}",
-//   "MessageReception Worker 0"
+//    "FileTransfer{BatchBuilderThread, FilePartSendingThread#0, FilePartSendingThread#1, FilePartSendingThread#2, FilePartSendingThread#3}",
+//    "MessageReception Worker 0"
 //  }
 func (c *Cmix) GetRunningProcesses() ([]byte, error) {
 	return json.Marshal(c.api.GetRunningProcesses())
diff --git a/bindings/group.go b/bindings/group.go
index 9ea24b1fdb76bbf89f4d5b04a8421b0cfe0ed960..b7faa628b8b1439b27b0f27413b7049e0f926a97 100644
--- a/bindings/group.go
+++ b/bindings/group.go
@@ -203,8 +203,8 @@ func (g *GroupChat) Send(groupId, message []byte, tag string) ([]byte, error) {
 
 	// Construct send report
 	sendReport := &GroupSendReport{
-		RoundsList: makeRoundsList(round),
-		RoundURL:   getRoundURL(round),
+		RoundURL:   getRoundURL(round.ID),
+		RoundsList: makeRoundsList(round.ID),
 		Timestamp:  timestamp.UnixNano(),
 		MessageID:  msgID.Bytes(),
 	}
@@ -322,7 +322,7 @@ func (g *Group) Serialize() []byte {
 	return g.g.Serialize()
 }
 
-// DeserializeGroup converts the results of Group.Serialize() into a Group
+// DeserializeGroup converts the results of Group.Serialize into a Group
 // so that its methods can be called.
 func DeserializeGroup(serializedGroupData []byte) (*Group, error) {
 	grp, err := gs.DeserializeGroup(serializedGroupData)
diff --git a/bindings/params.go b/bindings/params.go
index 3b4b515b2dd2745995adbba039188466617e82cb..c1444e70320f8351514f6d3ddbb2c26872329b83 100644
--- a/bindings/params.go
+++ b/bindings/params.go
@@ -78,27 +78,42 @@ func GetDefaultE2eFileTransferParams() []byte {
 	return data
 }
 
+// parseE2eFileTransferParams is a helper function which parses a JSON
+// marshalled [e2eFileTransfer.Params].
 func parseE2eFileTransferParams(data []byte) (e2eFileTransfer.Params, error) {
 	p := &e2eFileTransfer.Params{}
 	return *p, json.Unmarshal(data, p)
 }
 
+// parseSingleUseParams is a helper function which parses a JSON marshalled
+// [single.RequestParams].
 func parseSingleUseParams(data []byte) (single.RequestParams, error) {
 	p := &single.RequestParams{}
 	return *p, p.UnmarshalJSON(data)
 }
 
+// parseFileTransferParams is a helper function which parses a JSON marshalled
+// [fileTransfer.Params].
 func parseFileTransferParams(data []byte) (fileTransfer.Params, error) {
 	p := &fileTransfer.Params{}
 	return *p, json.Unmarshal(data, p)
 }
 
+// parseCMixParams is a helper function which parses a JSON marshalled
+// [xxdk.CMIXParams].
 func parseCMixParams(data []byte) (xxdk.CMIXParams, error) {
+	if len(data) == 0 {
+		jww.WARN.Printf("cMix params not specified, using defaults...")
+		data = GetDefaultCMixParams()
+	}
+
 	p := &xxdk.CMIXParams{}
 	err := p.Unmarshal(data)
 	return *p, err
 }
 
+// parseE2EParams is a helper function which parses a JSON marshalled
+// [xxdk.E2EParams].
 func parseE2EParams(data []byte) (xxdk.E2EParams, error) {
 	p := &xxdk.E2EParams{}
 	err := p.Unmarshal(data)
diff --git a/bindings/timeNow.go b/bindings/timeNow.go
new file mode 100644
index 0000000000000000000000000000000000000000..6aad2298f81a9651bbd6c80eb3b70a4ac4c9966a
--- /dev/null
+++ b/bindings/timeNow.go
@@ -0,0 +1,36 @@
+///////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                          //
+//                                                                           //
+// Use of this source code is governed by a license that can be found in the //
+// LICENSE file                                                              //
+///////////////////////////////////////////////////////////////////////////////
+
+package bindings
+
+import (
+	"gitlab.com/xx_network/primitives/netTime"
+	"time"
+)
+
+// SetTimeSource will set the time source that will be used when retrieving the
+// current time using [netTime.Now]. This should be called BEFORE Login()
+// and only be called once. Using this after Login is undefined behavior that
+// may result in a crash.
+//
+// Parameters:
+//  - timeNow is an object which adheres to [netTime.TimeSource]. Specifically,
+//    this object should a NowMs() method which return a 64-bit integer value.
+func SetTimeSource(timeNow netTime.TimeSource) {
+	netTime.SetTimeSource(timeNow)
+}
+
+// SetOffset will set an internal offset variable. All calls to [netTime.Now]
+// will have this offset applied to this value.
+//
+// Parameters:
+//  - offset is a time by which netTime.Now will be offset. This value may be
+//    negative or positive. This expects a 64-bit integer value which will
+//    represent the number in microseconds this offset will be.
+func SetOffset(offset int64) {
+	netTime.SetOffset(time.Duration(offset) * time.Microsecond)
+}
diff --git a/broadcast/asymmetric.go b/broadcast/asymmetric.go
deleted file mode 100644
index 37d5cac8417ccbdd98f8d3d28f1cfa6bf35b4816..0000000000000000000000000000000000000000
--- a/broadcast/asymmetric.go
+++ /dev/null
@@ -1,74 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-package broadcast
-
-import (
-	"encoding/binary"
-	"github.com/pkg/errors"
-	"gitlab.com/elixxir/client/cmix"
-	"gitlab.com/elixxir/client/cmix/message"
-	"gitlab.com/xx_network/crypto/multicastRSA"
-	"gitlab.com/xx_network/primitives/id"
-	"gitlab.com/xx_network/primitives/id/ephemeral"
-)
-
-const (
-	asymmetricBroadcastServiceTag = "AsymmBcast"
-	asymmCMixSendTag              = "AsymmetricBroadcast"
-	internalPayloadSizeLength     = 2
-)
-
-// BroadcastAsymmetric broadcasts the payload to the channel. Requires a healthy network state to send
-// Payload must be equal to bc.MaxAsymmetricPayloadSize, and the channel PrivateKey must be passed in
-func (bc *broadcastClient) BroadcastAsymmetric(pk multicastRSA.PrivateKey, payload []byte, cMixParams cmix.CMIXParams) (
-	id.Round, ephemeral.Id, error) {
-	// Confirm network health
-	if !bc.net.IsHealthy() {
-		return 0, ephemeral.Id{}, errors.New(errNetworkHealth)
-	}
-
-	// Check payload size
-	if len(payload) > bc.MaxAsymmetricPayloadSize() {
-		return 0, ephemeral.Id{},
-			errors.Errorf(errPayloadSize, len(payload), bc.MaxAsymmetricPayloadSize())
-	}
-	payloadLength := uint16(len(payload))
-
-	finalPayload := make([]byte, bc.maxAsymmetricPayloadSizeRaw())
-	binary.BigEndian.PutUint16(finalPayload[:internalPayloadSizeLength], payloadLength)
-	copy(finalPayload[internalPayloadSizeLength:], payload)
-
-	// Encrypt payload
-	encryptedPayload, mac, fp, err := bc.channel.EncryptAsymmetric(finalPayload, pk, bc.rng.GetStream())
-	if err != nil {
-		return 0, ephemeral.Id{}, errors.WithMessage(err, "Failed to encrypt asymmetric broadcast message")
-	}
-
-	// Create service using asymmetric broadcast service tag & channel reception ID
-	// Allows anybody with this info to listen for messages on this channel
-	service := message.Service{
-		Identifier: bc.channel.ReceptionID.Bytes(),
-		Tag:        asymmetricBroadcastServiceTag,
-	}
-
-	if cMixParams.DebugTag == cmix.DefaultDebugTag {
-		cMixParams.DebugTag = asymmCMixSendTag
-	}
-
-	// Create payload sized for sending over cmix
-	sizedPayload := make([]byte, bc.net.GetMaxMessageLength())
-	// Read random data into sized payload
-	_, err = bc.rng.GetStream().Read(sizedPayload)
-	if err != nil {
-		return 0, ephemeral.Id{}, errors.WithMessage(err, "Failed to add random data to sized broadcast")
-	}
-	copy(sizedPayload[:len(encryptedPayload)], encryptedPayload)
-
-	return bc.net.Send(
-		bc.channel.ReceptionID, fp, service, sizedPayload, mac, cMixParams)
-}
diff --git a/broadcast/broadcastClient.go b/broadcast/client.go
similarity index 57%
rename from broadcast/broadcastClient.go
rename to broadcast/client.go
index 71e9ab35a74d6e9c4597f082786aaa009702bee8..1b4891a00bdf4d8ae9ce6bf31c2b020b356fa114 100644
--- a/broadcast/broadcastClient.go
+++ b/broadcast/client.go
@@ -14,25 +14,30 @@ import (
 	"gitlab.com/elixxir/client/cmix/message"
 	crypto "gitlab.com/elixxir/crypto/broadcast"
 	"gitlab.com/elixxir/crypto/fastRNG"
-	"gitlab.com/xx_network/crypto/signature/rsa"
 )
 
-// broadcastClient implements the Channel interface for sending/receiving asymmetric or symmetric broadcast messages
+// broadcastClient implements the [broadcast.Channel] interface for sending/
+// receiving asymmetric or symmetric broadcast messages.
 type broadcastClient struct {
-	channel crypto.Channel
+	channel *crypto.Channel
 	net     Client
 	rng     *fastRNG.StreamGenerator
 }
 
-// NewBroadcastChannel creates a channel interface based on crypto.Channel, accepts net client connection & callback for received messages
-func NewBroadcastChannel(channel crypto.Channel, net Client, rng *fastRNG.StreamGenerator) (Channel, error) {
+type NewBroadcastChannelFunc func(channel *crypto.Channel, net Client,
+	rng *fastRNG.StreamGenerator) (Channel, error)
+
+// NewBroadcastChannel creates a channel interface based on [broadcast.Channel].
+// It accepts a Cmix client connection.
+func NewBroadcastChannel(channel *crypto.Channel, net Client,
+	rng *fastRNG.StreamGenerator) (Channel, error) {
 	bc := &broadcastClient{
 		channel: channel,
 		net:     net,
 		rng:     rng,
 	}
 
-	if !bc.verifyID() {
+	if !channel.Verify() {
 		return nil, errors.New("Failed ID verification for broadcast channel")
 	}
 
@@ -45,20 +50,21 @@ func NewBroadcastChannel(channel crypto.Channel, net Client, rng *fastRNG.Stream
 	return bc, nil
 }
 
-// RegisterListener adds a service to hear broadcast messages of a given type via the passed in callback
+// RegisterListener registers a listener for broadcast messages.
 func (bc *broadcastClient) RegisterListener(listenerCb ListenerFunc, method Method) error {
 	var tag string
 	switch method {
 	case Symmetric:
 		tag = symmetricBroadcastServiceTag
-	case Asymmetric:
-		tag = asymmetricBroadcastServiceTag
+	case RSAToPublic:
+		tag = asymmetricRSAToPublicBroadcastServiceTag
 	default:
-		return errors.Errorf("Cannot register listener for broadcast method %s", method)
+		return errors.Errorf(
+			"Cannot register listener for broadcast method %s", method)
 	}
 
 	p := &processor{
-		c:      &bc.channel,
+		c:      bc.channel,
 		cb:     listenerCb,
 		method: method,
 	}
@@ -72,8 +78,8 @@ func (bc *broadcastClient) RegisterListener(listenerCb ListenerFunc, method Meth
 	return nil
 }
 
-// Stop unregisters the listener callback and stops the channel's identity
-// from being tracked.
+// Stop unregisters the listener callback and stops the channel's identity from
+// being tracked.
 func (bc *broadcastClient) Stop() {
 	// Removes currently tracked identity
 	bc.net.RemoveIdentity(bc.channel.ReceptionID)
@@ -82,29 +88,27 @@ func (bc *broadcastClient) Stop() {
 	bc.net.DeleteClientService(bc.channel.ReceptionID)
 }
 
-// Get returns the underlying crypto.Channel object
-func (bc *broadcastClient) Get() crypto.Channel {
+// Get returns the underlying [broadcast.Channel] object.
+func (bc *broadcastClient) Get() *crypto.Channel {
 	return bc.channel
 }
 
-// verifyID generates a symmetric ID based on the info in the channel & compares it to the one passed in
-func (bc *broadcastClient) verifyID() bool {
-	gen, err := crypto.NewChannelID(bc.channel.Name, bc.channel.Description, bc.channel.Salt, rsa.CreatePublicKeyPem(bc.channel.RsaPubKey))
-	if err != nil {
-		jww.FATAL.Panicf("[verifyID] Failed to generate verified channel ID")
-		return false
-	}
-	return bc.channel.ReceptionID.Cmp(gen)
-}
-
+// MaxPayloadSize returns the maximum size for a symmetric broadcast payload.
 func (bc *broadcastClient) MaxPayloadSize() int {
 	return bc.maxSymmetricPayload()
 }
 
-func (bc *broadcastClient) MaxAsymmetricPayloadSize() int {
-	return bc.maxAsymmetricPayloadSizeRaw() - internalPayloadSizeLength
+func (bc *broadcastClient) maxSymmetricPayload() int {
+	return bc.channel.GetMaxSymmetricPayloadSize(bc.net.GetMaxMessageLength())
+}
+
+// MaxRSAToPublicPayloadSize return the maximum payload size for a
+// [broadcast.RSAToPublic] asymmetric payload.
+func (bc *broadcastClient) MaxRSAToPublicPayloadSize() int {
+	return bc.maxRSAToPublicPayloadSizeRaw() - internalPayloadSizeLength
 }
 
-func (bc *broadcastClient) maxAsymmetricPayloadSizeRaw() int {
-	return bc.channel.MaxAsymmetricPayloadSize()
+func (bc *broadcastClient) maxRSAToPublicPayloadSizeRaw() int {
+	size, _, _ := bc.channel.GetRSAToPublicMessageLength()
+	return size
 }
diff --git a/broadcast/interface.go b/broadcast/interface.go
index dc112ea8bebf457bf44f722c5332f001f27b67d3..3c674c0db3438862296e8980afcb8177eb3d2a9a 100644
--- a/broadcast/interface.go
+++ b/broadcast/interface.go
@@ -13,40 +13,69 @@ import (
 	"gitlab.com/elixxir/client/cmix/message"
 	"gitlab.com/elixxir/client/cmix/rounds"
 	crypto "gitlab.com/elixxir/crypto/broadcast"
-	"gitlab.com/elixxir/primitives/format"
-	"gitlab.com/xx_network/crypto/multicastRSA"
+	"gitlab.com/elixxir/crypto/rsa"
 	"gitlab.com/xx_network/primitives/id"
 	"gitlab.com/xx_network/primitives/id/ephemeral"
 	"time"
 )
 
-// ListenerFunc is registered when creating a new broadcasting channel
-// and receives all new broadcast messages for the channel.
+// ListenerFunc is registered when creating a new broadcasting channel and
+// receives all new broadcast messages for the channel.
 type ListenerFunc func(payload []byte,
 	receptionID receptionID.EphemeralIdentity, round rounds.Round)
 
-// Channel is the public-facing interface to interact with broadcast channels
+// Channel is the public-facing interface to interact with broadcast channels.
 type Channel interface {
-	// MaxPayloadSize returns the maximum size for a symmetric broadcast payload
+	// MaxPayloadSize returns the maximum size for a symmetric broadcast
+	// payload.
 	MaxPayloadSize() int
 
-	// MaxAsymmetricPayloadSize returns the maximum size for an asymmetric broadcast payload
-	MaxAsymmetricPayloadSize() int
+	// MaxRSAToPublicPayloadSize returns the maximum size for an asymmetric
+	// broadcast payload.
+	MaxRSAToPublicPayloadSize() int
 
-	// Get returns the underlying crypto.Channel
-	Get() crypto.Channel
+	// Get returns the underlying [broadcast.Channel] object.
+	Get() *crypto.Channel
 
-	// Broadcast broadcasts the payload to the channel. The payload size must be
-	// equal to MaxPayloadSize.
+	// Broadcast broadcasts a payload to the channel. The payload must be of the
+	// size [Channel.MaxPayloadSize] or smaller.
+	//
+	// The network must be healthy to send.
 	Broadcast(payload []byte, cMixParams cmix.CMIXParams) (
-		id.Round, ephemeral.Id, error)
+		rounds.Round, ephemeral.Id, error)
 
-	// BroadcastAsymmetric broadcasts an asymmetric payload to the channel. The payload size must be
-	// equal to MaxPayloadSize & private key for channel must be passed in
-	BroadcastAsymmetric(pk multicastRSA.PrivateKey, payload []byte, cMixParams cmix.CMIXParams) (
-		id.Round, ephemeral.Id, error)
+	// BroadcastWithAssembler broadcasts a payload over a channel with a payload
+	// assembled after the round is selected, allowing the round info to be
+	// included in the payload.
+	//
+	// The payload must be of the size [Channel.MaxPayloadSize] or smaller.
+	//
+	// The network must be healthy to send.
+	BroadcastWithAssembler(assembler Assembler, cMixParams cmix.CMIXParams) (
+		rounds.Round, ephemeral.Id, error)
 
-	// RegisterListener registers a listener for broadcast messages
+	// BroadcastRSAtoPublic broadcasts the payload to the channel.
+	//
+	// The payload must be of the size [Channel.MaxRSAToPublicPayloadSize] or
+	// smaller and the channel [rsa.PrivateKey] must be passed in.
+	//
+	// The network must be healthy to send.
+	BroadcastRSAtoPublic(pk rsa.PrivateKey, payload []byte,
+		cMixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error)
+
+	// BroadcastRSAToPublicWithAssembler broadcasts the payload to the channel
+	// with a function that builds the payload based upon the ID of the selected
+	// round.
+	//
+	// The payload must be of the size [Channel.MaxRSAToPublicPayloadSize] or
+	// smaller and the channel [rsa.PrivateKey] must be passed in.
+	//
+	// The network must be healthy to send.
+	BroadcastRSAToPublicWithAssembler(
+		pk rsa.PrivateKey, assembler Assembler,
+		cMixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error)
+
+	// RegisterListener registers a listener for broadcast messages.
 	RegisterListener(listenerCb ListenerFunc, method Method) error
 
 	// Stop unregisters the listener callback and stops the channel's identity
@@ -54,17 +83,19 @@ type Channel interface {
 	Stop()
 }
 
-// Client contains the methods from cmix.Client that are required by
-// symmetricClient.
+// Assembler is a function which allows a bre
+type Assembler func(rid id.Round) (payload []byte, err error)
+
+// Client contains the methods from [cmix.Client] that are required by
+// broadcastClient.
 type Client interface {
-	GetMaxMessageLength() int
-	Send(recipient *id.ID, fingerprint format.Fingerprint,
-		service message.Service, payload, mac []byte,
-		cMixParams cmix.CMIXParams) (id.Round, ephemeral.Id, error)
+	SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler,
+		cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error)
 	IsHealthy() bool
 	AddIdentity(id *id.ID, validUntil time.Time, persistent bool)
 	AddService(clientID *id.ID, newService message.Service,
 		response message.Processor)
 	DeleteClientService(clientID *id.ID)
 	RemoveIdentity(id *id.ID)
+	GetMaxMessageLength() int
 }
diff --git a/broadcast/method.go b/broadcast/method.go
index 8e93ea57dba204b2f616071e54d7317bcec96624..c154fc027b81a55f600c5f54fa3847aa7616ea89 100644
--- a/broadcast/method.go
+++ b/broadcast/method.go
@@ -12,15 +12,18 @@ type Method uint8
 
 const (
 	Symmetric Method = iota
-	Asymmetric
+	RSAToPublic
+	RSAToPrivate
 )
 
 func (m Method) String() string {
 	switch m {
 	case Symmetric:
 		return "Symmetric"
-	case Asymmetric:
-		return "Asymmetric"
+	case RSAToPublic:
+		return "RSAToPublic"
+	case RSAToPrivate:
+		return "RSAToPrivate"
 	default:
 		return "Unknown"
 	}
diff --git a/broadcast/processor.go b/broadcast/processor.go
index a387c9e43ea1f8cb376f90adc7d7589e6ef2fd16..8ee1f85d07ad093337711ef15e7f6e3ced3dd1f8 100644
--- a/broadcast/processor.go
+++ b/broadcast/processor.go
@@ -35,10 +35,8 @@ func (p *processor) Process(msg format.Message,
 	var payload []byte
 	var err error
 	switch p.method {
-	case Asymmetric:
-		encPartSize := p.c.RsaPubKey.Size()               // Size returned by multicast RSA encryption
-		encodedMessage := msg.GetContents()[:encPartSize] // Only one message is encoded, rest of it is random data
-		decodedMessage, decryptErr := p.c.DecryptAsymmetric(encodedMessage)
+	case RSAToPublic:
+		decodedMessage, decryptErr := p.c.DecryptRSAToPublic(msg.GetContents(), msg.GetMac(), msg.GetKeyFP())
 		if decryptErr != nil {
 			jww.ERROR.Printf(errDecrypt, p.c.ReceptionID, p.c.Name, decryptErr)
 			return
@@ -53,10 +51,10 @@ func (p *processor) Process(msg format.Message,
 			return
 		}
 	default:
-		jww.ERROR.Printf("Unrecognized broadcast method %d", p.method)
+		jww.FATAL.Panicf("Unrecognized broadcast method %d", p.method)
 	}
 
-	go p.cb(payload, receptionID, round)
+	p.cb(payload, receptionID, round)
 }
 
 // String returns a string identifying the symmetricProcessor for debugging purposes.
diff --git a/broadcast/processor_test.go b/broadcast/processor_test.go
index 6e0aa8edec9d054dc8c3283fc2689fd7eae71593..b758c20dbfa8756a31cc554671ab884d6b312a56 100644
--- a/broadcast/processor_test.go
+++ b/broadcast/processor_test.go
@@ -7,20 +7,7 @@
 
 package broadcast
 
-import (
-	"bytes"
-	"gitlab.com/elixxir/client/cmix/identity/receptionID"
-	"gitlab.com/elixxir/client/cmix/rounds"
-	crypto "gitlab.com/elixxir/crypto/broadcast"
-	"gitlab.com/elixxir/crypto/cmix"
-	"gitlab.com/elixxir/primitives/format"
-	"gitlab.com/xx_network/crypto/csprng"
-	"gitlab.com/xx_network/crypto/signature/rsa"
-	"gitlab.com/xx_network/primitives/id"
-	"testing"
-	"time"
-)
-
+/*
 // Tests that process.Process properly decrypts the payload and passes it to the
 // callback.
 func Test_processor_Process(t *testing.T) {
@@ -37,7 +24,7 @@ func Test_processor_Process(t *testing.T) {
 		RsaPubKey:   rsaPrivKey.GetPublic(),
 	}
 
-	cbChan := make(chan []byte)
+	cbChan := make(chan []byte, 1)
 	cb := func(payload []byte, _ receptionID.EphemeralIdentity, _ rounds.Round) {
 		cbChan <- payload
 	}
@@ -67,4 +54,4 @@ func Test_processor_Process(t *testing.T) {
 	case <-time.After(15 * time.Millisecond):
 		t.Error("Timed out waiting for listener channel to be called.")
 	}
-}
+}*/
diff --git a/broadcast/rsaToPublic.go b/broadcast/rsaToPublic.go
new file mode 100644
index 0000000000000000000000000000000000000000..26c4d0924af04c6464610f20fe8040ac1aa7b082
--- /dev/null
+++ b/broadcast/rsaToPublic.go
@@ -0,0 +1,121 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+package broadcast
+
+import (
+	"encoding/binary"
+	"github.com/pkg/errors"
+	"gitlab.com/elixxir/client/cmix"
+	"gitlab.com/elixxir/client/cmix/message"
+	"gitlab.com/elixxir/client/cmix/rounds"
+	"gitlab.com/elixxir/crypto/rsa"
+	"gitlab.com/elixxir/primitives/format"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/id/ephemeral"
+)
+
+const (
+	asymmetricRSAToPublicBroadcastServiceTag = "AsymmToPublicBcast"
+	asymmCMixSendTag                         = "AsymmetricBroadcast"
+	internalPayloadSizeLength                = 2
+)
+
+// BroadcastRSAtoPublic broadcasts the payload to the channel. Requires a
+// healthy network state to send Payload length less than or equal to
+// bc.MaxRSAToPublicPayloadSize, and the channel PrivateKey must be passed in
+//
+// BroadcastRSAtoPublic broadcasts the payload to the channel.
+//
+// The payload must be of the size [broadcastClient.MaxRSAToPublicPayloadSize]
+// or smaller and the channel [rsa.PrivateKey] must be passed in.
+//
+// The network must be healthy to send.
+func (bc *broadcastClient) BroadcastRSAtoPublic(pk rsa.PrivateKey,
+	payload []byte, cMixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
+	// Confirm network health
+
+	assemble := func(rid id.Round) ([]byte, error) {
+		return payload, nil
+	}
+	return bc.BroadcastRSAToPublicWithAssembler(pk, assemble, cMixParams)
+}
+
+// BroadcastRSAToPublicWithAssembler broadcasts the payload to the channel
+// with a function that builds the payload based upon the ID of the selected
+// round.
+//
+// The payload must be of the size [broadcastClient.MaxRSAToPublicPayloadSize]
+// or smaller and the channel [rsa.PrivateKey] must be passed in.
+//
+// The network must be healthy to send.
+func (bc *broadcastClient) BroadcastRSAToPublicWithAssembler(
+	pk rsa.PrivateKey, assembler Assembler,
+	cMixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
+	// Confirm network health
+	if !bc.net.IsHealthy() {
+		return rounds.Round{}, ephemeral.Id{}, errors.New(errNetworkHealth)
+	}
+
+	assemble := func(rid id.Round) (fp format.Fingerprint,
+		service message.Service, encryptedPayload, mac []byte, err error) {
+		payload, err := assembler(rid)
+		if err != nil {
+			return format.Fingerprint{}, message.Service{}, nil,
+				nil, err
+		}
+		// Check payload size
+		if len(payload) > bc.MaxRSAToPublicPayloadSize() {
+			return format.Fingerprint{}, message.Service{}, nil,
+				nil, errors.Errorf(errPayloadSize, len(payload),
+					bc.MaxRSAToPublicPayloadSize())
+		}
+		payloadLength := uint16(len(payload))
+
+		finalPayload := make([]byte, bc.maxRSAToPublicPayloadSizeRaw())
+		binary.BigEndian.PutUint16(finalPayload[:internalPayloadSizeLength],
+			payloadLength)
+		copy(finalPayload[internalPayloadSizeLength:], payload)
+
+		// Encrypt payload
+		encryptedPayload, mac, fp, err =
+			bc.channel.EncryptRSAToPublic(finalPayload, pk, bc.net.GetMaxMessageLength(),
+				bc.rng.GetStream())
+		if err != nil {
+			return format.Fingerprint{}, message.Service{}, nil,
+				nil, errors.WithMessage(err, "Failed to encrypt "+
+					"asymmetric broadcast message")
+		}
+
+		// Create service using asymmetric broadcast service tag & channel
+		// reception ID allows anybody with this info to listen for messages on
+		// this channel
+		service = message.Service{
+			Identifier: bc.channel.ReceptionID.Bytes(),
+			Tag:        asymmetricRSAToPublicBroadcastServiceTag,
+		}
+
+		if cMixParams.DebugTag == cmix.DefaultDebugTag {
+			cMixParams.DebugTag = asymmCMixSendTag
+		}
+
+		// Create payload sized for sending over cmix
+		sizedPayload := make([]byte, bc.net.GetMaxMessageLength())
+		// Read random data into sized payload
+		_, err = bc.rng.GetStream().Read(sizedPayload)
+		if err != nil {
+			return format.Fingerprint{}, message.Service{}, nil,
+				nil, errors.WithMessage(err, "Failed to add "+
+					"random data to sized broadcast")
+		}
+		copy(sizedPayload[:len(encryptedPayload)], encryptedPayload)
+
+		return
+	}
+
+	return bc.net.SendWithAssembler(bc.channel.ReceptionID, assemble, cMixParams)
+}
diff --git a/broadcast/asymmetric_test.go b/broadcast/rsaToPublic_test.go
similarity index 60%
rename from broadcast/asymmetric_test.go
rename to broadcast/rsaToPublic_test.go
index e817688ded54df75a0cad3da24bdb85624e5e8dd..9be17453b96db68ca8a735fdcd9234ccd7ad2e3e 100644
--- a/broadcast/asymmetric_test.go
+++ b/broadcast/rsaToPublic_test.go
@@ -10,56 +10,71 @@ package broadcast
 import (
 	"bytes"
 	"fmt"
+	"reflect"
+	"sync"
+	"testing"
+	"time"
+
+	"gitlab.com/xx_network/crypto/csprng"
+
 	"gitlab.com/elixxir/client/cmix"
 	"gitlab.com/elixxir/client/cmix/identity/receptionID"
+	"gitlab.com/elixxir/client/cmix/message"
 	"gitlab.com/elixxir/client/cmix/rounds"
 	crypto "gitlab.com/elixxir/crypto/broadcast"
-	cMixCrypto "gitlab.com/elixxir/crypto/cmix"
 	"gitlab.com/elixxir/crypto/fastRNG"
-	"gitlab.com/xx_network/crypto/csprng"
-	"gitlab.com/xx_network/crypto/signature/rsa"
-	"reflect"
-	"sync"
-	"testing"
-	"time"
+	"gitlab.com/elixxir/primitives/format"
 )
 
-// Tests that symmetricClient adheres to the Symmetric interface.
+// Tests that broadcastClient adheres to the Channel interface.
 var _ Channel = (*broadcastClient)(nil)
 
-// Tests that symmetricClient adheres to the Symmetric interface.
+// Tests that cmix.Client adheres to the Client interface.
 var _ Client = (cmix.Client)(nil)
 
+// Tests that mockProcessor adheres to the message.Processor interface.
+var _ message.Processor = (*mockProcessor)(nil)
+
+// mockProcessor adheres to the message.Processor interface.
+type mockProcessor struct {
+	messages []format.Message
+}
+
+func newMockProcessor() *mockProcessor {
+	m := new(mockProcessor)
+	m.messages = make([]format.Message, 0)
+	return m
+}
+func (p *mockProcessor) Process(message format.Message,
+	_ receptionID.EphemeralIdentity, _ rounds.Round) {
+	p.messages = append(p.messages, message)
+}
+func (p *mockProcessor) String() string { return "hello" }
+
 func Test_asymmetricClient_Smoke(t *testing.T) {
 	cMixHandler := newMockCmixHandler()
 	rngGen := fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG)
-	pk, err := rsa.GenerateKey(rngGen.GetStream(), 4096)
-	if err != nil {
-		t.Fatalf("Failed to generate priv key: %+v", err)
-	}
-	cname := "MyChannel"
-	cdesc := "This is my channel about stuff."
-	csalt := cMixCrypto.NewSalt(csprng.NewSystemRNG(), 32)
-	cpubkey := pk.GetPublic()
-	cid, err := crypto.NewChannelID(cname, cdesc, csalt, rsa.CreatePublicKeyPem(cpubkey))
-	if err != nil {
-		t.Errorf("Failed to create channel ID: %+v", err)
-	}
-	channel := crypto.Channel{
-		ReceptionID: cid,
-		Name:        cname,
-		Description: cdesc,
-		Salt:        csalt,
-		RsaPubKey:   cpubkey,
-	}
+	cName := "MyChannel"
+	cDesc := "This is my channel about stuff."
+	packetPayloadLength := newMockCmix(cMixHandler).GetMaxMessageLength()
+
+	channel, pk, _ := crypto.NewChannel(
+		cName, cDesc, crypto.Public, packetPayloadLength, rngGen.GetStream())
+	cid := channel.ReceptionID
+
+	// Must mutate cMixHandler such that it's processorMap contains a
+	// message.Processor
+	mockProc := newMockProcessor()
+	cMixHandler.processorMap[*cid] = make(map[string][]message.Processor)
+	cMixHandler.processorMap[*cid]["AsymmBcast"] = []message.Processor{mockProc}
 
-	const n = 5
+	const n = 1
 	cbChans := make([]chan []byte, n)
 	clients := make([]Channel, n)
 	for i := range clients {
 		cbChan := make(chan []byte, 10)
-		cb := func(payload []byte, _ receptionID.EphemeralIdentity,
-			_ rounds.Round) {
+		cb := func(
+			payload []byte, _ receptionID.EphemeralIdentity, _ rounds.Round) {
 			cbChan <- payload
 		}
 
@@ -68,7 +83,7 @@ func Test_asymmetricClient_Smoke(t *testing.T) {
 			t.Errorf("Failed to create broadcast channel: %+v", err)
 		}
 
-		err = s.RegisterListener(cb, Asymmetric)
+		err = s.RegisterListener(cb, RSAToPublic)
 		if err != nil {
 			t.Errorf("Failed to register listener: %+v", err)
 		}
@@ -76,7 +91,7 @@ func Test_asymmetricClient_Smoke(t *testing.T) {
 		cbChans[i] = cbChan
 		clients[i] = s
 
-		// Test that Get returns the expected channel
+		// Test that Channel.Get returns the expected channel
 		if !reflect.DeepEqual(s.Get(), channel) {
 			t.Errorf("Cmix %d returned wrong channel."+
 				"\nexpected: %+v\nreceived: %+v", i, channel, s.Get())
@@ -85,7 +100,7 @@ func Test_asymmetricClient_Smoke(t *testing.T) {
 
 	// Send broadcast from each client
 	for i := range clients {
-		payload := make([]byte, clients[i].MaxAsymmetricPayloadSize())
+		payload := make([]byte, clients[i].MaxRSAToPublicPayloadSize())
 		copy(payload,
 			fmt.Sprintf("Hello from client %d of %d.", i, len(clients)))
 
@@ -98,9 +113,9 @@ func Test_asymmetricClient_Smoke(t *testing.T) {
 				select {
 				case r := <-cbChan:
 					if !bytes.Equal(payload, r) {
-						t.Errorf("Cmix %d failed to receive expected "+
-							"payload from client %d."+
-							"\nexpected: %q\nreceived: %q", j, i, payload, r)
+						t.Errorf("Cmix %d failed to receive expected payload "+
+							"from client %d.\nexpected: %q\nreceived: %q",
+							j, i, payload, r)
 					}
 				case <-time.After(time.Second):
 					t.Errorf("Cmix %d timed out waiting for broadcast "+
@@ -110,7 +125,8 @@ func Test_asymmetricClient_Smoke(t *testing.T) {
 		}
 
 		// Broadcast payload
-		_, _, err := clients[i].BroadcastAsymmetric(pk, payload, cmix.GetDefaultCMIXParams())
+		_, _, err := clients[i].BroadcastRSAtoPublic(
+			pk, payload, cmix.GetDefaultCMIXParams())
 		if err != nil {
 			t.Errorf("Cmix %d failed to send broadcast: %+v", i, err)
 		}
@@ -124,7 +140,7 @@ func Test_asymmetricClient_Smoke(t *testing.T) {
 		clients[i].Stop()
 	}
 
-	payload := make([]byte, clients[0].MaxAsymmetricPayloadSize())
+	payload := make([]byte, clients[0].MaxRSAToPublicPayloadSize())
 	copy(payload, "This message should not get through.")
 
 	// Start waiting on channels and error if anything is received
@@ -142,7 +158,7 @@ func Test_asymmetricClient_Smoke(t *testing.T) {
 	}
 
 	// Broadcast payload
-	_, _, err = clients[0].BroadcastAsymmetric(pk, payload, cmix.GetDefaultCMIXParams())
+	_, _, err := clients[0].BroadcastRSAtoPublic(pk, payload, cmix.GetDefaultCMIXParams())
 	if err != nil {
 		t.Errorf("Cmix 0 failed to send broadcast: %+v", err)
 	}
diff --git a/broadcast/sizedBroadcast.go b/broadcast/sizedBroadcast.go
deleted file mode 100644
index e7099e02b089b3b0c8eaddcd4903313ce9d79e2c..0000000000000000000000000000000000000000
--- a/broadcast/sizedBroadcast.go
+++ /dev/null
@@ -1,83 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-package broadcast
-
-import (
-	"encoding/binary"
-	"github.com/pkg/errors"
-)
-
-// Message field sizes.
-const (
-	sizeSize              = 2
-	sizedBroadcastMinSize = sizeSize
-)
-
-// Error messages.
-const (
-	// NewSizedBroadcast
-	errNewSizedBroadcastMaxSize = "size of payload and its size %d too large to fit in max payload size %d"
-
-	// DecodeSizedBroadcast
-	errDecodeSizedBroadcastDataLen = "size of data %d must be greater than %d"
-	errDecodeSizedBroadcastSize    = "stated payload size %d larger than provided data %d"
-)
-
-/*
-+---------------------------+
-|   cMix Message Contents   |
-+---------+-----------------+
-|  Size   |     Payload     |
-| 2 bytes | remaining space |
-+---------+-----------------+
-*/
-
-// NewSizedBroadcast creates a new broadcast payload of size maxPayloadSize that
-// contains the given payload so that it fits completely inside a broadcasted
-// cMix message payload. The length of the payload is stored internally and used
-// to strip extraneous padding when decoding the payload.
-// The maxPayloadSize is the maximum size of the resulting payload. Returns an
-// error when the provided payload cannot fit in the max payload size.
-func NewSizedBroadcast(maxPayloadSize int, payload []byte) ([]byte, error) {
-	if len(payload)+sizedBroadcastMinSize > maxPayloadSize {
-		return nil, errors.Errorf(errNewSizedBroadcastMaxSize,
-			len(payload)+sizedBroadcastMinSize, maxPayloadSize)
-	}
-
-	b := make([]byte, sizeSize)
-	binary.LittleEndian.PutUint16(b, uint16(len(payload)))
-
-	sizedPayload := make([]byte, maxPayloadSize)
-	copy(sizedPayload, append(b, payload...))
-
-	return sizedPayload, nil
-}
-
-// DecodeSizedBroadcast decodes the data into its original payload stripping off
-// extraneous padding.
-func DecodeSizedBroadcast(data []byte) ([]byte, error) {
-	if len(data) < sizedBroadcastMinSize {
-		return nil, errors.Errorf(
-			errDecodeSizedBroadcastDataLen, len(data), sizedBroadcastMinSize)
-	}
-
-	size := binary.LittleEndian.Uint16(data[:sizeSize])
-	if int(size) > len(data[sizeSize:]) {
-		return nil, errors.Errorf(
-			errDecodeSizedBroadcastSize, size, len(data[sizeSize:]))
-	}
-
-	return data[sizeSize : size+sizeSize], nil
-}
-
-// MaxSizedBroadcastPayloadSize returns the maximum size of a payload that can
-// fit in a sized broadcast message for the given maximum cMix message payload
-// size.
-func MaxSizedBroadcastPayloadSize(maxPayloadSize int) int {
-	return maxPayloadSize - sizedBroadcastMinSize
-}
diff --git a/broadcast/sizedBroadcast_test.go b/broadcast/sizedBroadcast_test.go
deleted file mode 100644
index d81b439c097574e0c58d35525da79d14a1334471..0000000000000000000000000000000000000000
--- a/broadcast/sizedBroadcast_test.go
+++ /dev/null
@@ -1,115 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-package broadcast
-
-import (
-	"bytes"
-	"fmt"
-	"testing"
-)
-
-// Tests that a payload smaller than the max payload size encoded via
-// NewSizedBroadcast and decoded via DecodeSizedBroadcast matches the original.
-func TestNewSizedBroadcast_DecodeSizedBroadcast_SmallPayload(t *testing.T) {
-	const maxPayloadSize = 512
-	payload := []byte("This is my payload message.")
-
-	data, err := NewSizedBroadcast(maxPayloadSize, payload)
-	if err != nil {
-		t.Errorf("NewSizedBroadcast returned an error: %+v", err)
-	}
-
-	decodedPayload, err := DecodeSizedBroadcast(data)
-	if err != nil {
-		t.Errorf("DecodeSizedBroadcast returned an error: %+v", err)
-	}
-
-	if !bytes.Equal(payload, decodedPayload) {
-		t.Errorf("Decoded payload does not match original."+
-			"\nexpected: %q\nreceived: %q", payload, decodedPayload)
-	}
-}
-
-// Tests that a payload the same size as the max payload size encoded via
-// NewSizedBroadcast and decoded via DecodeSizedBroadcast matches the original.
-func TestNewSizedBroadcast_DecodeSizedBroadcast_FullSizesPayload(t *testing.T) {
-	payload := []byte("This is my payload message.")
-	maxPayloadSize := len(payload) + sizeSize
-
-	data, err := NewSizedBroadcast(maxPayloadSize, payload)
-	if err != nil {
-		t.Errorf("NewSizedBroadcast returned an error: %+v", err)
-	}
-
-	decodedPayload, err := DecodeSizedBroadcast(data)
-	if err != nil {
-		t.Errorf("DecodeSizedBroadcast returned an error: %+v", err)
-	}
-
-	if !bytes.Equal(payload, decodedPayload) {
-		t.Errorf("Decoded payload does not match original."+
-			"\nexpected: %q\nreceived: %q", payload, decodedPayload)
-	}
-}
-
-// Error path: tests that NewSizedBroadcast returns an error when the payload is
-// larger than the max payload size.
-func TestNewSizedBroadcast_MaxPayloadSizeError(t *testing.T) {
-	payload := []byte("This is my payload message.")
-	maxPayloadSize := len(payload)
-	expectedErr := fmt.Sprintf(errNewSizedBroadcastMaxSize,
-		len(payload)+sizedBroadcastMinSize, maxPayloadSize)
-
-	_, err := NewSizedBroadcast(maxPayloadSize, payload)
-	if err == nil || err.Error() != expectedErr {
-		t.Errorf("NewSizedBroadcast did not return the expected error when "+
-			"the payload is too large.\nexpected: %s\nreceived: %+v",
-			expectedErr, err)
-	}
-}
-
-// Error path: tests that DecodeSizedBroadcast returns an error when the length
-// of the data is shorter than the minimum length of a sized broadcast.
-func TestDecodeSizedBroadcast_DataTooShortError(t *testing.T) {
-	data := []byte{0}
-	expectedErr := fmt.Sprintf(
-		errDecodeSizedBroadcastDataLen, len(data), sizedBroadcastMinSize)
-
-	_, err := DecodeSizedBroadcast(data)
-	if err == nil || err.Error() != expectedErr {
-		t.Errorf("DecodeSizedBroadcast did not return the expected error "+
-			"when the data is too small.\nexpected: %s\nreceived: %+v",
-			expectedErr, err)
-	}
-}
-
-// Error path: tests that DecodeSizedBroadcast returns an error when the payload
-// size is larger than the actual payload contained in the data.
-func TestDecodeSizedBroadcast_SizeMismatchError(t *testing.T) {
-	data := []byte{255, 0, 10}
-	expectedErr := fmt.Sprintf(
-		errDecodeSizedBroadcastSize, data[0], len(data[sizeSize:]))
-
-	_, err := DecodeSizedBroadcast(data)
-	if err == nil || err.Error() != expectedErr {
-		t.Errorf("DecodeSizedBroadcast did not return the expected error "+
-			"when the size is too large.\nexpected: %s\nreceived: %+v",
-			expectedErr, err)
-	}
-}
-
-// Tests that MaxSizedBroadcastPayloadSize returns the correct max size.
-func TestMaxSizedBroadcastPayloadSize(t *testing.T) {
-	maxPayloadSize := 512
-	expectedSize := maxPayloadSize - sizedBroadcastMinSize
-	receivedSize := MaxSizedBroadcastPayloadSize(maxPayloadSize)
-	if receivedSize != expectedSize {
-		t.Errorf("Incorrect max paylaod size.\nexpected: %d\nreceived: %d",
-			expectedSize, receivedSize)
-	}
-}
diff --git a/broadcast/symmetric.go b/broadcast/symmetric.go
index 9a6885c8bc59fbd5b0d9482e99bbeefcc404c6be..432c53c8cab2ae80ea16f915d0142136a0d1532d 100644
--- a/broadcast/symmetric.go
+++ b/broadcast/symmetric.go
@@ -11,13 +11,15 @@ import (
 	"github.com/pkg/errors"
 	"gitlab.com/elixxir/client/cmix"
 	"gitlab.com/elixxir/client/cmix/message"
+	"gitlab.com/elixxir/client/cmix/rounds"
+	"gitlab.com/elixxir/primitives/format"
 	"gitlab.com/xx_network/primitives/id"
 	"gitlab.com/xx_network/primitives/id/ephemeral"
 )
 
 // Error messages.
 const (
-	// symmetricClient.Broadcast
+	// broadcastClient.Broadcast
 	errNetworkHealth       = "cannot send broadcast when the network is not healthy"
 	errPayloadSize         = "size of payload %d must be less than %d"
 	errBroadcastMethodType = "cannot call %s broadcast using %s channel"
@@ -29,41 +31,68 @@ const (
 	symmetricBroadcastServiceTag = "SymmetricBroadcast"
 )
 
-// MaxSymmetricPayloadSize returns the maximum size for a broadcasted payload.
-func (bc *broadcastClient) maxSymmetricPayload() int {
-	return bc.net.GetMaxMessageLength()
+// Broadcast broadcasts a payload to a symmetric channel. The payload must be of
+// size [broadcastClient.MaxPayloadSize] or smaller.
+//
+// The network must be healthy to send.
+func (bc *broadcastClient) Broadcast(payload []byte, cMixParams cmix.CMIXParams) (
+	rounds.Round, ephemeral.Id, error) {
+	assemble := func(rid id.Round) ([]byte, error) {
+		return payload, nil
+	}
+	return bc.BroadcastWithAssembler(assemble, cMixParams)
 }
 
-// Broadcast broadcasts a payload over a symmetric channel.
-// Network must be healthy to send
-// Requires a payload of size bc.MaxSymmetricPayloadSize()
-func (bc *broadcastClient) Broadcast(payload []byte, cMixParams cmix.CMIXParams) (
-	id.Round, ephemeral.Id, error) {
+// BroadcastWithAssembler broadcasts a payload over a symmetric channel with a
+// payload assembled after the round is selected, allowing the round info to be
+// included in the payload.
+//
+// The payload must be of the size [Channel.MaxPayloadSize] or smaller.
+//
+// The network must be healthy to send.
+func (bc *broadcastClient) BroadcastWithAssembler(assembler Assembler, cMixParams cmix.CMIXParams) (
+	rounds.Round, ephemeral.Id, error) {
 	if !bc.net.IsHealthy() {
-		return 0, ephemeral.Id{}, errors.New(errNetworkHealth)
+		return rounds.Round{}, ephemeral.Id{}, errors.New(errNetworkHealth)
 	}
 
-	if len(payload) != bc.maxSymmetricPayload() {
-		return 0, ephemeral.Id{},
-			errors.Errorf(errPayloadSize, len(payload), bc.maxSymmetricPayload())
-	}
+	assemble := func(rid id.Round) (fp format.Fingerprint,
+		service message.Service, encryptedPayload, mac []byte, err error) {
 
-	// Encrypt payload
-	rng := bc.rng.GetStream()
-	encryptedPayload, mac, fp := bc.channel.EncryptSymmetric(payload, rng)
-	rng.Close()
+		//assemble the passed payload
+		payload, err := assembler(rid)
+		if err != nil {
+			return format.Fingerprint{}, message.Service{}, nil, nil, err
+		}
 
-	// Create service using symmetric broadcast service tag & channel reception ID
-	// Allows anybody with this info to listen for messages on this channel
-	service := message.Service{
-		Identifier: bc.channel.ReceptionID.Bytes(),
-		Tag:        symmetricBroadcastServiceTag,
-	}
+		if len(payload) > bc.maxSymmetricPayload() {
+			return format.Fingerprint{}, message.Service{}, nil, nil,
+				errors.Errorf(errPayloadSize, len(payload), bc.maxSymmetricPayload())
+		}
+
+		// Encrypt payload
+		rng := bc.rng.GetStream()
+		defer rng.Close()
+		encryptedPayload, mac, fp, err = bc.channel.EncryptSymmetric(payload,
+			bc.net.GetMaxMessageLength(), rng)
+		if err != nil {
+			return format.Fingerprint{}, message.Service{},
+				nil, nil, err
+		}
+
+		// Create service using symmetric broadcast service tag & channel reception ID
+		// Allows anybody with this info to listen for messages on this channel
+		service = message.Service{
+			Identifier: bc.channel.ReceptionID.Bytes(),
+			Tag:        symmetricBroadcastServiceTag,
+		}
 
-	if cMixParams.DebugTag == cmix.DefaultDebugTag {
-		cMixParams.DebugTag = symmCMixSendTag
+		if cMixParams.DebugTag == cmix.DefaultDebugTag {
+			cMixParams.DebugTag = symmCMixSendTag
+		}
+		return
 	}
 
-	return bc.net.Send(
-		bc.channel.ReceptionID, fp, service, encryptedPayload, mac, cMixParams)
+	return bc.net.SendWithAssembler(bc.channel.ReceptionID, assemble,
+		cMixParams)
 }
diff --git a/broadcast/symmetric_test.go b/broadcast/symmetric_test.go
index c339838c68cd6b2b2cebadef08157bb31a5d735f..3386a71ead8fb2ffada89329017975216be91507 100644
--- a/broadcast/symmetric_test.go
+++ b/broadcast/symmetric_test.go
@@ -7,6 +7,7 @@
 
 package broadcast
 
+/*
 import (
 	"bytes"
 	"fmt"
@@ -14,10 +15,9 @@ import (
 	"gitlab.com/elixxir/client/cmix/identity/receptionID"
 	"gitlab.com/elixxir/client/cmix/rounds"
 	crypto "gitlab.com/elixxir/crypto/broadcast"
-	cMixCrypto "gitlab.com/elixxir/crypto/cmix"
+
 	"gitlab.com/elixxir/crypto/fastRNG"
 	"gitlab.com/xx_network/crypto/csprng"
-	"gitlab.com/xx_network/crypto/signature/rsa"
 	"reflect"
 	"sync"
 	"testing"
@@ -38,19 +38,10 @@ func Test_symmetricClient_Smoke(t *testing.T) {
 	rngGen := fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG)
 	cname := "MyChannel"
 	cdesc := "This is my channel about stuff."
-	csalt := cMixCrypto.NewSalt(csprng.NewSystemRNG(), 32)
-	cpubkey := newRsaPubKey(64, t)
-	cid, err := crypto.NewChannelID(cname, cdesc, csalt, rsa.CreatePublicKeyPem(cpubkey))
-	if err != nil {
-		t.Errorf("Failed to create channel ID: %+v", err)
-	}
-	channel := crypto.Channel{
-		ReceptionID: cid,
-		Name:        cname,
-		Description: cdesc,
-		Salt:        csalt,
-		RsaPubKey:   cpubkey,
-	}
+	mCmix := newMockCmix(cMixHandler)
+	channel,_,_ := crypto.NewChannel(cname, cdesc,
+		mCmix.GetMaxMessageLength(),
+		rngGen.GetStream())
 
 	// Set up callbacks, callback channels, and the symmetric clients
 	const n = 5
@@ -85,7 +76,7 @@ func Test_symmetricClient_Smoke(t *testing.T) {
 
 	// Send broadcast from each client
 	for i := range clients {
-		payload := make([]byte, newMockCmix(cMixHandler).GetMaxMessageLength())
+		payload := make([]byte, clients[i].MaxPayloadSize())
 		copy(payload,
 			fmt.Sprintf("Hello from client %d of %d.", i, len(clients)))
 
@@ -142,10 +133,10 @@ func Test_symmetricClient_Smoke(t *testing.T) {
 	}
 
 	// Broadcast payload
-	_, _, err = clients[0].Broadcast(payload, cmix.GetDefaultCMIXParams())
+	_, _, err := clients[0].Broadcast(payload, cmix.GetDefaultCMIXParams())
 	if err != nil {
 		t.Errorf("Cmix 0 failed to send broadcast: %+v", err)
 	}
 
 	wg.Wait()
-}
+}*/
diff --git a/broadcast/utils_test.go b/broadcast/utils_test.go
index 5ed5d22f099783ed5d47bdfff84451af96dc5378..ee1dfebc264c208e037bcbd45385bac6160fa154 100644
--- a/broadcast/utils_test.go
+++ b/broadcast/utils_test.go
@@ -8,33 +8,21 @@
 package broadcast
 
 import (
+	"sync"
+	"time"
+
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/id/ephemeral"
+
 	"gitlab.com/elixxir/client/cmix"
 	"gitlab.com/elixxir/client/cmix/identity/receptionID"
 	"gitlab.com/elixxir/client/cmix/message"
 	"gitlab.com/elixxir/client/cmix/rounds"
 	"gitlab.com/elixxir/primitives/format"
-	"gitlab.com/xx_network/crypto/signature/rsa"
-	"gitlab.com/xx_network/primitives/id"
-	"gitlab.com/xx_network/primitives/id/ephemeral"
-	"math/rand"
-	"sync"
-	"testing"
-	"time"
 )
 
-// newRsaPubKey generates a new random RSA public key for testing.
-func newRsaPubKey(seed int64, t *testing.T) *rsa.PublicKey {
-	prng := rand.New(rand.NewSource(seed))
-	privKey, err := rsa.GenerateKey(prng, 64)
-	if err != nil {
-		t.Errorf("Failed to generate new RSA key: %+v", err)
-	}
-
-	return privKey.GetPublic()
-}
-
 ////////////////////////////////////////////////////////////////////////////////
-// Mock cMix                                                           //
+// Mock cMix                                                                  //
 ////////////////////////////////////////////////////////////////////////////////
 
 type mockCmixHandler struct {
@@ -56,7 +44,7 @@ type mockCmix struct {
 
 func newMockCmix(handler *mockCmixHandler) *mockCmix {
 	return &mockCmix{
-		numPrimeBytes: 4096,
+		numPrimeBytes: 4096 / 8,
 		health:        true,
 		handler:       handler,
 	}
@@ -66,6 +54,30 @@ func (m *mockCmix) GetMaxMessageLength() int {
 	return format.NewMessage(m.numPrimeBytes).ContentsSize()
 }
 
+func (m *mockCmix) SendWithAssembler(recipient *id.ID,
+	assembler cmix.MessageAssembler, _ cmix.CMIXParams) (
+	rounds.Round, ephemeral.Id, error) {
+
+	fingerprint, service, payload, mac, err := assembler(42)
+	if err != nil {
+		panic(err)
+	}
+
+	msg := format.NewMessage(m.numPrimeBytes)
+	msg.SetContents(payload)
+	msg.SetMac(mac)
+	msg.SetKeyFP(fingerprint)
+
+	m.handler.Lock()
+	defer m.handler.Unlock()
+
+	for _, p := range m.handler.processorMap[*recipient][service.Tag] {
+		p.Process(msg, receptionID.EphemeralIdentity{}, rounds.Round{})
+	}
+
+	return rounds.Round{}, ephemeral.Id{}, nil
+}
+
 func (m *mockCmix) Send(recipient *id.ID, fingerprint format.Fingerprint,
 	service message.Service, payload, mac []byte, _ cmix.CMIXParams) (
 	id.Round, ephemeral.Id, error) {
diff --git a/channels/adminListener.go b/channels/adminListener.go
new file mode 100644
index 0000000000000000000000000000000000000000..86f80142ef5345a3aad346907d96940097a281a6
--- /dev/null
+++ b/channels/adminListener.go
@@ -0,0 +1,71 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"github.com/golang/protobuf/proto"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/cmix/identity/receptionID"
+	"gitlab.com/elixxir/client/cmix/rounds"
+	"gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/primitives/states"
+	"gitlab.com/xx_network/primitives/id"
+	"time"
+)
+
+// adminListener adheres to the [broadcast.ListenerFunc] interface and is used
+// when admin messages are received on the channel.
+type adminListener struct {
+	chID      *id.ID
+	trigger   triggerAdminEventFunc
+	checkSent messageReceiveFunc
+}
+
+// Listen is called when a message is received for the admin listener
+func (al *adminListener) Listen(payload []byte,
+	receptionID receptionID.EphemeralIdentity, round rounds.Round) {
+	// Get the message ID
+	msgID := channel.MakeMessageID(payload, al.chID)
+
+	// Decode the message as a channel message
+	cm := &ChannelMessage{}
+	if err := proto.Unmarshal(payload, cm); err != nil {
+		jww.WARN.Printf("Failed to unmarshal Channel Message from Admin on "+
+			"channel %s", al.chID)
+		return
+	}
+
+	//check if we sent the message, ignore triggering if we sent
+	if al.checkSent(msgID, round) {
+		return
+	}
+
+	/* CRYPTOGRAPHICALLY RELEVANT CHECKS */
+
+	// Check the round to ensure that the message is not a replay
+	if id.Round(cm.RoundID) != round.ID {
+		jww.WARN.Printf("The round message %s send on %s referenced "+
+			"(%d) was not the same as the round the message was found on (%d)",
+			msgID, al.chID, cm.RoundID, round.ID)
+		return
+	}
+
+	// Replace the timestamp on the message if it is outside of the
+	// allowable range
+	ts := vetTimestamp(time.Unix(0, cm.LocalTimestamp),
+		round.Timestamps[states.QUEUED], msgID)
+
+	// Submit the message to the event model for listening
+	if uuid, err := al.trigger(al.chID, cm, ts, msgID, receptionID,
+		round, Delivered); err != nil {
+		jww.WARN.Printf("Error in passing off trigger for admin "+
+			"message (UUID: %d): %+v", uuid, err)
+	}
+
+	return
+}
diff --git a/channels/adminListener_test.go b/channels/adminListener_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..cb571d5724dcd7318d7ab4dec78bd2c0fe14866c
--- /dev/null
+++ b/channels/adminListener_test.go
@@ -0,0 +1,231 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"bytes"
+	"gitlab.com/xx_network/primitives/netTime"
+	"testing"
+	"time"
+
+	"github.com/golang/protobuf/proto"
+
+	"gitlab.com/elixxir/client/cmix/identity/receptionID"
+	"gitlab.com/elixxir/client/cmix/rounds"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/primitives/states"
+	"gitlab.com/xx_network/primitives/id"
+)
+
+type triggerAdminEventDummy struct {
+	gotData bool
+
+	chID        *id.ID
+	cm          *ChannelMessage
+	msgID       cryptoChannel.MessageID
+	receptionID receptionID.EphemeralIdentity
+	round       rounds.Round
+}
+
+func (taed *triggerAdminEventDummy) triggerAdminEvent(chID *id.ID,
+	cm *ChannelMessage, ts time.Time, messageID cryptoChannel.MessageID,
+	receptionID receptionID.EphemeralIdentity, round rounds.Round,
+	status SentStatus) (uint64, error) {
+	taed.gotData = true
+
+	taed.chID = chID
+	taed.cm = cm
+	taed.msgID = messageID
+	taed.receptionID = receptionID
+	taed.round = round
+
+	return 0, nil
+}
+
+// Tests the happy path.
+func TestAdminListener_Listen(t *testing.T) {
+
+	// Build inputs
+	chID := &id.ID{}
+	chID[0] = 1
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	cm := &ChannelMessage{
+		Lease:       int64(time.Hour),
+		RoundID:     uint64(r.ID),
+		PayloadType: 42,
+		Payload:     []byte("blarg"),
+	}
+
+	cmSerial, err := proto.Marshal(cm)
+	if err != nil {
+		t.Fatalf("Failed to marshal proto: %+v", err)
+	}
+
+	msgID := cryptoChannel.MakeMessageID(cmSerial, chID)
+
+	// Build the listener
+	dummy := &triggerAdminEventDummy{}
+
+	al := adminListener{
+		chID:      chID,
+		trigger:   dummy.triggerAdminEvent,
+		checkSent: func(messageID cryptoChannel.MessageID, r rounds.Round) bool { return false },
+	}
+
+	// Call the listener
+	al.Listen(cmSerial, receptionID.EphemeralIdentity{}, r)
+
+	// Check the results
+	if !dummy.gotData {
+		t.Fatalf("No data returned after valid listen")
+	}
+
+	if !dummy.chID.Cmp(chID) {
+		t.Errorf("Channel ID not correct: %s vs %s", dummy.chID, chID)
+	}
+
+	if !bytes.Equal(cm.Payload, dummy.cm.Payload) {
+		t.Errorf("payload not correct: %s vs %s", cm.Payload,
+			dummy.cm.Payload)
+	}
+
+	if !msgID.Equals(dummy.msgID) {
+		t.Errorf("messageIDs not correct: %s vs %s", msgID,
+			dummy.msgID)
+	}
+
+	if r.ID != dummy.round.ID {
+		t.Errorf("rounds not correct: %s vs %s", r.ID,
+			dummy.round.ID)
+	}
+}
+
+// Tests that the message is rejected when the round it came on doesn't match
+// the round in the channel message.
+func TestAdminListener_Listen_BadRound(t *testing.T) {
+
+	// build inputs
+	chID := &id.ID{}
+	chID[0] = 1
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	cm := &ChannelMessage{
+		Lease: int64(time.Hour),
+		// Different from the round above
+		RoundID:     69,
+		PayloadType: 42,
+		Payload:     []byte("blarg"),
+	}
+
+	cmSerial, err := proto.Marshal(cm)
+	if err != nil {
+		t.Fatalf("Failed to marshal proto: %+v", err)
+	}
+
+	// Build the listener
+	dummy := &triggerAdminEventDummy{}
+
+	al := adminListener{
+		chID:      chID,
+		trigger:   dummy.triggerAdminEvent,
+		checkSent: func(messageID cryptoChannel.MessageID, r rounds.Round) bool { return false },
+	}
+
+	// Call the listener
+	al.Listen(cmSerial, receptionID.EphemeralIdentity{}, r)
+
+	// check the results
+	if dummy.gotData {
+		t.Fatalf("payload handled when it should have failed due to " +
+			"a round issue")
+	}
+
+}
+
+// Tests that the message is rejected when the channel message is malformed.
+func TestAdminListener_Listen_BadChannelMessage(t *testing.T) {
+
+	// Build inputs
+	chID := &id.ID{}
+	chID[0] = 1
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	cmSerial := []byte("blarg")
+
+	// Build the listener
+	dummy := &triggerAdminEventDummy{}
+
+	al := adminListener{
+		chID:      chID,
+		trigger:   dummy.triggerAdminEvent,
+		checkSent: func(messageID cryptoChannel.MessageID, r rounds.Round) bool { return false },
+	}
+
+	// Call the listener
+	al.Listen(cmSerial, receptionID.EphemeralIdentity{}, r)
+
+	// Check the results
+	if dummy.gotData {
+		t.Fatalf("payload handled when it should have failed due to " +
+			"a malformed channel message")
+	}
+
+}
+
+// Tests that the message is rejected when the sized broadcast message is
+// malformed.
+func TestAdminListener_Listen_BadSizedBroadcast(t *testing.T) {
+
+	// build inputs
+	chID := &id.ID{}
+	chID[0] = 1
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	cm := &ChannelMessage{
+		Lease: int64(time.Hour),
+		// Different from the round above
+		RoundID:     69,
+		PayloadType: 42,
+		Payload:     []byte("blarg"),
+	}
+
+	cmSerial, err := proto.Marshal(cm)
+	if err != nil {
+		t.Fatalf("Failed to marshal proto: %+v", err)
+	}
+
+	// Remove half the sized broadcast to make it malformed
+	chMsgSerialSized := cmSerial[:len(cmSerial)/2]
+
+	// Build the listener
+	dummy := &triggerAdminEventDummy{}
+
+	al := adminListener{
+		chID:      chID,
+		trigger:   dummy.triggerAdminEvent,
+		checkSent: func(messageID cryptoChannel.MessageID, r rounds.Round) bool { return false },
+	}
+
+	// Call the listener
+	al.Listen(chMsgSerialSized, receptionID.EphemeralIdentity{}, r)
+
+	// Check the results
+	if dummy.gotData {
+		t.Fatalf("payload handled when it should have failed due to " +
+			"a malformed sized broadcast")
+	}
+}
diff --git a/channels/channelMessages.pb.go b/channels/channelMessages.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..1a7a414fe860fcfbf9439ed5422a3e550c5a55a0
--- /dev/null
+++ b/channels/channelMessages.pb.go
@@ -0,0 +1,320 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// 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.27.1
+// 	protoc        v3.15.6
+// source: channelMessages.proto
+
+package channels
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+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)
+)
+
+// ChannelMessage is transmitted by the channel. Effectively it is a command for
+// the channel sent by a user with admin access of the channel.
+type ChannelMessage struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Lease is the length that this channel message will take effect.
+	Lease int64 `protobuf:"varint,1,opt,name=Lease,proto3" json:"Lease,omitempty"`
+	// The round this message was sent on.
+	RoundID uint64 `protobuf:"varint,2,opt,name=RoundID,proto3" json:"RoundID,omitempty"`
+	// The type the below payload is. This may be some form of channel command,
+	// such as BAN<username1>.
+	PayloadType uint32 `protobuf:"varint,3,opt,name=PayloadType,proto3" json:"PayloadType,omitempty"`
+	// Payload is the actual message payload. It will be processed differently
+	// based on the PayloadType.
+	Payload []byte `protobuf:"bytes,4,opt,name=Payload,proto3" json:"Payload,omitempty"`
+	// nickname is the name which the user is using for this message
+	// it will not be longer than 24 characters
+	Nickname string `protobuf:"bytes,5,opt,name=Nickname,proto3" json:"Nickname,omitempty"`
+	// Nonce is 32 bits of randomness to ensure that two messages in the same
+	// round with that have the same nickname, payload, and lease will not have
+	// the same message ID.
+	Nonce []byte `protobuf:"bytes,6,opt,name=Nonce,proto3" json:"Nonce,omitempty"`
+	// LocalTimestamp is the timestamp when the "send call" is made based upon the
+	// local clock. If this differs by more than 5 seconds +/- from when the round
+	// it sent on is queued, then a random mutation on the queued time (+/- 200ms)
+	// will be used by local clients instead
+	LocalTimestamp int64 `protobuf:"varint,7,opt,name=LocalTimestamp,proto3" json:"LocalTimestamp,omitempty"`
+}
+
+func (x *ChannelMessage) Reset() {
+	*x = ChannelMessage{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_channelMessages_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ChannelMessage) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ChannelMessage) ProtoMessage() {}
+
+func (x *ChannelMessage) ProtoReflect() protoreflect.Message {
+	mi := &file_channelMessages_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)
+}
+
+// Deprecated: Use ChannelMessage.ProtoReflect.Descriptor instead.
+func (*ChannelMessage) Descriptor() ([]byte, []int) {
+	return file_channelMessages_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *ChannelMessage) GetLease() int64 {
+	if x != nil {
+		return x.Lease
+	}
+	return 0
+}
+
+func (x *ChannelMessage) GetRoundID() uint64 {
+	if x != nil {
+		return x.RoundID
+	}
+	return 0
+}
+
+func (x *ChannelMessage) GetPayloadType() uint32 {
+	if x != nil {
+		return x.PayloadType
+	}
+	return 0
+}
+
+func (x *ChannelMessage) GetPayload() []byte {
+	if x != nil {
+		return x.Payload
+	}
+	return nil
+}
+
+func (x *ChannelMessage) GetNickname() string {
+	if x != nil {
+		return x.Nickname
+	}
+	return ""
+}
+
+func (x *ChannelMessage) GetNonce() []byte {
+	if x != nil {
+		return x.Nonce
+	}
+	return nil
+}
+
+func (x *ChannelMessage) GetLocalTimestamp() int64 {
+	if x != nil {
+		return x.LocalTimestamp
+	}
+	return 0
+}
+
+// UserMessage is a message sent by a user who is a member within the channel.
+type UserMessage struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Message contains the contents of the message. This is typically what the
+	// end-user has submitted to the channel. This is a serialization of the
+	// ChannelMessage.
+	Message []byte `protobuf:"bytes,1,opt,name=Message,proto3" json:"Message,omitempty"`
+	// Signature is the signature proving this message has been sent by the
+	// owner of this user's public key.
+	//
+	//  Signature = Sig(User_ECCPublicKey, Message)
+	Signature []byte `protobuf:"bytes,3,opt,name=Signature,proto3" json:"Signature,omitempty"`
+	// ECCPublicKey is the user's EC Public key. This is provided by the
+	// network.
+	ECCPublicKey []byte `protobuf:"bytes,5,opt,name=ECCPublicKey,proto3" json:"ECCPublicKey,omitempty"`
+}
+
+func (x *UserMessage) Reset() {
+	*x = UserMessage{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_channelMessages_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *UserMessage) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*UserMessage) ProtoMessage() {}
+
+func (x *UserMessage) ProtoReflect() protoreflect.Message {
+	mi := &file_channelMessages_proto_msgTypes[1]
+	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)
+}
+
+// Deprecated: Use UserMessage.ProtoReflect.Descriptor instead.
+func (*UserMessage) Descriptor() ([]byte, []int) {
+	return file_channelMessages_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *UserMessage) GetMessage() []byte {
+	if x != nil {
+		return x.Message
+	}
+	return nil
+}
+
+func (x *UserMessage) GetSignature() []byte {
+	if x != nil {
+		return x.Signature
+	}
+	return nil
+}
+
+func (x *UserMessage) GetECCPublicKey() []byte {
+	if x != nil {
+		return x.ECCPublicKey
+	}
+	return nil
+}
+
+var File_channelMessages_proto protoreflect.FileDescriptor
+
+var file_channelMessages_proto_rawDesc = []byte{
+	0x0a, 0x15, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
+	0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c,
+	0x73, 0x22, 0xd6, 0x01, 0x0a, 0x0e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x4d, 0x65, 0x73,
+	0x73, 0x61, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20,
+	0x01, 0x28, 0x03, 0x52, 0x05, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x6f,
+	0x75, 0x6e, 0x64, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x52, 0x6f, 0x75,
+	0x6e, 0x64, 0x49, 0x44, 0x12, 0x20, 0x0a, 0x0b, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x54,
+	0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x50, 0x61, 0x79, 0x6c, 0x6f,
+	0x61, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61,
+	0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
+	0x12, 0x1a, 0x0a, 0x08, 0x4e, 0x69, 0x63, 0x6b, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x08, 0x4e, 0x69, 0x63, 0x6b, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05,
+	0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x4e, 0x6f, 0x6e,
+	0x63, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x73,
+	0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x4c, 0x6f, 0x63, 0x61,
+	0x6c, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x69, 0x0a, 0x0b, 0x55, 0x73,
+	0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x65, 0x73,
+	0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x4d, 0x65, 0x73, 0x73,
+	0x61, 0x67, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65,
+	0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72,
+	0x65, 0x12, 0x22, 0x0a, 0x0c, 0x45, 0x43, 0x43, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65,
+	0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x45, 0x43, 0x43, 0x50, 0x75, 0x62, 0x6c,
+	0x69, 0x63, 0x4b, 0x65, 0x79, 0x42, 0x24, 0x5a, 0x22, 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, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x33,
+}
+
+var (
+	file_channelMessages_proto_rawDescOnce sync.Once
+	file_channelMessages_proto_rawDescData = file_channelMessages_proto_rawDesc
+)
+
+func file_channelMessages_proto_rawDescGZIP() []byte {
+	file_channelMessages_proto_rawDescOnce.Do(func() {
+		file_channelMessages_proto_rawDescData = protoimpl.X.CompressGZIP(file_channelMessages_proto_rawDescData)
+	})
+	return file_channelMessages_proto_rawDescData
+}
+
+var file_channelMessages_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_channelMessages_proto_goTypes = []interface{}{
+	(*ChannelMessage)(nil), // 0: channels.ChannelMessage
+	(*UserMessage)(nil),    // 1: channels.UserMessage
+}
+var file_channelMessages_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_channelMessages_proto_init() }
+func file_channelMessages_proto_init() {
+	if File_channelMessages_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_channelMessages_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ChannelMessage); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_channelMessages_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*UserMessage); 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_channelMessages_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   2,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_channelMessages_proto_goTypes,
+		DependencyIndexes: file_channelMessages_proto_depIdxs,
+		MessageInfos:      file_channelMessages_proto_msgTypes,
+	}.Build()
+	File_channelMessages_proto = out.File
+	file_channelMessages_proto_rawDesc = nil
+	file_channelMessages_proto_goTypes = nil
+	file_channelMessages_proto_depIdxs = nil
+}
diff --git a/channels/channelMessages.proto b/channels/channelMessages.proto
new file mode 100644
index 0000000000000000000000000000000000000000..d89337d366a8cc4cfc27948f9e37e7570eddb254
--- /dev/null
+++ b/channels/channelMessages.proto
@@ -0,0 +1,63 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+syntax = "proto3";
+
+option go_package = "gitlab.com/elixxir/client/channels";
+
+package channels;
+
+// ChannelMessage is transmitted by the channel. Effectively it is a command for
+// the channel sent by a user with admin access of the channel.
+message ChannelMessage{
+    // Lease is the length that this channel message will take effect.
+    int64  Lease = 1;
+
+    // The round this message was sent on.
+    uint64 RoundID = 2;
+
+    // The type the below payload is. This may be some form of channel command,
+    // such as BAN<username1>.
+    uint32 PayloadType = 3;
+
+    // Payload is the actual message payload. It will be processed differently
+    // based on the PayloadType.
+    bytes  Payload = 4;
+
+    // nickname is the name which the user is using for this message
+    // it will not be longer than 24 characters
+    string Nickname = 5;
+
+    // Nonce is 32 bits of randomness to ensure that two messages in the same
+    // round with that have the same nickname, payload, and lease will not have
+    // the same message ID.
+    bytes Nonce = 6;
+
+    // LocalTimestamp is the timestamp when the "send call" is made based upon the
+    // local clock. If this differs by more than 5 seconds +/- from when the round
+    // it sent on is queued, then a random mutation on the queued time (+/- 200ms)
+    // will be used by local clients instead
+    int64 LocalTimestamp = 7;
+}
+
+// UserMessage is a message sent by a user who is a member within the channel.
+message UserMessage {
+    // Message contains the contents of the message. This is typically what the
+    // end-user has submitted to the channel. This is a serialization of the
+    // ChannelMessage.
+    bytes  Message = 1;
+
+    // Signature is the signature proving this message has been sent by the
+    // owner of this user's public key.
+    //
+    //  Signature = Sig(User_ECCPublicKey, Message)
+    bytes  Signature = 3;
+
+    // ECCPublicKey is the user's EC Public key. This is provided by the
+    // network.
+    bytes  ECCPublicKey = 5;
+}
\ No newline at end of file
diff --git a/channels/compileProtobuf.sh b/channels/compileProtobuf.sh
new file mode 100644
index 0000000000000000000000000000000000000000..dff697ecd9976fa8330d7c45052f7b3ca8eef382
--- /dev/null
+++ b/channels/compileProtobuf.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+################################################################################
+## Copyright © 2022 xx foundation                                             ##
+##                                                                            ##
+## Use of this source code is governed by a license that can be found in the  ##
+## LICENSE file.                                                              ##
+################################################################################
+
+# This script will compile the Protobuf file to a Go file (pb.go).
+# This is meant to be called from the top level of the repo.
+
+cd ./channels/ || return
+
+protoc --go_out=. --go_opt=paths=source_relative ./channelMessages.proto
+protoc --go_out=. --go_opt=paths=source_relative ./text.proto
diff --git a/channels/dummyNameServer.go b/channels/dummyNameServer.go
new file mode 100644
index 0000000000000000000000000000000000000000..8c14e40279d8091bf34f29a5a881246715c27c34
--- /dev/null
+++ b/channels/dummyNameServer.go
@@ -0,0 +1,109 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"crypto/ed25519"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/xx_network/primitives/netTime"
+	"io"
+	"time"
+)
+
+// NewDummyNameService returns a dummy object adhering to the name service
+// This neither produces valid signatures nor validates passed signatures.
+//
+// THIS IS FOR DEVELOPMENT AND DEBUGGING PURPOSES ONLY.
+func NewDummyNameService(username string, rng io.Reader) (NameService, error) {
+	jww.WARN.Printf("Creating a Dummy Name Service. This is for " +
+		"development and debugging only. It does not produce valid " +
+		"signatures or verify passed signatures. YOU SHOULD NEVER SEE THIS " +
+		"MESSAGE IN PRODUCTION")
+
+	dns := &dummyNameService{
+		username: username,
+		lease:    netTime.Now().Add(35 * 24 * time.Hour),
+	}
+
+	//generate the private key
+	var err error
+	dns.public, dns.private, err = ed25519.GenerateKey(rng)
+	if err != nil {
+		return nil, err
+	}
+
+	//generate a dummy user discover identity to produce a validation signature
+	//just sign with our own key, it wont be evaluated anyhow
+	dns.validationSig = channel.SignChannelLease(dns.public, dns.username,
+		dns.lease, dns.private)
+
+	return dns, nil
+}
+
+// dummyNameService is a dummy NameService implementation. This is NOT meant
+// for use in production
+type dummyNameService struct {
+	private       ed25519.PrivateKey
+	public        ed25519.PublicKey
+	username      string
+	validationSig []byte
+	lease         time.Time
+}
+
+// GetUsername returns the username for the dummyNameService. This is what was
+// passed in through NewDummyNameService.
+//
+// THIS IS FOR DEVELOPMENT AND DEBUGGING PURPOSES ONLY.
+func (dns *dummyNameService) GetUsername() string {
+	return dns.username
+}
+
+// GetChannelValidationSignature will return the dummy validation signature
+// generated in through the constructor, NewDummyNameService.
+//
+// THIS IS FOR DEVELOPMENT AND DEBUGGING PURPOSES ONLY.
+func (dns *dummyNameService) GetChannelValidationSignature() ([]byte, time.Time) {
+	jww.WARN.Printf("GetChannelValidationSignature called on Dummy Name " +
+		"Service, dummy signature from a random key returned - identity not " +
+		"proven. YOU SHOULD NEVER SEE THIS MESSAGE IN PRODUCTION")
+	return dns.validationSig, dns.lease
+}
+
+// GetChannelPubkey returns the ed25519.PublicKey generates in the constructor,
+// NewDummyNameService.
+func (dns *dummyNameService) GetChannelPubkey() ed25519.PublicKey {
+	return dns.public
+}
+
+// SignChannelMessage will sign the passed in message using the
+// dummyNameService's private key.
+//
+// THIS IS FOR DEVELOPMENT AND DEBUGGING PURPOSES ONLY.
+func (dns *dummyNameService) SignChannelMessage(message []byte) (
+	signature []byte, err error) {
+	jww.WARN.Printf("SignChannelMessage called on Dummy Name Service, " +
+		"signature from a random key - identity not proven. YOU SHOULD " +
+		"NEVER SEE THIS MESSAGE IN PRODUCTION")
+	sig := ed25519.Sign(dns.private, message)
+	return sig, nil
+}
+
+// ValidateChannelMessage will always return true, indicating the the channel
+// message is valid. This will ignore the passed in arguments. As a result,
+// these values may be dummy or precanned.
+//
+// THIS IS FOR DEVELOPMENT AND DEBUGGING PURPOSES ONLY.
+func (dns *dummyNameService) ValidateChannelMessage(username string, lease time.Time,
+	pubKey ed25519.PublicKey, authorIDSignature []byte) bool {
+	//ignore the authorIDSignature
+	jww.WARN.Printf("ValidateChannelMessage called on Dummy Name Service, " +
+		"no validation done - identity not validated. YOU SHOULD NEVER SEE " +
+		"THIS MESSAGE IN PRODUCTION")
+	return true
+}
diff --git a/channels/dummyNameServer_test.go b/channels/dummyNameServer_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..183da2abaa10cbd811048023c6447f61f3261ae4
--- /dev/null
+++ b/channels/dummyNameServer_test.go
@@ -0,0 +1,126 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"crypto/ed25519"
+	"gitlab.com/xx_network/crypto/csprng"
+	"gitlab.com/xx_network/primitives/netTime"
+	"testing"
+)
+
+const numTests = 10
+
+// Smoke test.
+func TestNewDummyNameService(t *testing.T) {
+	rng := csprng.NewSystemRNG()
+	username := "floridaMan"
+	_, err := NewDummyNameService(username, rng)
+	if err != nil {
+		t.Fatalf("NewDummyNameService error: %+v", err)
+	}
+
+}
+
+// Smoke test.
+func TestDummyNameService_GetUsername(t *testing.T) {
+	rng := csprng.NewSystemRNG()
+	username := "floridaMan"
+	ns, err := NewDummyNameService(username, rng)
+	if err != nil {
+		t.Fatalf("NewDummyNameService error: %+v", err)
+	}
+
+	if username != ns.GetUsername() {
+		t.Fatalf("GetUsername did not return expected value."+
+			"\nExpected: %s"+
+			"\nReceived: %s", username, ns.GetUsername())
+	}
+
+}
+
+// Smoke test.
+func TestDummyNameService_SignChannelMessage(t *testing.T) {
+	rng := csprng.NewSystemRNG()
+	username := "floridaMan"
+	ns, err := NewDummyNameService(username, rng)
+	if err != nil {
+		t.Fatalf("NewDummyNameService error: %+v", err)
+	}
+
+	message := []byte("the secret is in the sauce.")
+
+	signature, err := ns.SignChannelMessage(message)
+	if err != nil {
+		t.Fatalf("SignChannelMessage error: %v", err)
+	}
+
+	if len(signature) != ed25519.SignatureSize {
+		t.Errorf("DummyNameService's SignChannelMessage did not return a "+
+			"signature of expected size, according to ed25519 specifications."+
+			"\nExpected: %d"+
+			"\nReceived: %d", ed25519.SignatureSize, len(signature))
+	}
+
+}
+
+// Smoke test.
+func TestDummyNameService_GetChannelValidationSignature(t *testing.T) {
+	rng := csprng.NewSystemRNG()
+	username := "floridaMan"
+	ns, err := NewDummyNameService(username, rng)
+	if err != nil {
+		t.Fatalf("NewDummyNameService error: %+v", err)
+	}
+
+	validationSig, _ := ns.GetChannelValidationSignature()
+
+	if len(validationSig) != ed25519.SignatureSize {
+		t.Errorf("DummyNameService's GetChannelValidationSignature did not "+
+			"return a validation signature of expected size, according to "+
+			"ed25519 specifications."+
+			"\nExpected: %d"+
+			"\nReceived: %d", ed25519.SignatureSize, len(validationSig))
+	}
+
+}
+
+// Smoke test.
+func TestDummyNameService_ValidateChannelMessage(t *testing.T) {
+	rng := csprng.NewSystemRNG()
+	username := "floridaMan"
+	ns, err := NewDummyNameService(username, rng)
+	if err != nil {
+		t.Fatalf("NewDummyNameService error: %+v", err)
+	}
+
+	for i := 0; i < numTests; i++ {
+		if !ns.ValidateChannelMessage(username, netTime.Now(), nil, nil) {
+			t.Errorf("ValidateChannelMessage returned false. This should " +
+				"only ever return true.")
+		}
+	}
+}
+
+// Smoke test.
+func TestDummyNameService_GetChannelPubkey(t *testing.T) {
+	rng := csprng.NewSystemRNG()
+	username := "floridaMan"
+	ns, err := NewDummyNameService(username, rng)
+	if err != nil {
+		t.Fatalf("NewDummyNameService error: %+v", err)
+	}
+
+	if len(ns.GetChannelPubkey()) != ed25519.PublicKeySize {
+		t.Errorf("DummyNameService's GetChannelPubkey did not "+
+			"return a validation signature of expected size, according to "+
+			"ed25519 specifications."+
+			"\nExpected: %d"+
+			"\nReceived: %d", ed25519.PublicKeySize, ns.GetChannelPubkey())
+	}
+}
diff --git a/channels/emoji.go b/channels/emoji.go
new file mode 100644
index 0000000000000000000000000000000000000000..71c7ae4f66dd6b96e812521056f44a548fbf74c6
--- /dev/null
+++ b/channels/emoji.go
@@ -0,0 +1,37 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"bytes"
+	"regexp"
+)
+
+//based on emojis found at https://unicode.org/emoji/charts/full-emoji-list.html
+const findEmoji = `[\xA9\xAE\x{2000}-\x{3300}\x{1F000}-\x{1FBFF}]`
+
+var compiledFindEmoji = regexp.MustCompile(findEmoji)
+
+// ValidateReaction checks that the reaction only contains a single emoji.
+func ValidateReaction(reaction string) error {
+
+	//make sure it is only only character
+	reactRunes := []rune(reaction)
+	if len(reactRunes) > 1 {
+		return InvalidReaction
+	}
+
+	reader := bytes.NewReader([]byte(reaction))
+
+	// make sure it has emojis
+	if !compiledFindEmoji.MatchReader(reader) {
+		return InvalidReaction
+	}
+
+	return nil
+}
diff --git a/channels/emoji_test.go b/channels/emoji_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..d320b05a6b0a404f23d3412aca8fb6a1fea393ef
--- /dev/null
+++ b/channels/emoji_test.go
@@ -0,0 +1,35 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"testing"
+)
+
+func TestValidateReaction(t *testing.T) {
+
+	testReactions := []string{"🍆", "😂", "❤", "🤣", "👍", "😭", "🙏", "😘", "🥰",
+		"😍", "😊", "☺", "A", "b", "AA", "1", "🍆🍆", "🍆A", "👍👍👍", "👍😘A",
+		"O", "\u0000", "\u0011", "\u001F", "\u007F", "\u0080", "\u008A",
+		"\u009F"}
+
+	expected := []error{
+		nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
+		InvalidReaction, InvalidReaction, InvalidReaction, InvalidReaction,
+		InvalidReaction, InvalidReaction, InvalidReaction, InvalidReaction,
+		InvalidReaction, InvalidReaction, InvalidReaction, InvalidReaction,
+		InvalidReaction, InvalidReaction, InvalidReaction, InvalidReaction}
+
+	for i, r := range testReactions {
+		err := ValidateReaction(r)
+		if err != expected[i] {
+			t.Errorf("Got incorrect response for `%s` (%d): "+
+				"`%s` vs `%s`", r, i, err, expected[i])
+		}
+	}
+}
diff --git a/channels/errors.go b/channels/errors.go
new file mode 100644
index 0000000000000000000000000000000000000000..319f065966a899f5a64c05b41eda84caa887fd02
--- /dev/null
+++ b/channels/errors.go
@@ -0,0 +1,25 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import "github.com/pkg/errors"
+
+var (
+	ChannelAlreadyExistsErr = errors.New(
+		"the channel cannot be added because it already exists")
+	ChannelDoesNotExistsErr = errors.New(
+		"the channel cannot be found")
+	MessageTooLongErr = errors.New(
+		"the passed message is too long")
+	WrongPrivateKey = errors.New(
+		"the passed private key does not match the channel")
+	MessageTypeAlreadyRegistered = errors.New("the given message type has " +
+		"already been registered")
+	InvalidReaction = errors.New(
+		"The reaction is not valid, it must be a single emoji")
+)
diff --git a/channels/eventModel.go b/channels/eventModel.go
new file mode 100644
index 0000000000000000000000000000000000000000..153a6bd68df07144219fc249332c5c3f0f316db2
--- /dev/null
+++ b/channels/eventModel.go
@@ -0,0 +1,381 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"crypto/ed25519"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"github.com/golang/protobuf/proto"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/cmix/identity/receptionID"
+	"sync"
+	"time"
+
+	"gitlab.com/elixxir/client/cmix/rounds"
+	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/xx_network/primitives/id"
+)
+
+// AdminUsername defines the displayed username of admin messages, which are
+// unique users for every channel defined by the channel's private key.
+const AdminUsername = "Admin"
+
+type SentStatus uint8
+
+const (
+	Unsent SentStatus = iota
+	Sent
+	Delivered
+	Failed
+)
+
+var AdminFakePubKey = ed25519.PublicKey{}
+
+// EventModel is an interface which an external party which uses the channels
+// system passed an object which adheres to in order to get events on the
+// channel.
+type EventModel interface {
+	// JoinChannel is called whenever a channel is joined locally.
+	JoinChannel(channel *cryptoBroadcast.Channel)
+
+	// LeaveChannel is called whenever a channel is left locally.
+	LeaveChannel(channelID *id.ID)
+
+	// ReceiveMessage is called whenever a message is received on a given
+	// channel. It may be called multiple times on the same message. It is
+	// incumbent on the user of the API to filter such called by message ID.
+	//
+	// The API needs to return a UUID of the message that can be referenced at a
+	// later time.
+	//
+	// messageID, timestamp, and round are all nillable and may be updated based
+	// upon the UUID at a later date. A time of time.Time{} will be passed for a
+	// nilled timestamp.
+	//
+	// Nickname may be empty, in which case the UI is expected to display the
+	// codename.
+	//
+	// Message type is included in the call; it will always be Text (1) for this
+	// call, but it may be required in downstream databases.
+	ReceiveMessage(channelID *id.ID, messageID cryptoChannel.MessageID,
+		nickname, text string, pubKey ed25519.PublicKey, codeset uint8,
+		timestamp time.Time, lease time.Duration, round rounds.Round,
+		mType MessageType, status SentStatus) uint64
+
+	// ReceiveReply is called whenever a message is received that is a reply on
+	// a given channel. It may be called multiple times on the same message. It
+	// is incumbent on the user of the API to filter such called by message ID.
+	//
+	// Messages may arrive our of order, so a reply, in theory, can arrive
+	// before the initial message. As a result, it may be important to buffer
+	// replies.
+	//
+	// The API needs to return a UUID of the message that can be referenced at a
+	// later time.
+	//
+	// messageID, timestamp, and round are all nillable and may be updated based
+	// upon the UUID at a later date. A time of time.Time{} will be passed for a
+	// nilled timestamp.
+	//
+	// Nickname may be empty, in which case the UI is expected to display the
+	// codename.
+	//
+	// Message type is included in the call; it will always be Text (1) for this
+	// call, but it may be required in downstream databases.
+	ReceiveReply(channelID *id.ID, messageID cryptoChannel.MessageID,
+		reactionTo cryptoChannel.MessageID, nickname, text string,
+		pubKey ed25519.PublicKey, codeset uint8, timestamp time.Time,
+		lease time.Duration, round rounds.Round, mType MessageType,
+		status SentStatus) uint64
+
+	// ReceiveReaction is called whenever a reaction to a message is received on
+	// a given channel. It may be called multiple times on the same reaction. It
+	// is incumbent on the user of the API to filter such called by message ID.
+	//
+	// Messages may arrive our of order, so a reply, in theory, can arrive
+	// before the initial message. As a result, it may be important to buffer
+	// replies.
+	//
+	// The API needs to return a UUID of the message that can be referenced at a
+	// later time.
+	//
+	// messageID, timestamp, and round are all nillable and may be updated based
+	// upon the UUID at a later date. A time of time.Time{} will be passed for a
+	// nilled timestamp.
+	//
+	// Nickname may be empty, in which case the UI is expected to display the
+	// codename.
+	//
+	// Message type is included in the call; it will always be Text (1) for this
+	// call, but it may be required in downstream databases.
+	ReceiveReaction(channelID *id.ID, messageID cryptoChannel.MessageID,
+		reactionTo cryptoChannel.MessageID, nickname, reaction string,
+		pubKey ed25519.PublicKey, codeset uint8, timestamp time.Time,
+		lease time.Duration, round rounds.Round, mType MessageType,
+		status SentStatus) uint64
+
+	// UpdateSentStatus is called whenever the sent status of a message has
+	// changed.
+	//
+	// messageID, timestamp, and round are all nillable and may be updated based
+	// upon the UUID at a later date. A time of time.Time{} will be passed for a
+	// nilled timestamp. If a nil value is passed, make no update.
+	UpdateSentStatus(uuid uint64, messageID cryptoChannel.MessageID,
+		timestamp time.Time, round rounds.Round, status SentStatus)
+
+	// unimplemented
+	// IgnoreMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID)
+	// UnIgnoreMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID)
+	// PinMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID, end time.Time)
+	// UnPinMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID)
+}
+
+// MessageTypeReceiveMessage defines handlers for messages of various message
+// types. Default ones for Text, Reaction, and AdminText.
+//
+// A unique UUID must be returned by which the message can be referenced later
+// via [EventModel.UpdateSentStatus].
+//
+// It must return a unique UUID for the message by which it can be referenced
+// later.
+type MessageTypeReceiveMessage func(channelID *id.ID,
+	messageID cryptoChannel.MessageID, messageType MessageType,
+	nickname string, content []byte, pubKey ed25519.PublicKey, codeset uint8,
+	timestamp time.Time, lease time.Duration, round rounds.Round,
+	status SentStatus) uint64
+
+// updateStatusFunc is a function type for EventModel.UpdateSentStatus so it can
+// be mocked for testing where used.
+type updateStatusFunc func(uuid uint64, messageID cryptoChannel.MessageID,
+	timestamp time.Time, round rounds.Round, status SentStatus)
+
+// events is an internal structure that processes events and stores the handlers
+// for those events.
+type events struct {
+	model      EventModel
+	registered map[MessageType]MessageTypeReceiveMessage
+	mux        sync.RWMutex
+}
+
+// initEvents initializes the event model and registers default message type
+// handlers.
+func initEvents(model EventModel) *events {
+	e := &events{
+		model:      model,
+		registered: make(map[MessageType]MessageTypeReceiveMessage),
+		mux:        sync.RWMutex{},
+	}
+
+	// set up default message types
+	e.registered[Text] = e.receiveTextMessage
+	e.registered[AdminText] = e.receiveTextMessage
+	e.registered[Reaction] = e.receiveReaction
+	return e
+}
+
+// RegisterReceiveHandler is used to register handlers for non default message
+// types s they can be processed by modules. It is important that such modules
+// sync up with the event model implementation.
+//
+// There can only be one handler per message type, and this will return an error
+// on a multiple registration.
+func (e *events) RegisterReceiveHandler(messageType MessageType,
+	listener MessageTypeReceiveMessage) error {
+	e.mux.Lock()
+	defer e.mux.Unlock()
+
+	// check if the type is already registered
+	if _, exists := e.registered[messageType]; exists {
+		return MessageTypeAlreadyRegistered
+	}
+
+	// register the message type
+	e.registered[messageType] = listener
+	jww.INFO.Printf("Registered Listener for Message Type %s", messageType)
+	return nil
+}
+
+type triggerEventFunc func(chID *id.ID, umi *userMessageInternal, ts time.Time,
+	receptionID receptionID.EphemeralIdentity, round rounds.Round,
+	status SentStatus) (uint64, error)
+
+// triggerEvent is an internal function that is used to trigger message
+// reception on a message received from a user (symmetric encryption).
+//
+// It will call the appropriate MessageTypeHandler assuming one exists.
+func (e *events) triggerEvent(chID *id.ID, umi *userMessageInternal,
+	ts time.Time, _ receptionID.EphemeralIdentity, round rounds.Round,
+	status SentStatus) (uint64, error) {
+	um := umi.GetUserMessage()
+	cm := umi.GetChannelMessage()
+	messageType := MessageType(cm.PayloadType)
+
+	// Check if the type is already registered
+	e.mux.RLock()
+	listener, exists := e.registered[messageType]
+	e.mux.RUnlock()
+	if !exists {
+		errStr := fmt.Sprintf("Received message from %x on channel %s in "+
+			"round %d which could not be handled due to unregistered message "+
+			"type %s; Contents: %v", um.ECCPublicKey, chID, round.ID, messageType,
+			cm.Payload)
+		jww.WARN.Printf(errStr)
+		return 0, errors.New(errStr)
+	}
+
+	// Call the listener. This is already in an instanced event, no new thread needed.
+	uuid := listener(chID, umi.GetMessageID(), messageType, cm.Nickname, cm.Payload,
+		um.ECCPublicKey, 0, ts, time.Duration(cm.Lease),
+		round, status)
+	return uuid, nil
+}
+
+type triggerAdminEventFunc func(chID *id.ID, cm *ChannelMessage, ts time.Time,
+	messageID cryptoChannel.MessageID, receptionID receptionID.EphemeralIdentity,
+	round rounds.Round, status SentStatus) (uint64, error)
+
+// triggerAdminEvent is an internal function that is used to trigger message
+// reception on a message received from the admin (asymmetric encryption).
+//
+// It will call the appropriate MessageTypeHandler assuming one exists.
+func (e *events) triggerAdminEvent(chID *id.ID, cm *ChannelMessage,
+	ts time.Time, messageID cryptoChannel.MessageID,
+	_ receptionID.EphemeralIdentity, round rounds.Round, status SentStatus) (
+	uint64, error) {
+	messageType := MessageType(cm.PayloadType)
+
+	// check if the type is already registered
+	e.mux.RLock()
+	listener, exists := e.registered[messageType]
+	e.mux.RUnlock()
+	if !exists {
+		errStr := fmt.Sprintf("Received Admin message from %s on channel %s in "+
+			"round %d which could not be handled due to unregistered message "+
+			"type %s; Contents: %v", AdminUsername, chID, round.ID, messageType,
+			cm.Payload)
+		jww.WARN.Printf(errStr)
+		return 0, errors.New(errStr)
+	}
+
+	// Call the listener. This is already in an instanced event, no new thread needed.
+	uuid := listener(chID, messageID, messageType, AdminUsername, cm.Payload,
+		AdminFakePubKey, 0, ts,
+		time.Duration(cm.Lease), round, status)
+	return uuid, nil
+}
+
+// receiveTextMessage is the internal function that handles the reception of
+// text messages. It handles both messages and replies and calls the correct
+// function on the event model.
+//
+// If the message has a reply, but it is malformed, it will drop the reply and
+// write to the log.
+func (e *events) receiveTextMessage(channelID *id.ID,
+	messageID cryptoChannel.MessageID, messageType MessageType,
+	nickname string, content []byte, pubKey ed25519.PublicKey, codeset uint8,
+	timestamp time.Time, lease time.Duration, round rounds.Round,
+	status SentStatus) uint64 {
+	txt := &CMIXChannelText{}
+
+	if err := proto.Unmarshal(content, txt); err != nil {
+		jww.ERROR.Printf("Failed to text unmarshal message %s from %x on "+
+			"channel %s, type %s, ts: %s, lease: %s, round: %d: %+v",
+			messageID, pubKey, channelID, messageType, timestamp, lease,
+			round.ID, err)
+		return 0
+	}
+
+	if txt.ReplyMessageID != nil {
+
+		if len(txt.ReplyMessageID) == cryptoChannel.MessageIDLen {
+			var replyTo cryptoChannel.MessageID
+			copy(replyTo[:], txt.ReplyMessageID)
+			tag := makeChaDebugTag(channelID, pubKey, content, SendReplyTag)
+			jww.INFO.Printf("[%s]Channels - Received reply from %s "+
+				"to %s on %s", tag, base64.StdEncoding.EncodeToString(pubKey),
+				base64.StdEncoding.EncodeToString(txt.ReplyMessageID),
+				channelID)
+			return e.model.ReceiveReply(channelID, messageID, replyTo,
+				nickname, txt.Text, pubKey, codeset, timestamp, lease, round, Text, status)
+
+		} else {
+			jww.ERROR.Printf("Failed process reply to for message %s from "+
+				"public key %v (codeset %d) on channel %s, type %s, ts: %s, "+
+				"lease: %s, round: %d, returning without reply",
+				messageID, pubKey, codeset, channelID, messageType, timestamp,
+				lease, round.ID)
+			// Still process the message, but drop the reply because it is
+			// malformed
+		}
+	}
+
+	tag := makeChaDebugTag(channelID, pubKey, content, SendMessageTag)
+	jww.INFO.Printf("[%s]Channels - Received message from %s "+
+		"to %s on %s", tag, base64.StdEncoding.EncodeToString(pubKey),
+		base64.StdEncoding.EncodeToString(txt.ReplyMessageID),
+		channelID)
+
+	return e.model.ReceiveMessage(channelID, messageID, nickname, txt.Text, pubKey, codeset,
+		timestamp, lease, round, Text, status)
+}
+
+// receiveReaction is the internal function that handles the reception of
+// Reactions.
+//
+// It does edge checking to ensure the received reaction is just a single emoji.
+// If the received reaction is not, the reaction is dropped.
+// If the messageID for the message the reaction is to is malformed, the
+// reaction is dropped.
+func (e *events) receiveReaction(channelID *id.ID,
+	messageID cryptoChannel.MessageID, messageType MessageType,
+	nickname string, content []byte, pubKey ed25519.PublicKey, codeset uint8,
+	timestamp time.Time, lease time.Duration, round rounds.Round,
+	status SentStatus) uint64 {
+	react := &CMIXChannelReaction{}
+	if err := proto.Unmarshal(content, react); err != nil {
+		jww.ERROR.Printf("Failed to text unmarshal message %s from %x on "+
+			"channel %s, type %s, ts: %s, lease: %s, round: %d: %+v",
+			messageID, pubKey, channelID, messageType, timestamp, lease,
+			round.ID, err)
+		return 0
+	}
+
+	// check that the reaction is a single emoji and ignore if it isn't
+	if err := ValidateReaction(react.Reaction); err != nil {
+		jww.ERROR.Printf("Failed process reaction %s from %x on channel "+
+			"%s, type %s, ts: %s, lease: %s, round: %d, due to malformed "+
+			"reaction (%s), ignoring reaction",
+			messageID, pubKey, channelID, messageType, timestamp, lease,
+			round.ID, err)
+		return 0
+	}
+
+	if react.ReactionMessageID != nil && len(react.ReactionMessageID) == cryptoChannel.MessageIDLen {
+		var reactTo cryptoChannel.MessageID
+		copy(reactTo[:], react.ReactionMessageID)
+
+		tag := makeChaDebugTag(channelID, pubKey, content, SendReactionTag)
+		jww.INFO.Printf("[%s]Channels - Received reaction from %s "+
+			"to %s on %s", tag, base64.StdEncoding.EncodeToString(pubKey),
+			base64.StdEncoding.EncodeToString(react.ReactionMessageID),
+			channelID)
+
+		return e.model.ReceiveReaction(channelID, messageID, reactTo, nickname,
+			react.Reaction, pubKey, codeset, timestamp, lease, round, Reaction, status)
+	} else {
+		jww.ERROR.Printf("Failed process reaction %s from public key %v "+
+			"(codeset %d) on channel %s, type %s, ts: %s, lease: %s, "+
+			"round: %d, reacting to invalid message, ignoring reaction",
+			messageID, pubKey, codeset, channelID, messageType, timestamp,
+			lease, round.ID)
+	}
+	return 0
+}
diff --git a/channels/eventModel_test.go b/channels/eventModel_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..4edf780c46076867789a298c224bfb6194e073b0
--- /dev/null
+++ b/channels/eventModel_test.go
@@ -0,0 +1,906 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"bytes"
+	"crypto/ed25519"
+	"fmt"
+	"github.com/golang/protobuf/proto"
+	"gitlab.com/elixxir/client/cmix/identity/receptionID"
+	"gitlab.com/elixxir/client/cmix/rounds"
+	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/primitives/states"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/netTime"
+	"math/rand"
+	"reflect"
+	"runtime"
+	"testing"
+	"time"
+)
+
+type eventReceive struct {
+	channelID  *id.ID
+	messageID  cryptoChannel.MessageID
+	reactionTo cryptoChannel.MessageID
+	nickname   string
+	content    []byte
+	timestamp  time.Time
+	lease      time.Duration
+	round      rounds.Round
+}
+
+type MockEvent struct {
+	uuid uint64
+	eventReceive
+}
+
+func (m *MockEvent) getUUID() uint64 {
+	old := m.uuid
+	m.uuid++
+	return old
+}
+
+func (*MockEvent) JoinChannel(*cryptoBroadcast.Channel) {}
+func (*MockEvent) LeaveChannel(*id.ID)                  {}
+func (m *MockEvent) ReceiveMessage(channelID *id.ID,
+	messageID cryptoChannel.MessageID, nickname, text string,
+	_ ed25519.PublicKey, _ uint8, timestamp time.Time, lease time.Duration,
+	round rounds.Round, _ MessageType, _ SentStatus) uint64 {
+	m.eventReceive = eventReceive{
+		channelID:  channelID,
+		messageID:  messageID,
+		reactionTo: cryptoChannel.MessageID{},
+		nickname:   nickname,
+		content:    []byte(text),
+		timestamp:  timestamp,
+		lease:      lease,
+		round:      round,
+	}
+	return m.getUUID()
+}
+func (m *MockEvent) ReceiveReply(channelID *id.ID,
+	messageID cryptoChannel.MessageID, reactionTo cryptoChannel.MessageID,
+	nickname, text string, _ ed25519.PublicKey, _ uint8, timestamp time.Time,
+	lease time.Duration, round rounds.Round, _ MessageType, _ SentStatus) uint64 {
+	fmt.Println(reactionTo)
+	m.eventReceive = eventReceive{
+		channelID:  channelID,
+		messageID:  messageID,
+		reactionTo: reactionTo,
+		nickname:   nickname,
+		content:    []byte(text),
+		timestamp:  timestamp,
+		lease:      lease,
+		round:      round,
+	}
+	return m.getUUID()
+}
+func (m *MockEvent) ReceiveReaction(channelID *id.ID,
+	messageID cryptoChannel.MessageID, reactionTo cryptoChannel.MessageID,
+	nickname, reaction string, _ ed25519.PublicKey, _ uint8, timestamp time.Time,
+	lease time.Duration, round rounds.Round, _ MessageType, _ SentStatus) uint64 {
+	m.eventReceive = eventReceive{
+		channelID:  channelID,
+		messageID:  messageID,
+		reactionTo: reactionTo,
+		nickname:   nickname,
+		content:    []byte(reaction),
+		timestamp:  timestamp,
+		lease:      lease,
+		round:      round,
+	}
+	return m.getUUID()
+}
+
+func (m *MockEvent) UpdateSentStatus(uint64, cryptoChannel.MessageID,
+	time.Time, rounds.Round, SentStatus) {
+	// TODO implement me
+	panic("implement me")
+}
+
+func Test_initEvents(t *testing.T) {
+
+	me := &MockEvent{}
+
+	e := initEvents(me)
+
+	// verify the model is registered
+	if e.model != me {
+		t.Errorf("Event model is not registered")
+	}
+
+	// check registered channels was created
+	if e.registered == nil {
+		t.Fatalf("Registered handlers is not registered")
+	}
+
+	// check that all the default callbacks are registered
+	if len(e.registered) != 3 {
+		t.Errorf("The correct number of default handlers are not "+
+			"registered; %d vs %d", len(e.registered), 3)
+		// If this fails, is means the default handlers have changed. edit the
+		// number here and add tests below. be suspicious if it goes down.
+	}
+
+	if getFuncName(e.registered[Text]) != getFuncName(e.receiveTextMessage) {
+		t.Errorf("Text does not have recieveTextMessageRegistred")
+	}
+
+	if getFuncName(e.registered[AdminText]) != getFuncName(e.receiveTextMessage) {
+		t.Errorf("AdminText does not have recieveTextMessageRegistred")
+	}
+
+	if getFuncName(e.registered[Reaction]) != getFuncName(e.receiveReaction) {
+		t.Errorf("Reaction does not have recieveReaction")
+	}
+}
+
+func TestEvents_RegisterReceiveHandler(t *testing.T) {
+	me := &MockEvent{}
+
+	e := initEvents(me)
+
+	// Test that a new reception handler can be registered.
+	mt := MessageType(42)
+	err := e.RegisterReceiveHandler(mt, e.receiveReaction)
+	if err != nil {
+		t.Fatalf("Failed to register '%s' when it should be "+
+			"sucesfull: %+v", mt, err)
+	}
+
+	// check that it is written
+	returnedHandler, exists := e.registered[mt]
+	if !exists {
+		t.Fatalf("Failed to get handler '%s' after registration", mt)
+	}
+
+	// check that the correct function is written
+	if getFuncName(e.receiveReaction) != getFuncName(returnedHandler) {
+		t.Fatalf("Failed to get correct handler for '%s' after "+
+			"registration, %s vs %s", mt, getFuncName(e.receiveReaction),
+			getFuncName(returnedHandler))
+	}
+
+	// test that writing to the same receive handler fails
+	err = e.RegisterReceiveHandler(mt, e.receiveTextMessage)
+	if err == nil {
+		t.Fatalf("Failed to register '%s' when it should be "+
+			"sucesfull: %+v", mt, err)
+	} else if err != MessageTypeAlreadyRegistered {
+		t.Fatalf("Wrong error returned when reregierting message "+
+			"tyle '%s': %+v", mt, err)
+	}
+
+	// check that it is still written
+	returnedHandler, exists = e.registered[mt]
+	if !exists {
+		t.Fatalf("Failed to get handler '%s' after second "+
+			"registration", mt)
+	}
+
+	// check that the correct function is written
+	if getFuncName(e.receiveReaction) != getFuncName(returnedHandler) {
+		t.Fatalf("Failed to get correct handler for '%s' after "+
+			"second registration, %s vs %s", mt, getFuncName(e.receiveReaction),
+			getFuncName(returnedHandler))
+	}
+}
+
+type dummyMessageTypeHandler struct {
+	triggered   bool
+	channelID   *id.ID
+	messageID   cryptoChannel.MessageID
+	messageType MessageType
+	nickname    string
+	content     []byte
+	timestamp   time.Time
+	lease       time.Duration
+	round       rounds.Round
+}
+
+func (dmth *dummyMessageTypeHandler) dummyMessageTypeReceiveMessage(
+	channelID *id.ID, messageID cryptoChannel.MessageID,
+	messageType MessageType, nickname string, content []byte,
+	_ ed25519.PublicKey, _ uint8, timestamp time.Time, lease time.Duration,
+	round rounds.Round, _ SentStatus) uint64 {
+	dmth.triggered = true
+	dmth.channelID = channelID
+	dmth.messageID = messageID
+	dmth.messageType = messageType
+	dmth.nickname = nickname
+	dmth.content = content
+	dmth.timestamp = timestamp
+	dmth.lease = lease
+	dmth.round = round
+	return rand.Uint64()
+}
+
+func TestEvents_triggerEvents(t *testing.T) {
+	me := &MockEvent{}
+
+	e := initEvents(me)
+
+	dummy := &dummyMessageTypeHandler{}
+
+	// register the handler
+	mt := MessageType(42)
+	err := e.RegisterReceiveHandler(mt, dummy.dummyMessageTypeReceiveMessage)
+	if err != nil {
+		t.Fatalf("Error on registration, should not have happened: "+
+			"%+v", err)
+	}
+
+	// craft the input for the event
+	chID := &id.ID{}
+	chID[0] = 1
+
+	umi, _, _ := builtTestUMI(t, mt)
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	// call the trigger
+	_, err = e.triggerEvent(chID, umi, netTime.Now(), receptionID.EphemeralIdentity{}, r, Delivered)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+	// check that the event was triggered
+	if !dummy.triggered {
+		t.Errorf("The event was not triggered")
+	}
+
+	// check the data is stored in the dummy
+	if !dummy.channelID.Cmp(chID) {
+		t.Errorf("The channel IDs do not match %s vs %s",
+			dummy.channelID, chID)
+	}
+
+	if !dummy.messageID.Equals(umi.GetMessageID()) {
+		t.Errorf("The message IDs do not match %s vs %s",
+			dummy.messageID, umi.GetMessageID())
+	}
+
+	if dummy.messageType != mt {
+		t.Errorf("The message types do not match %s vs %s",
+			dummy.messageType, mt)
+	}
+
+	if dummy.nickname != umi.channelMessage.Nickname {
+		t.Errorf("The usernames do not match %s vs %s",
+			dummy.nickname, umi.channelMessage.Nickname)
+	}
+
+	if !bytes.Equal(dummy.content, umi.GetChannelMessage().Payload) {
+		t.Errorf("The payloads do not match %s vs %s",
+			dummy.content, umi.GetChannelMessage().Payload)
+	}
+
+	if !withinMutationWindow(r.Timestamps[states.QUEUED], dummy.timestamp) {
+		t.Errorf("The timestamps do not match %s vs %s",
+			dummy.timestamp, r.Timestamps[states.QUEUED])
+	}
+
+	if dummy.lease != time.Duration(umi.GetChannelMessage().Lease) {
+		t.Errorf("The messge lease durations do not match %s vs %s",
+			dummy.lease, time.Duration(umi.GetChannelMessage().Lease))
+	}
+
+	if dummy.round.ID != r.ID {
+		t.Errorf("The messge round does not match %s vs %s",
+			dummy.round.ID, r.ID)
+	}
+}
+
+func TestEvents_triggerEvents_noChannel(t *testing.T) {
+	me := &MockEvent{}
+
+	e := initEvents(me)
+
+	dummy := &dummyMessageTypeHandler{}
+
+	// skip handler registration
+	mt := MessageType(1)
+
+	// craft the input for the event
+	chID := &id.ID{}
+	chID[0] = 1
+
+	umi, _, _ := builtTestUMI(t, mt)
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	// call the trigger
+	_, err := e.triggerEvent(chID, umi, netTime.Now(), receptionID.EphemeralIdentity{}, r, Delivered)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	// check that the event was triggered
+	if dummy.triggered {
+		t.Errorf("The event was triggered when it is unregistered")
+	}
+}
+
+func TestEvents_triggerAdminEvents(t *testing.T) {
+	me := &MockEvent{}
+
+	e := initEvents(me)
+
+	dummy := &dummyMessageTypeHandler{}
+
+	// register the handler
+	mt := MessageType(42)
+	err := e.RegisterReceiveHandler(mt, dummy.dummyMessageTypeReceiveMessage)
+	if err != nil {
+		t.Fatalf("Error on registration, should not have happened: "+
+			"%+v", err)
+	}
+
+	// craft the input for the event
+	chID := &id.ID{}
+	chID[0] = 1
+
+	u, _, cm := builtTestUMI(t, mt)
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	msgID := cryptoChannel.MakeMessageID(u.userMessage.Message, chID)
+
+	// call the trigger
+	_, err = e.triggerAdminEvent(chID, cm, netTime.Now(), msgID, receptionID.EphemeralIdentity{}, r,
+		Delivered)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	// check that the event was triggered
+	if !dummy.triggered {
+		t.Errorf("The admin event was not triggered")
+	}
+
+	// check the data is stored in the dummy
+	if !dummy.channelID.Cmp(chID) {
+		t.Errorf("The channel IDs do not match %s vs %s",
+			dummy.channelID, chID)
+	}
+
+	if !dummy.messageID.Equals(msgID) {
+		t.Errorf("The message IDs do not match %s vs %s",
+			dummy.messageID, msgID)
+	}
+
+	if dummy.messageType != mt {
+		t.Errorf("The message types do not match %s vs %s",
+			dummy.messageType, mt)
+	}
+
+	if dummy.nickname != AdminUsername {
+		t.Errorf("The usernames do not match %s vs %s",
+			dummy.nickname, AdminUsername)
+	}
+
+	if !bytes.Equal(dummy.content, cm.Payload) {
+		t.Errorf("The payloads do not match %s vs %s",
+			dummy.content, cm.Payload)
+	}
+
+	if !withinMutationWindow(r.Timestamps[states.QUEUED], dummy.timestamp) {
+		t.Errorf("The timestamps do not match %s vs %s",
+			dummy.timestamp, r.Timestamps[states.QUEUED])
+	}
+
+	if dummy.lease != time.Duration(cm.Lease) {
+		t.Errorf("The messge lease durations do not match %s vs %s",
+			dummy.lease, time.Duration(cm.Lease))
+	}
+
+	if dummy.round.ID != r.ID {
+		t.Errorf("The messge round does not match %s vs %s",
+			dummy.round.ID, r.ID)
+	}
+}
+
+func TestEvents_triggerAdminEvents_noChannel(t *testing.T) {
+	me := &MockEvent{}
+
+	e := initEvents(me)
+
+	dummy := &dummyMessageTypeHandler{}
+
+	mt := MessageType(1)
+	// skip handler registration
+
+	// craft the input for the event
+	chID := &id.ID{}
+	chID[0] = 1
+
+	u, _, cm := builtTestUMI(t, mt)
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	msgID := cryptoChannel.MakeMessageID(u.userMessage.Message, chID)
+
+	// call the trigger
+	_, err := e.triggerAdminEvent(chID, cm, netTime.Now(), msgID, receptionID.EphemeralIdentity{}, r,
+		Delivered)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	// check that the event was triggered
+	if dummy.triggered {
+		t.Errorf("The admin event was triggered when unregistered")
+	}
+}
+
+func TestEvents_receiveTextMessage_Message(t *testing.T) {
+	me := &MockEvent{}
+
+	e := initEvents(me)
+
+	// craft the input for the event
+	chID := &id.ID{}
+	chID[0] = 1
+
+	textPayload := &CMIXChannelText{
+		Version:        0,
+		Text:           "They Don't Think It Be Like It Is, But It Do",
+		ReplyMessageID: nil,
+	}
+
+	textMarshaled, err := proto.Marshal(textPayload)
+	if err != nil {
+		t.Fatalf("failed to marshael the message proto: %+v", err)
+	}
+
+	msgID := cryptoChannel.MakeMessageID(textMarshaled, chID)
+
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	senderNickname := "Alice"
+	ts := netTime.Now()
+
+	lease := 69 * time.Minute
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	// call the handler
+	e.receiveTextMessage(chID, msgID, 0, senderNickname,
+		textMarshaled, pi.PubKey, pi.CodesetVersion, ts, lease, r, Delivered)
+
+	// check the results on the model
+	if !me.eventReceive.channelID.Cmp(chID) {
+		t.Errorf("Channel ID did not propogate correctly, %s vs %s",
+			me.eventReceive.channelID, chID)
+	}
+
+	if !me.eventReceive.messageID.Equals(msgID) {
+		t.Errorf("Message ID did not propogate correctly, %s vs %s",
+			me.eventReceive.messageID, msgID)
+	}
+
+	if !me.eventReceive.reactionTo.Equals(cryptoChannel.MessageID{}) {
+		t.Errorf("Reaction ID is not blank, %s",
+			me.eventReceive.reactionTo)
+	}
+
+	if me.eventReceive.nickname != senderNickname {
+		t.Errorf("SenderID propogate correctly, %s vs %s",
+			me.eventReceive.nickname, senderNickname)
+	}
+
+	if me.eventReceive.timestamp != ts {
+		t.Errorf("Message timestamp did not propogate correctly, %s vs %s",
+			me.eventReceive.timestamp, ts)
+	}
+
+	if me.eventReceive.lease != lease {
+		t.Errorf("Message lease did not propogate correctly, %s vs %s",
+			me.eventReceive.lease, lease)
+	}
+
+	if me.eventReceive.round.ID != r.ID {
+		t.Errorf("Message round did not propogate correctly, %d vs %d",
+			me.eventReceive.round.ID, r.ID)
+	}
+}
+
+func TestEvents_receiveTextMessage_Reply(t *testing.T) {
+	me := &MockEvent{}
+
+	e := initEvents(me)
+
+	// craft the input for the event
+	chID := &id.ID{}
+	chID[0] = 1
+
+	replyMsgId := cryptoChannel.MakeMessageID([]byte("blarg"), chID)
+
+	textPayload := &CMIXChannelText{
+		Version:        0,
+		Text:           "They Don't Think It Be Like It Is, But It Do",
+		ReplyMessageID: replyMsgId[:],
+	}
+
+	textMarshaled, err := proto.Marshal(textPayload)
+	if err != nil {
+		t.Fatalf("failed to marshael the message proto: %+v", err)
+	}
+
+	msgID := cryptoChannel.MakeMessageID(textMarshaled, chID)
+
+	senderUsername := "Alice"
+	ts := netTime.Now()
+
+	lease := 69 * time.Minute
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	// call the handler
+	e.receiveTextMessage(chID, msgID, Text, senderUsername,
+		textMarshaled, pi.PubKey, pi.CodesetVersion, ts, lease, r, Delivered)
+
+	// check the results on the model
+	if !me.eventReceive.channelID.Cmp(chID) {
+		t.Errorf("Channel ID did not propogate correctly, %s vs %s",
+			me.eventReceive.channelID, chID)
+	}
+
+	if !me.eventReceive.messageID.Equals(msgID) {
+		t.Errorf("Message ID did not propogate correctly, %s vs %s",
+			me.eventReceive.messageID, msgID)
+	}
+
+	if !me.eventReceive.reactionTo.Equals(replyMsgId) {
+		t.Errorf("Reaction ID is not equal to what was passed in, "+
+			"%s vs %s", me.eventReceive.reactionTo, replyMsgId)
+	}
+
+	if me.eventReceive.nickname != senderUsername {
+		t.Errorf("SenderID propogate correctly, %s vs %s",
+			me.eventReceive.nickname, senderUsername)
+	}
+
+	if me.eventReceive.timestamp != ts {
+		t.Errorf("Message timestamp did not propogate correctly, "+
+			"%s vs %s", me.eventReceive.timestamp, ts)
+	}
+
+	if me.eventReceive.lease != lease {
+		t.Errorf("Message lease did not propogate correctly, %s vs %s",
+			me.eventReceive.lease, lease)
+	}
+
+	if me.eventReceive.round.ID != r.ID {
+		t.Errorf("Message round did not propogate correctly, %d vs %d",
+			me.eventReceive.round.ID, r.ID)
+	}
+}
+
+func TestEvents_receiveTextMessage_Reply_BadReply(t *testing.T) {
+	me := &MockEvent{}
+
+	e := initEvents(me)
+
+	// craft the input for the event
+	chID := &id.ID{}
+	chID[0] = 1
+
+	replyMsgId := []byte("blarg")
+
+	textPayload := &CMIXChannelText{
+		Version:        0,
+		Text:           "They Don't Think It Be Like It Is, But It Do",
+		ReplyMessageID: replyMsgId[:],
+	}
+
+	textMarshaled, err := proto.Marshal(textPayload)
+	if err != nil {
+		t.Fatalf("failed to marshael the message proto: %+v", err)
+	}
+
+	msgID := cryptoChannel.MakeMessageID(textMarshaled, chID)
+
+	senderUsername := "Alice"
+	ts := netTime.Now()
+
+	lease := 69 * time.Minute
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	// call the handler
+	e.receiveTextMessage(chID, msgID, 0, senderUsername,
+		textMarshaled, pi.PubKey, pi.CodesetVersion, ts, lease, r, Delivered)
+
+	// check the results on the model
+	if !me.eventReceive.channelID.Cmp(chID) {
+		t.Errorf("Channel ID did not propogate correctly, %s vs %s",
+			me.eventReceive.channelID, chID)
+	}
+
+	if !me.eventReceive.messageID.Equals(msgID) {
+		t.Errorf("Message ID did not propogate correctly, %s vs %s",
+			me.eventReceive.messageID, msgID)
+	}
+
+	if !me.eventReceive.reactionTo.Equals(cryptoChannel.MessageID{}) {
+		t.Errorf("Reaction ID is not blank, %s",
+			me.eventReceive.reactionTo)
+	}
+
+	if me.eventReceive.nickname != senderUsername {
+		t.Errorf("SenderID propogate correctly, %s vs %s",
+			me.eventReceive.nickname, senderUsername)
+	}
+
+	if me.eventReceive.timestamp != ts {
+		t.Errorf("Message timestamp did not propogate correctly, "+
+			"%s vs %s", me.eventReceive.timestamp, ts)
+	}
+
+	if me.eventReceive.lease != lease {
+		t.Errorf("Message lease did not propogate correctly, %s vs %s",
+			me.eventReceive.lease, lease)
+	}
+
+	if me.eventReceive.round.ID != r.ID {
+		t.Errorf("Message round did not propogate correctly, %d vs %d",
+			me.eventReceive.round.ID, r.ID)
+	}
+}
+
+func TestEvents_receiveReaction(t *testing.T) {
+	me := &MockEvent{}
+
+	e := initEvents(me)
+
+	// craft the input for the event
+	chID := &id.ID{}
+	chID[0] = 1
+
+	replyMsgId := cryptoChannel.MakeMessageID([]byte("blarg"), chID)
+
+	textPayload := &CMIXChannelReaction{
+		Version:           0,
+		Reaction:          "🍆",
+		ReactionMessageID: replyMsgId[:],
+	}
+
+	textMarshaled, err := proto.Marshal(textPayload)
+	if err != nil {
+		t.Fatalf("failed to marshael the message proto: %+v", err)
+	}
+
+	msgID := cryptoChannel.MakeMessageID(textMarshaled, chID)
+
+	senderUsername := "Alice"
+	ts := netTime.Now()
+
+	lease := 69 * time.Minute
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	// call the handler
+	e.receiveReaction(chID, msgID, 0, senderUsername,
+		textMarshaled, pi.PubKey, pi.CodesetVersion, ts, lease, r, Delivered)
+
+	// check the results on the model
+	if !me.eventReceive.channelID.Cmp(chID) {
+		t.Errorf("Channel ID did not propogate correctly, %s vs %s",
+			me.eventReceive.channelID, chID)
+	}
+
+	if !me.eventReceive.messageID.Equals(msgID) {
+		t.Errorf("Message ID did not propogate correctly, %s vs %s",
+			me.eventReceive.messageID, msgID)
+	}
+
+	if !me.eventReceive.reactionTo.Equals(replyMsgId) {
+		t.Errorf("Reaction ID is not equal to what was passed in, "+
+			"%s vs %s", me.eventReceive.reactionTo, replyMsgId)
+	}
+
+	if me.eventReceive.nickname != senderUsername {
+		t.Errorf("SenderID propogate correctly, %s vs %s",
+			me.eventReceive.nickname, senderUsername)
+	}
+
+	if me.eventReceive.timestamp != ts {
+		t.Errorf("Message timestamp did not propogate correctly, "+
+			"%s vs %s", me.eventReceive.timestamp, ts)
+	}
+
+	if me.eventReceive.lease != lease {
+		t.Errorf("Message lease did not propogate correctly, %s vs %s",
+			me.eventReceive.lease, lease)
+	}
+
+	if me.eventReceive.round.ID != r.ID {
+		t.Errorf("Message round did not propogate correctly, %d vs %d",
+			me.eventReceive.round.ID, r.ID)
+	}
+}
+
+func TestEvents_receiveReaction_InvalidReactionMessageID(t *testing.T) {
+	me := &MockEvent{}
+
+	e := initEvents(me)
+
+	// craft the input for the event
+	chID := &id.ID{}
+	chID[0] = 1
+
+	replyMsgId := []byte("blarg")
+
+	textPayload := &CMIXChannelReaction{
+		Version:           0,
+		Reaction:          "🍆",
+		ReactionMessageID: replyMsgId[:],
+	}
+
+	textMarshaled, err := proto.Marshal(textPayload)
+	if err != nil {
+		t.Fatalf("failed to marshael the message proto: %+v", err)
+	}
+
+	msgID := cryptoChannel.MakeMessageID(textMarshaled, chID)
+
+	senderUsername := "Alice"
+	ts := netTime.Now()
+
+	lease := 69 * time.Minute
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	// call the handler
+	e.receiveReaction(chID, msgID, 0, senderUsername,
+		textMarshaled, pi.PubKey, pi.CodesetVersion, ts, lease, r, Delivered)
+
+	// check the results on the model
+	if me.eventReceive.channelID != nil {
+		t.Errorf("Channel ID did propogated correctly when the reaction " +
+			"is bad")
+	}
+
+	if me.eventReceive.messageID.Equals(msgID) {
+		t.Errorf("Message ID propogated correctly when the reaction is " +
+			"bad")
+	}
+
+	if !me.eventReceive.reactionTo.Equals(cryptoChannel.MessageID{}) {
+		t.Errorf("Reaction ID propogated correctly when the reaction " +
+			"is bad")
+	}
+
+	if me.eventReceive.nickname != "" {
+		t.Errorf("SenderID propogated correctly when the reaction " +
+			"is bad")
+	}
+
+	if me.eventReceive.lease != 0 {
+		t.Errorf("Message lease propogated correctly when the " +
+			"reaction is bad")
+	}
+}
+
+func TestEvents_receiveReaction_InvalidReactionContent(t *testing.T) {
+	me := &MockEvent{}
+
+	e := initEvents(me)
+
+	// craft the input for the event
+	chID := &id.ID{}
+	chID[0] = 1
+
+	replyMsgId := cryptoChannel.MakeMessageID([]byte("blarg"), chID)
+
+	textPayload := &CMIXChannelReaction{
+		Version:           0,
+		Reaction:          "I'm not a reaction",
+		ReactionMessageID: replyMsgId[:],
+	}
+
+	textMarshaled, err := proto.Marshal(textPayload)
+	if err != nil {
+		t.Fatalf("failed to marshael the message proto: %+v", err)
+	}
+
+	msgID := cryptoChannel.MakeMessageID(textMarshaled, chID)
+
+	senderUsername := "Alice"
+	ts := netTime.Now()
+
+	lease := 69 * time.Minute
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+	// call the handler
+	e.receiveReaction(chID, msgID, 0, senderUsername,
+		textMarshaled, pi.PubKey, pi.CodesetVersion, ts, lease, r, Delivered)
+
+	// check the results on the model
+	if me.eventReceive.channelID != nil {
+		t.Errorf("Channel ID did propogated correctly when the reaction " +
+			"is bad")
+	}
+
+	if me.eventReceive.messageID.Equals(msgID) {
+		t.Errorf("Message ID propogated correctly when the reaction is " +
+			"bad")
+	}
+
+	if !me.eventReceive.reactionTo.Equals(cryptoChannel.MessageID{}) {
+		t.Errorf("Reaction ID propogated correctly when the reaction " +
+			"is bad")
+	}
+
+	if me.eventReceive.nickname != "" {
+		t.Errorf("SenderID propogated correctly when the reaction " +
+			"is bad")
+	}
+
+	if me.eventReceive.lease != 0 {
+		t.Errorf("Message lease propogated correctly when the " +
+			"reaction is bad")
+	}
+}
+
+func getFuncName(i interface{}) string {
+	return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
+}
diff --git a/channels/identityStore.go b/channels/identityStore.go
new file mode 100644
index 0000000000000000000000000000000000000000..2432a86c8ee10449502319ae443e32a88c74a908
--- /dev/null
+++ b/channels/identityStore.go
@@ -0,0 +1,31 @@
+package channels
+
+import (
+	"gitlab.com/elixxir/client/storage/versioned"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/xx_network/primitives/netTime"
+)
+
+const (
+	identityStoreStorageKey     = "identityStoreStorageKey"
+	identityStoreStorageVersion = 0
+)
+
+func storeIdentity(kv *versioned.KV, ident cryptoChannel.PrivateIdentity) error {
+	data := ident.Marshal()
+	obj := &versioned.Object{
+		Version:   identityStoreStorageVersion,
+		Timestamp: netTime.Now(),
+		Data:      data,
+	}
+
+	return kv.Set(identityStoreStorageKey, obj)
+}
+
+func loadIdentity(kv *versioned.KV) (cryptoChannel.PrivateIdentity, error) {
+	obj, err := kv.Get(identityStoreStorageKey, identityStoreStorageVersion)
+	if err != nil {
+		return cryptoChannel.PrivateIdentity{}, err
+	}
+	return cryptoChannel.UnmarshalPrivateIdentity(obj.Data)
+}
diff --git a/channels/identityStore_test.go b/channels/identityStore_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..ec2d72f9b90d77accb42d757682fa83f333689f2
--- /dev/null
+++ b/channels/identityStore_test.go
@@ -0,0 +1,46 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"bytes"
+	"encoding/base64"
+	"gitlab.com/elixxir/client/storage/versioned"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/ekv"
+	"gitlab.com/xx_network/crypto/csprng"
+	"testing"
+)
+
+func TestStoreLoadIdentity(t *testing.T) {
+	rng := &csprng.SystemRNG{}
+	privIdentity, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf("GenerateIdentity error: %+v", err)
+	}
+
+	kv := versioned.NewKV(ekv.MakeMemstore())
+	err = storeIdentity(kv, privIdentity)
+	if err != nil {
+		t.Fatalf("storeIdentity error: %+v", err)
+	}
+
+	loadedIdentity, err := loadIdentity(kv)
+	if err != nil {
+		t.Fatalf("loadIdentity error: %+v", err)
+	}
+
+	if !bytes.Equal(loadedIdentity.Marshal(), privIdentity.Marshal()) {
+		t.Fatalf("Failed to load private identity."+
+			"\nExpected: %s"+
+			"\nReceived: %s",
+			base64.StdEncoding.EncodeToString(privIdentity.Marshal()),
+			base64.StdEncoding.EncodeToString(loadedIdentity.Marshal()))
+	}
+
+}
\ No newline at end of file
diff --git a/channels/interface.go b/channels/interface.go
new file mode 100644
index 0000000000000000000000000000000000000000..b7d5445ced2690ef80a77ca0b30f087f814de43f
--- /dev/null
+++ b/channels/interface.go
@@ -0,0 +1,136 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"math"
+	"time"
+
+	"gitlab.com/elixxir/client/cmix"
+	"gitlab.com/elixxir/client/cmix/rounds"
+	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/rsa"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/id/ephemeral"
+)
+
+// ValidForever is used as a validUntil lease when sending to denote the
+// message or operation never expires. Note: A message relay must be
+// present to enforce this otherwise things expire after 3 weeks due to
+// network retention.
+var ValidForever = time.Duration(math.MaxInt64)
+
+type Manager interface {
+
+	// GetIdentity returns the public identity associated with this channel manager
+	GetIdentity() cryptoChannel.Identity
+
+	// ExportPrivateIdentity encrypts and exports the private identity to a
+	// portable string.
+	ExportPrivateIdentity(password string) ([]byte, error)
+
+	// GetStorageTag returns the tag at which this manager is store for loading
+	// it is derived from the public key
+	GetStorageTag() string
+
+	// JoinChannel joins the given channel. It will fail if the channel has
+	// already been joined.
+	JoinChannel(channel *cryptoBroadcast.Channel) error
+
+	// LeaveChannel leaves the given channel. It will return an error if the
+	// channel was not previously joined.
+	LeaveChannel(channelID *id.ID) error
+
+	// SendGeneric is used to send a raw message over a channel. In general, it
+	// should be wrapped in a function which defines the wire protocol
+	// If the final message, before being sent over the wire, is too long, this will
+	// return an error. Due to the underlying encoding using compression, it isn't
+	// possible to define the largest payload that can be sent, but
+	// it will always be possible to send a payload of 802 bytes at minimum
+	// Them meaning of validUntil depends on the use case.
+	SendGeneric(channelID *id.ID, messageType MessageType,
+		msg []byte, validUntil time.Duration, params cmix.CMIXParams) (
+		cryptoChannel.MessageID, rounds.Round, ephemeral.Id, error)
+
+	// SendAdminGeneric is used to send a raw message over a channel encrypted
+	// with admin keys, identifying it as sent by the admin. In general, it
+	// should be wrapped in a function which defines the wire protocol
+	// If the final message, before being sent over the wire, is too long, this will
+	// return an error. The message must be at most 510 bytes long.
+	SendAdminGeneric(privKey rsa.PrivateKey, channelID *id.ID,
+		messageType MessageType, msg []byte, validUntil time.Duration,
+		params cmix.CMIXParams) (cryptoChannel.MessageID,
+		rounds.Round, ephemeral.Id, error)
+
+	// SendMessage is used to send a formatted message over a channel.
+	// Due to the underlying encoding using compression, it isn't
+	// possible to define the largest payload that can be sent, but
+	// it will always be possible to send a payload of 798 bytes at minimum
+	// The message will auto delete validUntil after the round it is sent in,
+	// lasting forever if ValidForever is used
+	SendMessage(channelID *id.ID, msg string, validUntil time.Duration,
+		params cmix.CMIXParams) (
+		cryptoChannel.MessageID, rounds.Round, ephemeral.Id, error)
+
+	// SendReply is used to send a formatted message over a channel.
+	// Due to the underlying encoding using compression, it isn't
+	// possible to define the largest payload that can be sent, but
+	// it will always be possible to send a payload of 766 bytes at minimum.
+	// If the message ID the reply is sent to doesnt exist, the other side will
+	// post the message as a normal message and not a reply.
+	// The message will auto delete validUntil after the round it is sent in,
+	// lasting forever if ValidForever is used
+	SendReply(channelID *id.ID, msg string, replyTo cryptoChannel.MessageID,
+		validUntil time.Duration, params cmix.CMIXParams) (
+		cryptoChannel.MessageID, rounds.Round, ephemeral.Id, error)
+
+	// SendReaction is used to send a reaction to a message over a channel. The
+	// reaction must be a single emoji with no other characters, and will be
+	// rejected otherwise.
+	//
+	// Clients will drop the reaction if they do not recognize the reactTo
+	// message.
+	SendReaction(channelID *id.ID, reaction string,
+		reactTo cryptoChannel.MessageID, params cmix.CMIXParams) (
+		cryptoChannel.MessageID, rounds.Round, ephemeral.Id, error)
+
+	// RegisterReceiveHandler is used to register handlers for non default
+	// message types so that they can be processed by modules. It is important
+	// that such modules sync up with the event model implementation.
+	//
+	// There can only be one handler per message type, and this will return an
+	// error on a multiple registration.
+	RegisterReceiveHandler(messageType MessageType,
+		listener MessageTypeReceiveMessage) error
+
+	// GetChannels returns the IDs of all channels that have been joined. Use
+	// getChannelsUnsafe if you already have taken the mux.
+	GetChannels() []*id.ID
+
+	// GetChannel returns the underlying cryptographic structure for a given
+	// channel.
+	GetChannel(chID *id.ID) (*cryptoBroadcast.Channel, error)
+
+	// ReplayChannel replays all messages from the channel within the network's
+	// memory (~3 weeks) over the event model. It does this by wiping the
+	// underlying state tracking for message pickup for the channel, causing all
+	// messages to be re-retrieved from the network
+	ReplayChannel(chID *id.ID) error
+
+	// SetNickname sets the nickname for a channel after checking that the
+	// nickname is valid using IsNicknameValid.
+	SetNickname(newNick string, ch *id.ID) error
+
+	// DeleteNickname removes the nickname for a given channel, using the
+	// codename for that channel instead.
+	DeleteNickname(ch *id.ID) error
+
+	// GetNickname returns the nickname for the given channel if it exists.
+	GetNickname(ch *id.ID) (nickname string, exists bool)
+}
diff --git a/channels/joinedChannel.go b/channels/joinedChannel.go
new file mode 100644
index 0000000000000000000000000000000000000000..1b6aa53e7f9b2539b983c418eab52368d7c129c0
--- /dev/null
+++ b/channels/joinedChannel.go
@@ -0,0 +1,271 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"encoding/json"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/broadcast"
+	"gitlab.com/elixxir/client/storage/versioned"
+	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/netTime"
+)
+
+const (
+	joinedChannelsVersion = 0
+	joinedChannelsKey     = "JoinedChannelsKey"
+	joinedChannelVersion  = 0
+	joinedChannelKey      = "JoinedChannelKey-"
+)
+
+// store stores the list of joined channels to disk while taking the read lock.
+func (m *manager) store() error {
+	m.mux.RLock()
+	defer m.mux.RUnlock()
+	return m.storeUnsafe()
+}
+
+// storeUnsafe stores the list of joined channels to disk without taking the
+// read lock. It must be used by another function that has already taken the
+// read lock.
+func (m *manager) storeUnsafe() error {
+	channelsList := m.getChannelsUnsafe()
+
+	data, err := json.Marshal(&channelsList)
+	if err != nil {
+		return err
+	}
+
+	obj := &versioned.Object{
+		Version:   joinedChannelsVersion,
+		Timestamp: netTime.Now(),
+		Data:      data,
+	}
+
+	return m.kv.Set(joinedChannelsKey, obj)
+}
+
+// loadChannels loads all currently joined channels from disk and registers them
+// for message reception.
+func (m *manager) loadChannels() {
+	obj, err := m.kv.Get(joinedChannelsKey, joinedChannelsVersion)
+	if !m.kv.Exists(err) {
+		m.channels = make(map[id.ID]*joinedChannel)
+		return
+	} else if err != nil {
+		jww.FATAL.Panicf("Failed to load channels: %+v", err)
+	}
+
+	chList := make([]*id.ID, 0, len(m.channels))
+	if err = json.Unmarshal(obj.Data, &chList); err != nil {
+		jww.FATAL.Panicf("Failed to load channels: %+v", err)
+	}
+
+	chMap := make(map[id.ID]*joinedChannel)
+
+	for i := range chList {
+		jc, err := loadJoinedChannel(
+			chList[i], m.kv, m.net, m.rng, m.events, m.broadcastMaker,
+			m.st.MessageReceive)
+		if err != nil {
+			jww.FATAL.Panicf("Failed to load channel %s: %+v", chList[i], err)
+		}
+		chMap[*chList[i]] = jc
+	}
+
+	m.channels = chMap
+}
+
+// addChannel adds a channel.
+func (m *manager) addChannel(channel *cryptoBroadcast.Channel) error {
+	m.mux.Lock()
+	defer m.mux.Unlock()
+	if _, exists := m.channels[*channel.ReceptionID]; exists {
+		return ChannelAlreadyExistsErr
+	}
+
+	b, err := m.broadcastMaker(channel, m.net, m.rng)
+	if err != nil {
+		return err
+	}
+
+	jc := &joinedChannel{b}
+	if err = jc.Store(m.kv); err != nil {
+		go b.Stop()
+		return err
+	}
+
+	m.channels[*jc.broadcast.Get().ReceptionID] = jc
+
+	if err = m.storeUnsafe(); err != nil {
+		go b.Stop()
+		return err
+	}
+
+	// Connect to listeners
+	err = b.RegisterListener((&userListener{
+		chID:      channel.ReceptionID,
+		trigger:   m.events.triggerEvent,
+		checkSent: m.st.MessageReceive,
+	}).Listen, broadcast.Symmetric)
+	if err != nil {
+		return err
+	}
+
+	err = b.RegisterListener((&adminListener{
+		chID:      channel.ReceptionID,
+		trigger:   m.events.triggerAdminEvent,
+		checkSent: m.st.MessageReceive,
+	}).Listen, broadcast.RSAToPublic)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// removeChannel deletes the channel with the given ID from the channel list and
+// stops it from broadcasting. Returns ChannelDoesNotExistsErr error if the
+// channel does not exist.
+func (m *manager) removeChannel(channelID *id.ID) error {
+	m.mux.Lock()
+	defer m.mux.Unlock()
+
+	ch, exists := m.channels[*channelID]
+	if !exists {
+		return ChannelDoesNotExistsErr
+	}
+
+	ch.broadcast.Stop()
+
+	delete(m.channels, *channelID)
+
+	err := m.storeUnsafe()
+	if err != nil {
+		return err
+	}
+
+	return ch.delete(m.kv)
+}
+
+// getChannel returns the given channel. Returns ChannelDoesNotExistsErr error
+// if the channel does not exist.
+func (m *manager) getChannel(channelID *id.ID) (*joinedChannel, error) {
+	m.mux.RLock()
+	defer m.mux.RUnlock()
+
+	jc, exists := m.channels[*channelID]
+	if !exists {
+		return nil, ChannelDoesNotExistsErr
+	}
+
+	return jc, nil
+}
+
+// getChannelsUnsafe returns the IDs of all channels that have been joined. This
+// function is unsafe because it does not take the mux; only use this function
+// when under a lock.
+func (m *manager) getChannelsUnsafe() []*id.ID {
+	list := make([]*id.ID, 0, len(m.channels))
+	for chID := range m.channels {
+		list = append(list, chID.DeepCopy())
+	}
+	return list
+}
+
+// joinedChannel holds channel info. It will expand to include admin data, so it
+// will be treated as a struct for now.
+type joinedChannel struct {
+	broadcast broadcast.Channel
+}
+
+// joinedChannelDisk is the representation of joinedChannel for storage.
+type joinedChannelDisk struct {
+	Broadcast *cryptoBroadcast.Channel
+}
+
+// Store writes the given channel to a unique storage location within the EKV.
+func (jc *joinedChannel) Store(kv *versioned.KV) error {
+	jcd := joinedChannelDisk{jc.broadcast.Get()}
+	data, err := json.Marshal(&jcd)
+	if err != nil {
+		return err
+	}
+
+	obj := &versioned.Object{
+		Version:   joinedChannelVersion,
+		Timestamp: netTime.Now(),
+		Data:      data,
+	}
+
+	return kv.Set(makeJoinedChannelKey(jc.broadcast.Get().ReceptionID), obj)
+}
+
+// loadJoinedChannel loads a given channel from ekv storage.
+func loadJoinedChannel(chId *id.ID, kv *versioned.KV, net broadcast.Client,
+	rngGen *fastRNG.StreamGenerator, e *events,
+	broadcastMaker broadcast.NewBroadcastChannelFunc, mr messageReceiveFunc) (*joinedChannel, error) {
+	obj, err := kv.Get(makeJoinedChannelKey(chId), joinedChannelVersion)
+	if err != nil {
+		return nil, err
+	}
+
+	jcd := &joinedChannelDisk{}
+
+	err = json.Unmarshal(obj.Data, jcd)
+	if err != nil {
+		return nil, err
+	}
+
+	b, err := initBroadcast(jcd.Broadcast, e, net, broadcastMaker, rngGen, mr)
+
+	jc := &joinedChannel{broadcast: b}
+	return jc, nil
+}
+
+// delete removes the channel from the kv.
+func (jc *joinedChannel) delete(kv *versioned.KV) error {
+	return kv.Delete(makeJoinedChannelKey(jc.broadcast.Get().ReceptionID),
+		joinedChannelVersion)
+}
+
+func makeJoinedChannelKey(chId *id.ID) string {
+	return joinedChannelKey + chId.HexEncode()
+}
+
+func initBroadcast(c *cryptoBroadcast.Channel,
+	e *events, net broadcast.Client,
+	broadcastMaker broadcast.NewBroadcastChannelFunc,
+	rngGen *fastRNG.StreamGenerator, mr messageReceiveFunc) (broadcast.Channel, error) {
+	b, err := broadcastMaker(c, net, rngGen)
+	if err != nil {
+		return nil, err
+	}
+
+	err = b.RegisterListener((&userListener{
+		chID:      c.ReceptionID,
+		trigger:   e.triggerEvent,
+		checkSent: mr,
+	}).Listen, broadcast.Symmetric)
+	if err != nil {
+		return nil, err
+	}
+
+	err = b.RegisterListener((&adminListener{
+		chID:      c.ReceptionID,
+		trigger:   e.triggerAdminEvent,
+		checkSent: mr,
+	}).Listen, broadcast.RSAToPublic)
+	if err != nil {
+		return nil, err
+	}
+
+	return b, nil
+}
diff --git a/channels/joinedChannel_test.go b/channels/joinedChannel_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..ca0ba9ba8319bb246c05f33e816bfbbb9f8b8b39
--- /dev/null
+++ b/channels/joinedChannel_test.go
@@ -0,0 +1,651 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"bytes"
+	"crypto/ed25519"
+	"encoding/binary"
+	"gitlab.com/elixxir/client/broadcast"
+	clientCmix "gitlab.com/elixxir/client/cmix"
+	"gitlab.com/elixxir/client/cmix/message"
+	"gitlab.com/elixxir/client/cmix/rounds"
+	"gitlab.com/elixxir/client/storage/versioned"
+	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	"gitlab.com/elixxir/crypto/rsa"
+	"gitlab.com/elixxir/ekv"
+	"gitlab.com/xx_network/crypto/csprng"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/id/ephemeral"
+	"math/rand"
+	"reflect"
+	"sort"
+	"strconv"
+	"testing"
+	"time"
+)
+
+// Tests that manager.store stores the channel list in the ekv.
+func Test_manager_store(t *testing.T) {
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()),
+		new(mockBroadcastClient),
+		fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG),
+		mockEventModelBuilder)
+	if err != nil {
+		t.Errorf(err.Error())
+	}
+
+	m := mFace.(*manager)
+
+	for i := 0; i < 10; i++ {
+		ch, _, err := newTestChannel(
+			"name_"+strconv.Itoa(i), "description_"+strconv.Itoa(i),
+			m.rng.GetStream(), cryptoBroadcast.Public)
+		if err != nil {
+			t.Errorf("Failed to create new channel %d: %+v", i, err)
+		}
+
+		b, err := broadcast.NewBroadcastChannel(ch, m.net, m.rng)
+		if err != nil {
+			t.Errorf("Failed to make new broadcast channel: %+v", err)
+		}
+
+		m.channels[*ch.ReceptionID] = &joinedChannel{b}
+	}
+
+	err = m.store()
+	if err != nil {
+		t.Errorf("Error storing channels: %+v", err)
+	}
+
+	_, err = m.kv.Get(joinedChannelsKey, joinedChannelsVersion)
+	if !ekv.Exists(err) {
+		t.Errorf("channel list not found in KV: %+v", err)
+	}
+}
+
+// Tests that the manager.loadChannels loads all the expected channels from the
+// ekv.
+func Test_manager_loadChannels(t *testing.T) {
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()),
+		new(mockBroadcastClient),
+		fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG),
+		mockEventModelBuilder)
+	if err != nil {
+		t.Errorf(err.Error())
+	}
+
+	m := mFace.(*manager)
+
+	expected := make([]*joinedChannel, 10)
+
+	for i := range expected {
+		ch, _, err := newTestChannel(
+			"name_"+strconv.Itoa(i), "description_"+strconv.Itoa(i), m.rng.GetStream(), cryptoBroadcast.Public)
+		if err != nil {
+			t.Errorf("Failed to create new channel %d: %+v", i, err)
+		}
+
+		b, err := broadcast.NewBroadcastChannel(ch, m.net, m.rng)
+		if err != nil {
+			t.Errorf("Failed to make new broadcast channel: %+v", err)
+		}
+
+		jc := &joinedChannel{b}
+		if err = jc.Store(m.kv); err != nil {
+			t.Errorf("Failed to store joinedChannel %d: %+v", i, err)
+		}
+
+		chID := *ch.ReceptionID
+		m.channels[chID] = jc
+		expected[i] = jc
+	}
+
+	err = m.store()
+	if err != nil {
+		t.Errorf("Error storing channels: %+v", err)
+	}
+
+	newManager := &manager{
+		channels:       make(map[id.ID]*joinedChannel),
+		kv:             m.kv,
+		net:            m.net,
+		rng:            m.rng,
+		broadcastMaker: m.broadcastMaker,
+	}
+
+	newManager.loadChannels()
+
+	for chID, loadedCh := range newManager.channels {
+		ch, exists := m.channels[chID]
+		if !exists {
+			t.Errorf("Channel %s does not exist.", &chID)
+		}
+
+		if !reflect.DeepEqual(ch.broadcast, loadedCh.broadcast) {
+			t.Errorf("Channel %s does not match loaded channel."+
+				"\nexpected: %+v\nreceived: %+v", &chID, ch.broadcast, loadedCh.broadcast)
+		}
+	}
+}
+
+// Tests that manager.addChannel adds the channel to the map and stores it in
+// the kv.
+func Test_manager_addChannel(t *testing.T) {
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()),
+		new(mockBroadcastClient),
+		fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG),
+		mockEventModelBuilder)
+	if err != nil {
+		t.Errorf(err.Error())
+	}
+
+	m := mFace.(*manager)
+
+	ch, _, err := newTestChannel(
+		"name", "description", m.rng.GetStream(), cryptoBroadcast.Public)
+	if err != nil {
+		t.Errorf("Failed to create new channel: %+v", err)
+	}
+
+	err = m.addChannel(ch)
+	if err != nil {
+		t.Errorf("Failed to add new channel: %+v", err)
+	}
+
+	if _, exists := m.channels[*ch.ReceptionID]; !exists {
+		t.Errorf("Channel %s not added to channel map.", ch.Name)
+	}
+
+	_, err = m.kv.Get(makeJoinedChannelKey(ch.ReceptionID), joinedChannelVersion)
+	if err != nil {
+		t.Errorf("Failed to get joinedChannel from kv: %+v", err)
+	}
+
+	_, err = m.kv.Get(joinedChannelsKey, joinedChannelsVersion)
+	if err != nil {
+		t.Errorf("Failed to get channels from kv: %+v", err)
+	}
+}
+
+// Error path: tests that manager.addChannel returns ChannelAlreadyExistsErr
+// when the channel was already added.
+func Test_manager_addChannel_ChannelAlreadyExistsErr(t *testing.T) {
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()),
+		new(mockBroadcastClient),
+		fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG),
+		mockEventModelBuilder)
+	if err != nil {
+		t.Errorf(err.Error())
+	}
+
+	m := mFace.(*manager)
+
+	ch, _, err := newTestChannel(
+		"name", "description", m.rng.GetStream(), cryptoBroadcast.Public)
+	if err != nil {
+		t.Errorf("Failed to create new channel: %+v", err)
+	}
+
+	err = m.addChannel(ch)
+	if err != nil {
+		t.Errorf("Failed to add new channel: %+v", err)
+	}
+
+	err = m.addChannel(ch)
+	if err == nil || err != ChannelAlreadyExistsErr {
+		t.Errorf("Received incorrect error when adding a channel that already "+
+			"exists.\nexpected: %s\nreceived: %+v", ChannelAlreadyExistsErr, err)
+	}
+}
+
+// Tests the manager.removeChannel deletes the channel from the map.
+func Test_manager_removeChannel(t *testing.T) {
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()),
+		new(mockBroadcastClient),
+		fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG),
+		mockEventModelBuilder)
+	if err != nil {
+		t.Errorf(err.Error())
+	}
+
+	m := mFace.(*manager)
+
+	ch, _, err := newTestChannel(
+		"name", "description", m.rng.GetStream(), cryptoBroadcast.Public)
+	if err != nil {
+		t.Errorf("Failed to create new channel: %+v", err)
+	}
+
+	err = m.addChannel(ch)
+	if err != nil {
+		t.Errorf("Failed to add new channel: %+v", err)
+	}
+
+	err = m.removeChannel(ch.ReceptionID)
+	if err != nil {
+		t.Errorf("Error removing channel: %+v", err)
+	}
+
+	if _, exists := m.channels[*ch.ReceptionID]; exists {
+		t.Errorf("Channel %s was not remove from the channel map.", ch.Name)
+	}
+
+	_, err = m.kv.Get(makeJoinedChannelKey(ch.ReceptionID), joinedChannelVersion)
+	if ekv.Exists(err) {
+		t.Errorf("joinedChannel not removed from kv: %+v", err)
+	}
+}
+
+// Error path: tests that manager.removeChannel returns ChannelDoesNotExistsErr
+// when the channel was never added.
+func Test_manager_removeChannel_ChannelDoesNotExistsErr(t *testing.T) {
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()),
+		new(mockBroadcastClient),
+		fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG),
+		mockEventModelBuilder)
+	if err != nil {
+		t.Errorf(err.Error())
+	}
+
+	m := mFace.(*manager)
+
+	ch, _, err := newTestChannel(
+		"name", "description", m.rng.GetStream(), cryptoBroadcast.Public)
+	if err != nil {
+		t.Errorf("Failed to create new channel: %+v", err)
+	}
+
+	err = m.removeChannel(ch.ReceptionID)
+	if err == nil || err != ChannelDoesNotExistsErr {
+		t.Errorf("Received incorrect error when removing a channel that does "+
+			"not exists.\nexpected: %s\nreceived: %+v",
+			ChannelDoesNotExistsErr, err)
+	}
+}
+
+// Tests the manager.getChannel returns the expected channel.
+func Test_manager_getChannel(t *testing.T) {
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()),
+		new(mockBroadcastClient),
+		fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG),
+		mockEventModelBuilder)
+	if err != nil {
+		t.Errorf(err.Error())
+	}
+
+	m := mFace.(*manager)
+
+	ch, _, err := newTestChannel(
+		"name", "description", m.rng.GetStream(), cryptoBroadcast.Public)
+	if err != nil {
+		t.Errorf("Failed to create new channel: %+v", err)
+	}
+
+	err = m.addChannel(ch)
+	if err != nil {
+		t.Errorf("Failed to add new channel: %+v", err)
+	}
+
+	jc, err := m.getChannel(ch.ReceptionID)
+	if err != nil {
+		t.Errorf("Error getting channel: %+v", err)
+	}
+
+	if !reflect.DeepEqual(ch, jc.broadcast.Get()) {
+		t.Errorf("Received unexpected channel.\nexpected: %+v\nreceived: %+v",
+			ch, jc.broadcast.Get())
+	}
+}
+
+// Error path: tests that manager.getChannel returns ChannelDoesNotExistsErr
+// when the channel was never added.
+func Test_manager_getChannel_ChannelDoesNotExistsErr(t *testing.T) {
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()),
+		new(mockBroadcastClient),
+		fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG),
+		mockEventModelBuilder)
+	if err != nil {
+		t.Errorf(err.Error())
+	}
+
+	m := mFace.(*manager)
+
+	ch, _, err := newTestChannel(
+		"name", "description", m.rng.GetStream(), cryptoBroadcast.Public)
+	if err != nil {
+		t.Errorf("Failed to create new channel: %+v", err)
+	}
+
+	_, err = m.getChannel(ch.ReceptionID)
+	if err == nil || err != ChannelDoesNotExistsErr {
+		t.Errorf("Received incorrect error when getting a channel that does "+
+			"not exists.\nexpected: %s\nreceived: %+v",
+			ChannelDoesNotExistsErr, err)
+	}
+}
+
+// Tests that manager.getChannels returns all the channels that were added to
+// the map.
+func Test_manager_getChannels(t *testing.T) {
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()),
+		new(mockBroadcastClient),
+		fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG),
+		mockEventModelBuilder)
+	if err != nil {
+		t.Errorf(err.Error())
+	}
+
+	m := mFace.(*manager)
+
+	expected := make([]*id.ID, 10)
+
+	for i := range expected {
+		ch, _, err := newTestChannel(
+			"name_"+strconv.Itoa(i), "description_"+strconv.Itoa(i), m.rng.GetStream(), cryptoBroadcast.Public)
+		if err != nil {
+			t.Errorf("Failed to create new channel %d: %+v", i, err)
+		}
+		expected[i] = ch.ReceptionID
+
+		err = m.addChannel(ch)
+		if err != nil {
+			t.Errorf("Failed to add new channel %d: %+v", i, err)
+		}
+	}
+
+	channelIDs := m.getChannelsUnsafe()
+
+	sort.SliceStable(expected, func(i, j int) bool {
+		return bytes.Compare(expected[i][:], expected[j][:]) == -1
+	})
+	sort.SliceStable(channelIDs, func(i, j int) bool {
+		return bytes.Compare(channelIDs[i][:], channelIDs[j][:]) == -1
+	})
+
+	if !reflect.DeepEqual(expected, channelIDs) {
+		t.Errorf("ID list does not match expected.\nexpected: %v\nreceived: %v",
+			expected, channelIDs)
+	}
+}
+
+// Tests that joinedChannel.Store saves the joinedChannel to the expected place
+// in the ekv.
+func Test_joinedChannel_Store(t *testing.T) {
+	kv := versioned.NewKV(ekv.MakeMemstore())
+	rng := fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG)
+	ch, _, err := newTestChannel(
+		"name", "description", rng.GetStream(), cryptoBroadcast.Public)
+	if err != nil {
+		t.Errorf("Failed to create new channel: %+v", err)
+	}
+
+	b, err := broadcast.NewBroadcastChannel(ch, new(mockBroadcastClient), rng)
+	if err != nil {
+		t.Errorf("Failed to create new broadcast channel: %+v", err)
+	}
+
+	jc := &joinedChannel{b}
+
+	err = jc.Store(kv)
+	if err != nil {
+		t.Errorf("Error storing joinedChannel: %+v", err)
+	}
+
+	_, err = kv.Get(makeJoinedChannelKey(ch.ReceptionID), joinedChannelVersion)
+	if !ekv.Exists(err) {
+		t.Errorf("joinedChannel not found in KV: %+v", err)
+	}
+}
+
+// Tests that loadJoinedChannel returns a joinedChannel from storage that
+// matches the original.
+func Test_loadJoinedChannel(t *testing.T) {
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()),
+		new(mockBroadcastClient),
+		fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG),
+		mockEventModelBuilder)
+	if err != nil {
+		t.Errorf(err.Error())
+	}
+
+	m := mFace.(*manager)
+
+	ch, _, err := newTestChannel(
+		"name", "description", m.rng.GetStream(), cryptoBroadcast.Public)
+	if err != nil {
+		t.Errorf("Failed to create new channel: %+v", err)
+	}
+
+	err = m.addChannel(ch)
+	if err != nil {
+		t.Errorf("Failed to add channel: %+v", err)
+	}
+
+	loadedJc, err := loadJoinedChannel(ch.ReceptionID, m.kv, m.net, m.rng,
+		m.events, m.broadcastMaker, func(messageID cryptoChannel.MessageID, r rounds.Round) bool {
+			return false
+		})
+	if err != nil {
+		t.Errorf("Failed to load joinedChannel: %+v", err)
+	}
+
+	if !reflect.DeepEqual(ch, loadedJc.broadcast.Get()) {
+		t.Errorf("Loaded joinedChannel does not match original."+
+			"\nexpected: %+v\nreceived: %+v", ch, loadedJc.broadcast.Get())
+	}
+}
+
+// Tests that joinedChannel.delete deletes the stored joinedChannel from the
+// ekv.
+func Test_joinedChannel_delete(t *testing.T) {
+	kv := versioned.NewKV(ekv.MakeMemstore())
+	rng := fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG)
+	ch, _, err := newTestChannel(
+		"name", "description", rng.GetStream(), cryptoBroadcast.Public)
+	if err != nil {
+		t.Errorf("Failed to create new channel: %+v", err)
+	}
+
+	b, err := broadcast.NewBroadcastChannel(ch, new(mockBroadcastClient), rng)
+	if err != nil {
+		t.Errorf("Failed to create new broadcast channel: %+v", err)
+	}
+
+	jc := &joinedChannel{b}
+
+	err = jc.Store(kv)
+	if err != nil {
+		t.Errorf("Error storing joinedChannel: %+v", err)
+	}
+
+	err = jc.delete(kv)
+	if err != nil {
+		t.Errorf("Error deleting joinedChannel: %+v", err)
+	}
+
+	_, err = kv.Get(makeJoinedChannelKey(ch.ReceptionID), joinedChannelVersion)
+	if ekv.Exists(err) {
+		t.Errorf("joinedChannel found in KV: %+v", err)
+	}
+}
+
+// Consistency test of makeJoinedChannelKey.
+func Test_makeJoinedChannelKey_Consistency(t *testing.T) {
+	values := map[*id.ID]string{
+		id.NewIdFromUInt(0, id.User, t): "JoinedChannelKey-0x0000000000000000000000000000000000000000000000000000000000000000",
+		id.NewIdFromUInt(1, id.User, t): "JoinedChannelKey-0x0000000000000001000000000000000000000000000000000000000000000000",
+		id.NewIdFromUInt(2, id.User, t): "JoinedChannelKey-0x0000000000000002000000000000000000000000000000000000000000000000",
+		id.NewIdFromUInt(3, id.User, t): "JoinedChannelKey-0x0000000000000003000000000000000000000000000000000000000000000000",
+		id.NewIdFromUInt(4, id.User, t): "JoinedChannelKey-0x0000000000000004000000000000000000000000000000000000000000000000",
+		id.NewIdFromUInt(5, id.User, t): "JoinedChannelKey-0x0000000000000005000000000000000000000000000000000000000000000000",
+		id.NewIdFromUInt(6, id.User, t): "JoinedChannelKey-0x0000000000000006000000000000000000000000000000000000000000000000",
+		id.NewIdFromUInt(7, id.User, t): "JoinedChannelKey-0x0000000000000007000000000000000000000000000000000000000000000000",
+		id.NewIdFromUInt(8, id.User, t): "JoinedChannelKey-0x0000000000000008000000000000000000000000000000000000000000000000",
+		id.NewIdFromUInt(9, id.User, t): "JoinedChannelKey-0x0000000000000009000000000000000000000000000000000000000000000000",
+	}
+
+	for chID, expected := range values {
+		key := makeJoinedChannelKey(chID)
+
+		if expected != key {
+			t.Errorf("Unexpected key for ID %d.\nexpected: %s\nreceived: %s",
+				binary.BigEndian.Uint64(chID[:8]), expected, key)
+		}
+	}
+
+}
+
+// newTestChannel creates a new cryptoBroadcast.Channel in the same way that
+// cryptoBroadcast.NewChannel does but with a smaller RSA key and salt to make
+// tests run quicker.
+func newTestChannel(name, description string, rng csprng.Source,
+	level cryptoBroadcast.PrivacyLevel) (
+	*cryptoBroadcast.Channel, rsa.PrivateKey, error) {
+	c, pk, err := cryptoBroadcast.NewChannelVariableKeyUnsafe(
+		name, description, level, 1000, 512, rng)
+	return c, pk, err
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Mock Broadcast Client                                                      //
+////////////////////////////////////////////////////////////////////////////////
+
+// mockBroadcastClient adheres to the broadcast.Client interface.
+type mockBroadcastClient struct{}
+
+func (m *mockBroadcastClient) GetMaxMessageLength() int { return 123 }
+
+func (m *mockBroadcastClient) SendWithAssembler(*id.ID,
+	clientCmix.MessageAssembler, clientCmix.CMIXParams) (
+	rounds.Round, ephemeral.Id, error) {
+	return rounds.Round{ID: id.Round(567)}, ephemeral.Id{}, nil
+}
+
+func (m *mockBroadcastClient) IsHealthy() bool                                       { return true }
+func (m *mockBroadcastClient) AddIdentity(*id.ID, time.Time, bool)                   {}
+func (m *mockBroadcastClient) AddService(*id.ID, message.Service, message.Processor) {}
+func (m *mockBroadcastClient) DeleteClientService(*id.ID)                            {}
+func (m *mockBroadcastClient) RemoveIdentity(*id.ID)                                 {}
+func (m *mockBroadcastClient) GetRoundResults(time.Duration, clientCmix.RoundEventCallback, ...id.Round) {
+}
+func (m *mockBroadcastClient) AddHealthCallback(func(bool)) uint64 { return 0 }
+func (m *mockBroadcastClient) RemoveHealthCallback(uint64)         {}
+
+////////////////////////////////////////////////////////////////////////////////
+// Mock EventModel                                                            //
+////////////////////////////////////////////////////////////////////////////////
+
+func mockEventModelBuilder(string) (EventModel, error) {
+	return &mockEventModel{}, nil
+}
+
+// mockEventModel adheres to the EventModel interface.
+type mockEventModel struct {
+	joinedCh *cryptoBroadcast.Channel
+	leftCh   *id.ID
+}
+
+func (m *mockEventModel) JoinChannel(c *cryptoBroadcast.Channel) { m.joinedCh = c }
+func (m *mockEventModel) LeaveChannel(c *id.ID)                  { m.leftCh = c }
+
+func (m *mockEventModel) ReceiveMessage(*id.ID, cryptoChannel.MessageID, string,
+	string, ed25519.PublicKey, uint8, time.Time, time.Duration, rounds.Round,
+	MessageType, SentStatus) uint64 {
+	return 0
+}
+
+func (m *mockEventModel) ReceiveReply(*id.ID, cryptoChannel.MessageID,
+	cryptoChannel.MessageID, string, string, ed25519.PublicKey, uint8,
+	time.Time, time.Duration, rounds.Round, MessageType, SentStatus) uint64 {
+	return 0
+}
+
+func (m *mockEventModel) ReceiveReaction(*id.ID, cryptoChannel.MessageID,
+	cryptoChannel.MessageID, string, string, ed25519.PublicKey, uint8,
+	time.Time, time.Duration, rounds.Round, MessageType, SentStatus) uint64 {
+	return 0
+}
+
+func (m *mockEventModel) UpdateSentStatus(uint64, cryptoChannel.MessageID,
+	time.Time, rounds.Round, SentStatus) {
+	// TODO implement me
+	panic("implement me")
+}
diff --git a/channels/manager.go b/channels/manager.go
new file mode 100644
index 0000000000000000000000000000000000000000..b6a474e2fff2f874a12fbbcb2d6333f3eeb4db9e
--- /dev/null
+++ b/channels/manager.go
@@ -0,0 +1,262 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+// Package channels provides a channels implementation on top of broadcast
+// which is capable of handing the user facing features of channels, including
+// replies, reactions, and eventually admin commands.
+package channels
+
+import (
+	"crypto/ed25519"
+	"encoding/base64"
+	"fmt"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/broadcast"
+	"gitlab.com/elixxir/client/cmix"
+	"gitlab.com/elixxir/client/cmix/message"
+	"gitlab.com/elixxir/client/cmix/rounds"
+	"gitlab.com/elixxir/client/storage/versioned"
+	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/id/ephemeral"
+	"sync"
+	"time"
+)
+
+const storageTagFormat = "channelManagerStorageTag-%s"
+
+type manager struct {
+	// Sender Identity
+	me cryptoChannel.PrivateIdentity
+
+	// List of all channels
+	channels map[id.ID]*joinedChannel
+	mux      sync.RWMutex
+
+	// External references
+	kv  *versioned.KV
+	net Client
+	rng *fastRNG.StreamGenerator
+
+	// Events model
+	*events
+
+	// Nicknames
+	*nicknameManager
+
+	// Send tracker
+	st *sendTracker
+
+	// Makes the function that is used to create broadcasts be a pointer so that
+	// it can be replaced in tests
+	broadcastMaker broadcast.NewBroadcastChannelFunc
+}
+
+// Client contains the methods from cmix.Client that are required by the
+// [Manager].
+type Client interface {
+	GetMaxMessageLength() int
+	SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler,
+		cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error)
+	IsHealthy() bool
+	AddIdentity(id *id.ID, validUntil time.Time, persistent bool)
+	AddService(clientID *id.ID, newService message.Service,
+		response message.Processor)
+	DeleteClientService(clientID *id.ID)
+	RemoveIdentity(id *id.ID)
+	GetRoundResults(timeout time.Duration, roundCallback cmix.RoundEventCallback,
+		roundList ...id.Round)
+	AddHealthCallback(f func(bool)) uint64
+	RemoveHealthCallback(uint64)
+}
+
+// EventModelBuilder initialises the event model using the given path.
+type EventModelBuilder func(path string) (EventModel, error)
+
+// NewManager creates a new channel Manager from a [channel.PrivateIdentity]. It
+// prefixes the KV with a tag derived from the public key that can be retried
+// for reloading using [Manager.GetStorageTag].
+func NewManager(identity cryptoChannel.PrivateIdentity, kv *versioned.KV,
+	net Client, rng *fastRNG.StreamGenerator, modelBuilder EventModelBuilder) (
+	Manager, error) {
+
+	// Prefix the kv with the username so multiple can be run
+	storageTag := getStorageTag(identity.PubKey)
+	jww.INFO.Printf("NewManager(ID:%s-%s, tag:%s)", identity.Codename,
+		identity.PubKey, storageTag)
+	kv = kv.Prefix(storageTag)
+
+	if err := storeIdentity(kv, identity); err != nil {
+		return nil, err
+	}
+
+	model, err := modelBuilder(storageTag)
+	if err != nil {
+		return nil, errors.Errorf("Failed to build event model: %+v", err)
+	}
+
+	m := setupManager(identity, kv, net, rng, model)
+
+	return m, nil
+}
+
+// LoadManager restores a channel Manager from disk stored at the given storage
+// tag.
+func LoadManager(storageTag string, kv *versioned.KV, net Client,
+	rng *fastRNG.StreamGenerator, modelBuilder EventModelBuilder) (Manager, error) {
+
+	jww.INFO.Printf("LoadManager(tag:%s)", storageTag)
+
+	// Prefix the kv with the username so multiple can be run
+	kv = kv.Prefix(storageTag)
+
+	// Load the identity
+	identity, err := loadIdentity(kv)
+	if err != nil {
+		return nil, err
+	}
+
+	model, err := modelBuilder(storageTag)
+	if err != nil {
+		return nil, errors.Errorf("Failed to build event model: %+v", err)
+	}
+
+	m := setupManager(identity, kv, net, rng, model)
+
+	return m, nil
+}
+
+func setupManager(identity cryptoChannel.PrivateIdentity, kv *versioned.KV,
+	net Client, rng *fastRNG.StreamGenerator, model EventModel) *manager {
+
+	m := manager{
+		me:             identity,
+		kv:             kv,
+		net:            net,
+		rng:            rng,
+		broadcastMaker: broadcast.NewBroadcastChannel,
+	}
+
+	m.events = initEvents(model)
+
+	m.st = loadSendTracker(net, kv, m.events.triggerEvent,
+		m.events.triggerAdminEvent, model.UpdateSentStatus, rng)
+
+	m.loadChannels()
+
+	m.nicknameManager = loadOrNewNicknameManager(kv)
+
+	return &m
+}
+
+// JoinChannel joins the given channel. It will fail if the channel has already
+// been joined.
+func (m *manager) JoinChannel(channel *cryptoBroadcast.Channel) error {
+	jww.INFO.Printf("JoinChannel(%s[%s])", channel.Name, channel.ReceptionID)
+	err := m.addChannel(channel)
+	if err != nil {
+		return err
+	}
+
+	go m.events.model.JoinChannel(channel)
+
+	return nil
+}
+
+// LeaveChannel leaves the given channel. It will return an error if the channel
+// was not previously joined.
+func (m *manager) LeaveChannel(channelID *id.ID) error {
+	jww.INFO.Printf("LeaveChannel(%s)", channelID)
+	err := m.removeChannel(channelID)
+	if err != nil {
+		return err
+	}
+
+	go m.events.model.LeaveChannel(channelID)
+
+	return nil
+}
+
+// GetChannels returns the IDs of all channels that have been joined. Use
+// getChannelsUnsafe if you already have taken the mux.
+func (m *manager) GetChannels() []*id.ID {
+	jww.INFO.Printf("GetChannels")
+	m.mux.Lock()
+	defer m.mux.Unlock()
+	return m.getChannelsUnsafe()
+}
+
+// GetChannel returns the underlying cryptographic structure for a given channel.
+func (m *manager) GetChannel(chID *id.ID) (*cryptoBroadcast.Channel, error) {
+	jww.INFO.Printf("GetChannel(%s)", chID)
+	jc, err := m.getChannel(chID)
+	if err != nil {
+		return nil, err
+	} else if jc.broadcast == nil {
+		return nil, errors.New("broadcast.Channel on joinedChannel is nil")
+	}
+	return jc.broadcast.Get(), nil
+}
+
+// ReplayChannel replays all messages from the channel within the network's
+// memory (~3 weeks) over the event model. It does this by wiping the
+// underlying state tracking for message pickup for the channel, causing all
+// messages to be re-retrieved from the network
+func (m *manager) ReplayChannel(chID *id.ID) error {
+	jww.INFO.Printf("ReplayChannel(%s)", chID)
+	m.mux.RLock()
+	defer m.mux.RUnlock()
+
+	jc, exists := m.channels[*chID]
+	if !exists {
+		return ChannelDoesNotExistsErr
+	}
+
+	c := jc.broadcast.Get()
+
+	// Stop the broadcast that will completely wipe it from the underlying cmix
+	// object
+	jc.broadcast.Stop()
+
+	// Re-instantiate the broadcast, re-registering it from scratch
+	b, err := initBroadcast(c, m.events, m.net, m.broadcastMaker, m.rng,
+		m.st.MessageReceive)
+	if err != nil {
+		return err
+	}
+	jc.broadcast = b
+
+	return nil
+
+}
+
+// GetStorageTag returns the tag at which this manager is store for loading
+// it is derived from the public key
+func (m *manager) GetStorageTag() string {
+	return getStorageTag(m.me.PubKey)
+}
+
+// GetIdentity returns the public identity associated with this channel manager
+func (m *manager) GetIdentity() cryptoChannel.Identity {
+	return m.me.Identity
+}
+
+// ExportPrivateIdentity encrypts and exports the private identity to a portable
+// string.
+func (m *manager) ExportPrivateIdentity(password string) ([]byte, error) {
+	jww.INFO.Printf("ExportPrivateIdentity()")
+	rng := m.rng.GetStream()
+	defer rng.Close()
+	return m.me.Export(password, rng)
+}
+
+func getStorageTag(pub ed25519.PublicKey) string {
+	return fmt.Sprintf(storageTagFormat, base64.StdEncoding.EncodeToString(pub))
+}
diff --git a/channels/manager_test.go b/channels/manager_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..e4213bd7c369143fec41c60895150bf79e0ea3a6
--- /dev/null
+++ b/channels/manager_test.go
@@ -0,0 +1,225 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"fmt"
+	"gitlab.com/elixxir/client/broadcast"
+	"gitlab.com/elixxir/client/storage/versioned"
+	broadcast2 "gitlab.com/elixxir/crypto/broadcast"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	"gitlab.com/elixxir/ekv"
+	"gitlab.com/xx_network/crypto/csprng"
+	"gitlab.com/xx_network/primitives/id"
+	"math/rand"
+	"os"
+	"sync"
+	"testing"
+	"time"
+
+	jww "github.com/spf13/jwalterweatherman"
+)
+
+func TestMain(m *testing.M) {
+	// Many tests trigger WARN prints;, set the out threshold so the WARN prints
+	// can be seen in the logs
+	jww.SetStdoutThreshold(jww.LevelWarn)
+	os.Exit(m.Run())
+}
+
+func TestManager_JoinChannel(t *testing.T) {
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()),
+		new(mockBroadcastClient),
+		fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG),
+		mockEventModelBuilder)
+	if err != nil {
+		t.Errorf(err.Error())
+	}
+
+	m := mFace.(*manager)
+	mem := m.events.model.(*mockEventModel)
+
+	ch, _, err := newTestChannel(
+		"name", "description", m.rng.GetStream(), broadcast2.Public)
+	if err != nil {
+		t.Errorf("Failed to create new channel: %+v", err)
+	}
+
+	err = m.JoinChannel(ch)
+	if err != nil {
+		t.Fatalf("Join Channel Errored: %s", err)
+	}
+
+	if _, exists := m.channels[*ch.ReceptionID]; !exists {
+		t.Errorf("Channel %s not added to channel map.", ch.Name)
+	}
+
+	//wait because the event model is called in another thread
+	time.Sleep(1 * time.Second)
+
+	if mem.joinedCh == nil {
+		t.Errorf("the channel join call was not propogated to the event " +
+			"model")
+	}
+}
+
+func TestManager_LeaveChannel(t *testing.T) {
+
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()),
+		new(mockBroadcastClient),
+		fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG),
+		mockEventModelBuilder)
+	if err != nil {
+		t.Errorf(err.Error())
+	}
+
+	m := mFace.(*manager)
+	mem := m.events.model.(*mockEventModel)
+
+	ch, _, err := newTestChannel(
+		"name", "description", m.rng.GetStream(), broadcast2.Public)
+	if err != nil {
+		t.Errorf("Failed to create new channel: %+v", err)
+	}
+
+	err = m.JoinChannel(ch)
+	if err != nil {
+		t.Fatalf("Join Channel Errored: %s", err)
+	}
+
+	err = m.LeaveChannel(ch.ReceptionID)
+	if err != nil {
+		t.Fatalf("Leave Channel Errored: %s", err)
+	}
+
+	if _, exists := m.channels[*ch.ReceptionID]; exists {
+		t.Errorf("Channel %s still in map.", ch.Name)
+	}
+
+	//wait because the event model is called in another thread
+	time.Sleep(1 * time.Second)
+
+	if mem.leftCh == nil {
+		t.Errorf("the channel join call was not propogated to the event " +
+			"model")
+	}
+}
+
+func TestManager_GetChannels(t *testing.T) {
+	m := &manager{
+		channels: make(map[id.ID]*joinedChannel),
+		mux:      sync.RWMutex{},
+	}
+
+	rng := fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG)
+
+	numtests := 10
+
+	chList := make(map[id.ID]interface{})
+
+	for i := 0; i < 10; i++ {
+		name := fmt.Sprintf("testChannel_%d", numtests)
+		s := rng.GetStream()
+		tc, _, err := newTestChannel(name, "blarg", s, broadcast2.Public)
+		s.Close()
+		if err != nil {
+			t.Fatalf("failed to generate channel %s", name)
+		}
+		bc, err := broadcast.NewBroadcastChannel(tc, new(mockBroadcastClient), rng)
+		if err != nil {
+			t.Fatalf("failed to generate broadcast %s", name)
+		}
+		m.channels[*tc.ReceptionID] = &joinedChannel{broadcast: bc}
+		chList[*tc.ReceptionID] = nil
+	}
+
+	receivedChList := m.GetChannels()
+
+	for _, receivedCh := range receivedChList {
+		if _, exists := chList[*receivedCh]; !exists {
+			t.Errorf("Channel was not returned")
+		}
+	}
+}
+
+func TestManager_GetChannel(t *testing.T) {
+	m := &manager{
+		channels: make(map[id.ID]*joinedChannel),
+		mux:      sync.RWMutex{},
+	}
+
+	rng := fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG)
+
+	numtests := 10
+
+	chList := make([]*id.ID, 0, numtests)
+
+	for i := 0; i < 10; i++ {
+		name := fmt.Sprintf("testChannel_%d", numtests)
+		s := rng.GetStream()
+		tc, _, err := newTestChannel(name, "blarg", s, broadcast2.Public)
+		s.Close()
+		if err != nil {
+			t.Fatalf("failed to generate channel %s", name)
+		}
+		bc, err := broadcast.NewBroadcastChannel(tc, new(mockBroadcastClient), rng)
+		if err != nil {
+			t.Fatalf("failed to generate broadcast %s", name)
+		}
+		m.channels[*tc.ReceptionID] = &joinedChannel{broadcast: bc}
+		chList = append(chList, tc.ReceptionID)
+	}
+
+	for i, receivedCh := range chList {
+		ch, err := m.GetChannel(receivedCh)
+		if err != nil {
+			t.Errorf("Channel %d failed to be gotten", i)
+		} else if !ch.ReceptionID.Cmp(receivedCh) {
+			t.Errorf("Channel %d Get returned wrong channel", i)
+		}
+	}
+}
+
+func TestManager_GetChannel_BadChannel(t *testing.T) {
+	m := &manager{
+		channels: make(map[id.ID]*joinedChannel),
+		mux:      sync.RWMutex{},
+	}
+
+	numtests := 10
+
+	chList := make([]*id.ID, 0, numtests)
+
+	for i := 0; i < 10; i++ {
+		chId := &id.ID{}
+		chId[0] = byte(i)
+		chList = append(chList, chId)
+	}
+
+	for i, receivedCh := range chList {
+		_, err := m.GetChannel(receivedCh)
+		if err == nil {
+			t.Errorf("Channel %d returned when it doesnt exist", i)
+		}
+	}
+}
diff --git a/channels/messageTypes.go b/channels/messageTypes.go
new file mode 100644
index 0000000000000000000000000000000000000000..5ac6e2de3c914131655e484599171c5ac6114d90
--- /dev/null
+++ b/channels/messageTypes.go
@@ -0,0 +1,31 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import "fmt"
+
+type MessageType uint32
+
+const (
+	Text      = MessageType(1)
+	AdminText = MessageType(2)
+	Reaction  = MessageType(3)
+)
+
+func (mt MessageType) String() string {
+	switch mt {
+	case Text:
+		return "Text"
+	case AdminText:
+		return "AdminText"
+	case Reaction:
+		return "Reaction"
+	default:
+		return fmt.Sprintf("Unknown messageType %d", mt)
+	}
+}
diff --git a/channels/messageTypes_test.go b/channels/messageTypes_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..170ebf0542de153bb23c2f2135c66ff597d633f7
--- /dev/null
+++ b/channels/messageTypes_test.go
@@ -0,0 +1,25 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import "testing"
+
+func TestMessageType_String(t *testing.T) {
+	expected := []string{"Text", "AdminText", "Reaction", "Unknown messageType 4",
+		"Unknown messageType 5", "Unknown messageType 6", "Unknown messageType 7",
+		"Unknown messageType 8", "Unknown messageType 9",
+		"Unknown messageType 10"}
+
+	for i := 1; i <= 10; i++ {
+		mt := MessageType(i)
+		if mt.String() != expected[i-1] {
+			t.Errorf("Stringer failed on test %d, %s vs %s", i,
+				mt.String(), expected[i-1])
+		}
+	}
+}
diff --git a/channels/messages.go b/channels/messages.go
new file mode 100644
index 0000000000000000000000000000000000000000..0162c3c62f62c10c7c652d206e405e7a2383327b
--- /dev/null
+++ b/channels/messages.go
@@ -0,0 +1,75 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"github.com/golang/protobuf/proto"
+	"gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/xx_network/primitives/id"
+)
+
+// userMessageInternal is the internal structure of a UserMessage protobuf.
+type userMessageInternal struct {
+	userMessage    *UserMessage
+	channelMessage *ChannelMessage
+	messageID      channel.MessageID
+}
+
+func newUserMessageInternal(ursMsg *UserMessage, chid *id.ID) (*userMessageInternal, error) {
+	chanMessage := &ChannelMessage{}
+	err := proto.Unmarshal(ursMsg.Message, chanMessage)
+	if err != nil {
+		return nil, err
+	}
+
+	channelMessage := chanMessage
+	return &userMessageInternal{
+		userMessage:    ursMsg,
+		channelMessage: channelMessage,
+		messageID:      channel.MakeMessageID(ursMsg.Message, chid),
+	}, nil
+}
+
+func unmarshalUserMessageInternal(usrMsg []byte, chid *id.ID) (*userMessageInternal, error) {
+
+	um := &UserMessage{}
+	if err := proto.Unmarshal(usrMsg, um); err != nil {
+		return nil, err
+	}
+
+	chanMessage := &ChannelMessage{}
+	err := proto.Unmarshal(um.Message, chanMessage)
+	if err != nil {
+		return nil, err
+	}
+
+	channelMessage := chanMessage
+
+	return &userMessageInternal{
+		userMessage:    um,
+		channelMessage: channelMessage,
+		messageID:      channel.MakeMessageID(um.Message, chid),
+	}, nil
+}
+
+// GetUserMessage retrieves the UserMessage within
+// userMessageInternal.
+func (umi *userMessageInternal) GetUserMessage() *UserMessage {
+	return umi.userMessage
+}
+
+// GetChannelMessage retrieves the ChannelMessage within
+// userMessageInternal.
+func (umi *userMessageInternal) GetChannelMessage() *ChannelMessage {
+	return umi.channelMessage
+}
+
+// GetMessageID retrieves the messageID for the message.
+func (umi *userMessageInternal) GetMessageID() channel.MessageID {
+	return umi.messageID
+}
diff --git a/channels/messages_test.go b/channels/messages_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..b5109bd30a535156edc2e80556b9db51d55313e4
--- /dev/null
+++ b/channels/messages_test.go
@@ -0,0 +1,166 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"github.com/golang/protobuf/proto"
+	"gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/xx_network/primitives/id"
+	"reflect"
+	"testing"
+)
+
+func TestUnmarshalUserMessageInternal(t *testing.T) {
+	internal, usrMsg, _ := builtTestUMI(t, 7)
+
+	chID := &id.ID{}
+
+	usrMsgMarshaled, err := proto.Marshal(usrMsg)
+	if err != nil {
+		t.Fatalf("Failed to marshal user message: %+v", err)
+	}
+
+	umi, err := unmarshalUserMessageInternal(usrMsgMarshaled, chID)
+	if err != nil {
+		t.Fatalf("Failed to unmarshal user message: %+v", err)
+	}
+
+	if !umi.GetMessageID().Equals(internal.messageID) {
+		t.Errorf("Message IDs were changed in the unmarshal "+
+			"process, %s vs %s", internal.messageID, umi.GetMessageID())
+	}
+}
+
+func TestUnmarshalUserMessageInternal_BadUserMessage(t *testing.T) {
+	chID := &id.ID{}
+	_, err := unmarshalUserMessageInternal([]byte("Malformed"), chID)
+	if err == nil {
+		t.Fatalf("Error not returned on unmarshaling a bad user " +
+			"message")
+	}
+}
+
+func TestUnmarshalUserMessageInternal_BadChannelMessage(t *testing.T) {
+	_, usrMsg, _ := builtTestUMI(t, 7)
+
+	usrMsg.Message = []byte("Malformed")
+
+	chID := &id.ID{}
+
+	usrMsgMarshaled, err := proto.Marshal(usrMsg)
+	if err != nil {
+		t.Fatalf("Failed to marshal user message: %+v", err)
+	}
+
+	_, err = unmarshalUserMessageInternal(usrMsgMarshaled, chID)
+	if err == nil {
+		t.Fatalf("Error not returned on unmarshaling a user message " +
+			"with a bad channel message")
+	}
+}
+
+func TestNewUserMessageInternal_BadChannelMessage(t *testing.T) {
+	_, usrMsg, _ := builtTestUMI(t, 7)
+
+	usrMsg.Message = []byte("Malformed")
+
+	chID := &id.ID{}
+
+	_, err := newUserMessageInternal(usrMsg, chID)
+
+	if err == nil {
+		t.Fatalf("failed to produce error with malformed user message")
+	}
+}
+
+func TestUserMessageInternal_GetChannelMessage(t *testing.T) {
+	internal, _, channelMsg := builtTestUMI(t, 7)
+	received := internal.GetChannelMessage()
+
+	if !reflect.DeepEqual(received.Payload, channelMsg.Payload) ||
+		received.Lease != channelMsg.Lease ||
+		received.RoundID != channelMsg.RoundID ||
+		received.PayloadType != channelMsg.PayloadType {
+		t.Fatalf("GetChannelMessage did not return expected data."+
+			"\nExpected: %v"+
+			"\nReceived: %v", channelMsg, received)
+	}
+}
+
+func TestUserMessageInternal_GetUserMessage(t *testing.T) {
+	internal, usrMsg, _ := builtTestUMI(t, 7)
+	received := internal.GetUserMessage()
+
+	if !reflect.DeepEqual(received.Message, usrMsg.Message) ||
+		!reflect.DeepEqual(received.Signature, usrMsg.Signature) ||
+		!reflect.DeepEqual(received.ECCPublicKey, usrMsg.ECCPublicKey) {
+		t.Fatalf("GetUserMessage did not return expected data."+
+			"\nExpected: %v"+
+			"\nReceived: %v", usrMsg, received)
+	}
+}
+
+func TestUserMessageInternal_GetMessageID(t *testing.T) {
+	internal, usrMsg, _ := builtTestUMI(t, 7)
+	received := internal.GetMessageID()
+
+	chID := &id.ID{}
+
+	expected := channel.MakeMessageID(usrMsg.Message, chID)
+
+	if !reflect.DeepEqual(expected, received) {
+		t.Fatalf("GetMessageID did not return expected data."+
+			"\nExpected: %v"+
+			"\nReceived: %v", expected, received)
+	}
+}
+
+// Ensures the serialization hasn't changed, changing the message IDs. The
+// protocol is tolerant of this because only the sender seralizes, but
+// it would be good to know when this changes. If this test breaks, report it,
+// but it should be safe to update the expected
+func TestUserMessageInternal_GetMessageID_Consistency(t *testing.T) {
+	expected := "ChMsgID-LrGYLFCaPamZk44X+c/b08qtmJIorgNnoE68v1HYrf8="
+
+	internal, _, _ := builtTestUMI(t, 7)
+
+	received := internal.GetMessageID()
+
+	if expected != received.String() {
+		t.Fatalf("GetMessageID did not return expected data."+
+			"\nExpected: %v"+
+			"\nReceived: %v", expected, received)
+	}
+}
+
+func builtTestUMI(t *testing.T, mt MessageType) (*userMessageInternal, *UserMessage, *ChannelMessage) {
+	channelMsg := &ChannelMessage{
+		Lease:       69,
+		RoundID:     42,
+		PayloadType: uint32(mt),
+		Payload:     []byte("ban_badUSer"),
+		Nickname:    "paul",
+	}
+
+	serialized, err := proto.Marshal(channelMsg)
+	if err != nil {
+		t.Fatalf("Marshal error: %v", err)
+	}
+
+	usrMsg := &UserMessage{
+		Message:      serialized,
+		Signature:    []byte("sig2"),
+		ECCPublicKey: []byte("key"),
+	}
+
+	chID := &id.ID{}
+
+	internal, _ := newUserMessageInternal(usrMsg, chID)
+
+	return internal, usrMsg, channelMsg
+}
diff --git a/channels/mutateTimestamp.go b/channels/mutateTimestamp.go
new file mode 100644
index 0000000000000000000000000000000000000000..8b8bf683e971635458124783b76754fa9e3638b5
--- /dev/null
+++ b/channels/mutateTimestamp.go
@@ -0,0 +1,76 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/xx_network/crypto/large"
+	"time"
+)
+
+const (
+	// tenMsInNs is a prime close to one million to ensure patterns dont
+	// arise due to cofactors with the message ID when doing the modulo
+	tenMsInNs     = 10000019
+	halfTenMsInNs = tenMsInNs / 2
+	beforeGrace   = 5 * time.Second
+	afterGrace    = 2 * time.Second
+)
+
+var tenMsInNsLargeInt = large.NewInt(tenMsInNs)
+
+// vetTimestamp determines which timestamp to use for a message. It will
+// use the local timestamp provided in the message as long as it is within 5
+// seconds before the round and 2 second after the round. Otherwise, it will
+// use the round timestamp via mutateTimestamp
+func vetTimestamp(localTS, ts time.Time, msgID channel.MessageID) time.Time {
+
+	before := ts.Add(-beforeGrace)
+	after := ts.Add(afterGrace)
+
+	if localTS.Before(before) || localTS.After(after) {
+		return mutateTimestamp(ts, msgID)
+	}
+
+	return localTS
+}
+
+// mutateTimestamp is used to modify the the timestamps on all messages in a
+// deterministic manner. This is because message ordering is done by timestamp
+// and the timestamps come from the rounds, which means multiple messages can
+// have the same timestamp due to being in the same round. The meaning of
+// conversations can change depending on order, so while no explicit order
+// can be discovered because to do so can leak potential ordering info for the
+// mix, choosing an arbitrary order and having all clients agree will at least
+// ensure that misunderstandings due to disagreements in order cannot occur
+//
+// In order to do this, this function mutates the timestamp of the round within
+// +/- 5ms seeded based upon the message ID.
+// It should be noted that this is only a reasonable assumption when the number
+// of messages in a channel isn't too much. For example, under these conditions
+// the birthday paradox of getting a collision if there are 10 messages for the
+// channel in the same round is ~4*10^-6, but the chance if there are 50
+// messages is 10^-4, and if the entire round is full of messages for the
+// channel (1000 messages), .0487.
+func mutateTimestamp(ts time.Time, msgID channel.MessageID) time.Time {
+
+	// Treat the message ID as a number and mod it by the number of ns in an ms
+	// to get an offset factor. Use a prime close to 1000000 to make sure there
+	// are no patterns in the output and reduce the chance of collision. While
+	// the fields do not align, so there is some bias towards some parts of the
+	// output field, that bias is too small to matter because log2(10000019) ~23
+	// while the input field is 256.
+	offsetLarge := large.NewIntFromBytes(msgID.Bytes())
+	offsetLarge.Mod(offsetLarge, tenMsInNsLargeInt)
+
+	// subtract half the field size so on average (across many runs) the message
+	// timestamps are not changed
+	offset := offsetLarge.Int64() - halfTenMsInNs
+
+	return time.Unix(0, ts.UnixNano()+offset)
+}
diff --git a/channels/mutateTimestamp_test.go b/channels/mutateTimestamp_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..93561868b10ac781ef2c86cc0b483a0fed1815f8
--- /dev/null
+++ b/channels/mutateTimestamp_test.go
@@ -0,0 +1,129 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"gitlab.com/xx_network/primitives/netTime"
+	"math/rand"
+	"testing"
+	"time"
+
+	"gitlab.com/elixxir/crypto/channel"
+)
+
+// withinMutationWindow is a utility test function to check if a mutated
+// timestamp is within the allowable window
+func withinMutationWindow(raw, mutated time.Time) bool {
+	lowerBound := raw.Add(-time.Duration(halfTenMsInNs))
+	upperBound := raw.Add(time.Duration(halfTenMsInNs))
+
+	return mutated.After(lowerBound) && mutated.Before(upperBound)
+}
+
+func abs(n int64) int64 {
+	if n < 0 {
+		return -n
+	}
+	return n
+}
+
+func TestMutateTimestampDeltaAverage(t *testing.T) {
+	samples := 10000
+	t1 := netTime.Now()
+	sum := int64(0)
+
+	rng := rand.New(rand.NewSource(netTime.Now().UnixNano()))
+
+	for i := 0; i < samples; i++ {
+		var msgID channel.MessageID
+		rng.Read(msgID[:])
+		t2 := mutateTimestamp(t1, msgID)
+		delta := t2.Sub(t1)
+		sum += abs(int64(delta))
+	}
+
+	avg := sum / int64(samples)
+	diff := abs(avg - 2502865)
+	if diff > 30000 {
+		t.Fatal()
+	}
+}
+
+const generationRange = beforeGrace + afterGrace
+
+// TestVetTimestamp_Happy tests that when the localTS is within
+// the allowed range, it is unmodified
+func TestVetTimestamp_Happy(t *testing.T) {
+	samples := 10000
+
+	rng := rand.New(rand.NewSource(netTime.Now().UnixNano()))
+
+	for i := 0; i < samples; i++ {
+
+		now := time.Now()
+
+		tested := now.Add(-beforeGrace).Add(time.Duration(rng.Int63()) % generationRange)
+
+		var msgID channel.MessageID
+		rng.Read(msgID[:])
+
+		result := vetTimestamp(tested, now, msgID)
+
+		if !tested.Equal(result) {
+			t.Errorf("Timestamp was molested unexpectedly")
+		}
+	}
+}
+
+// TestVetTimestamp_Happy tests that when the localTS is less than
+// the allowed time period it is replaced
+func TestVetTimestamp_BeforePeriod(t *testing.T) {
+	samples := 10000
+
+	rng := rand.New(rand.NewSource(netTime.Now().UnixNano()))
+
+	for i := 0; i < samples; i++ {
+
+		now := time.Now()
+
+		tested := now.Add(-beforeGrace).Add(-time.Duration(rng.Int63()) % (100000 * time.Hour))
+
+		var msgID channel.MessageID
+		rng.Read(msgID[:])
+
+		result := vetTimestamp(tested, now, msgID)
+
+		if tested.Equal(result) {
+			t.Errorf("Timestamp was unmolested unexpectedly")
+		}
+	}
+}
+
+// TestVetTimestamp_Happy tests that when the localTS is greater than
+// the allowed time period it is replaced
+func TestVetTimestamp_AfterPeriod(t *testing.T) {
+	samples := 10000
+
+	rng := rand.New(rand.NewSource(netTime.Now().UnixNano()))
+
+	for i := 0; i < samples; i++ {
+
+		now := time.Now()
+
+		tested := now.Add(afterGrace).Add(-time.Duration(rng.Int63()) % (100000 * time.Hour))
+
+		var msgID channel.MessageID
+		rng.Read(msgID[:])
+
+		result := vetTimestamp(tested, now, msgID)
+
+		if tested.Equal(result) {
+			t.Errorf("Timestamp was unmolested unexpectedly")
+		}
+	}
+}
diff --git a/channels/nameService.go b/channels/nameService.go
new file mode 100644
index 0000000000000000000000000000000000000000..d77fa9414a4f0fe1d8a32fae130936187f2fcf92
--- /dev/null
+++ b/channels/nameService.go
@@ -0,0 +1,38 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"crypto/ed25519"
+	"time"
+)
+
+// NameService is an interface which encapsulates
+// the user identity channel tracking service.
+// NameService is currently unused
+type NameService interface {
+
+	// GetUsername returns the username.
+	GetUsername() string
+
+	// GetChannelValidationSignature returns the validation
+	// signature and the time it was signed.
+	GetChannelValidationSignature() ([]byte, time.Time)
+
+	// GetChannelPubkey returns the user's public key.
+	GetChannelPubkey() ed25519.PublicKey
+
+	// SignChannelMessage returns the signature of the
+	// given message.
+	SignChannelMessage(message []byte) (signature []byte, err error)
+
+	// ValidateChannelMessage validates that a received channel message's
+	// username lease is signed by the NameService
+	ValidateChannelMessage(username string, lease time.Time,
+		pubKey ed25519.PublicKey, authorIDSignature []byte) bool
+}
diff --git a/channels/nickname.go b/channels/nickname.go
new file mode 100644
index 0000000000000000000000000000000000000000..5116b597f754c09ce1b6bae06ec878c5192debfe
--- /dev/null
+++ b/channels/nickname.go
@@ -0,0 +1,146 @@
+package channels
+
+import (
+	"encoding/json"
+	"errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/storage/versioned"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/netTime"
+	"sync"
+)
+
+const (
+	nicknameStoreStorageKey     = "nicknameStoreStorageKey"
+	nicknameStoreStorageVersion = 0
+)
+
+type nicknameManager struct {
+	byChannel map[id.ID]string
+
+	mux sync.RWMutex
+
+	kv *versioned.KV
+}
+
+// loadOrNewNicknameManager returns the stored nickname manager if there is
+// one or returns a new one
+func loadOrNewNicknameManager(kv *versioned.KV) *nicknameManager {
+	nm := &nicknameManager{
+		byChannel: make(map[id.ID]string),
+		kv:        kv,
+	}
+	err := nm.load()
+	if err != nil && nm.kv.Exists(err) {
+		jww.FATAL.Panicf("Failed to load nicknameManager: %+v", err)
+	}
+
+	return nm
+
+}
+
+// GetNickname returns the nickname for the given channel if it exists
+func (nm *nicknameManager) GetNickname(ch *id.ID) (
+	nickname string, exists bool) {
+	nm.mux.RLock()
+	defer nm.mux.RUnlock()
+
+	nickname, exists = nm.byChannel[*ch]
+	return
+}
+
+// SetNickname sets the nickname for a channel after checking that the nickname
+// is valid using IsNicknameValid
+func (nm *nicknameManager) SetNickname(newNick string, ch *id.ID) error {
+	nm.mux.Lock()
+	defer nm.mux.Unlock()
+
+	if err := IsNicknameValid(newNick); err != nil {
+		return err
+	}
+
+	nm.byChannel[*ch] = newNick
+	return nm.save()
+}
+
+// DeleteNickname removes the nickname for a given channel, using the codename
+// for that channel instead
+func (nm *nicknameManager) DeleteNickname(ch *id.ID) error {
+	nm.mux.Lock()
+	defer nm.mux.Unlock()
+
+	delete(nm.byChannel, *ch)
+
+	return nm.save()
+}
+
+// channelIDToNickname is a serialization structure. This is used by the save
+// and load functions to serialize the nicknameManager's byChannel map.
+type channelIDToNickname struct {
+	ChannelId id.ID
+	Nickname  string
+}
+
+// save stores the nickname manager to disk. The caller of this must
+// hold the mux.
+func (nm *nicknameManager) save() error {
+	list := make([]channelIDToNickname, 0)
+	for chId, nickname := range nm.byChannel {
+		list = append(list, channelIDToNickname{
+			ChannelId: chId,
+			Nickname:  nickname,
+		})
+	}
+
+	data, err := json.Marshal(list)
+	if err != nil {
+		return err
+	}
+	obj := &versioned.Object{
+		Version:   nicknameStoreStorageVersion,
+		Timestamp: netTime.Now(),
+		Data:      data,
+	}
+
+	return nm.kv.Set(nicknameStoreStorageKey, obj)
+}
+
+// load restores the nickname manager from disk.
+func (nm *nicknameManager) load() error {
+	obj, err := nm.kv.Get(nicknameStoreStorageKey, nicknameStoreStorageVersion)
+	if err != nil {
+		return err
+	}
+
+	list := make([]channelIDToNickname, 0)
+	err = json.Unmarshal(obj.Data, &list)
+	if err != nil {
+		return err
+	}
+
+	for i := range list {
+		current := list[i]
+		nm.byChannel[current.ChannelId] = current.Nickname
+	}
+
+	return nil
+}
+
+// IsNicknameValid checks if a nickname is valid
+//
+// rules
+//   - a nickname must not be longer than 24 characters
+//   - a nickname must not be shorter than 1 character
+// todo: add character filtering
+func IsNicknameValid(nick string) error {
+	runeNick := []rune(nick)
+	if len(runeNick) > 24 {
+		return errors.New("nicknames must be 24 characters in length or less")
+	}
+
+	if len(runeNick) < 1 {
+		return errors.New("nicknames must be at least 1 character in length")
+	}
+
+	return nil
+}
diff --git a/channels/nickname_test.go b/channels/nickname_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..cf535245f82bb1e4b5c4caf5ac0b65f6355b4653
--- /dev/null
+++ b/channels/nickname_test.go
@@ -0,0 +1,108 @@
+package channels
+
+import (
+	"gitlab.com/elixxir/client/storage/versioned"
+	"gitlab.com/elixxir/ekv"
+	"gitlab.com/xx_network/primitives/id"
+	"strconv"
+	"testing"
+)
+
+// Unit test. Tests that once you set a nickname with SetNickname, you can
+// retrieve the nickname using GetNickname.
+func TestNicknameManager_SetGetNickname(t *testing.T) {
+	kv := versioned.NewKV(ekv.MakeMemstore())
+	nm := loadOrNewNicknameManager(kv)
+
+	for i := 0; i < numTests; i++ {
+		chId := id.NewIdFromUInt(uint64(i), id.User, t)
+		nickname := "nickname#" + strconv.Itoa(i)
+		err := nm.SetNickname(nickname, chId)
+		if err != nil {
+			t.Fatalf("SetNickname error when setting %s: %+v", nickname, err)
+		}
+
+		received, _ := nm.GetNickname(chId)
+		if received != nickname {
+			t.Fatalf("GetNickname did not return expected values."+
+				"\nExpected: %s"+
+				"\nReceived: %s", nickname, received)
+		}
+	}
+}
+
+// Unit test. Tests that once you set a nickname with SetNickname, you can
+// retrieve the nickname using GetNickname after a reload.
+func TestNicknameManager_SetGetNickname_Reload(t *testing.T) {
+	kv := versioned.NewKV(ekv.MakeMemstore())
+	nm := loadOrNewNicknameManager(kv)
+
+	for i := 0; i < numTests; i++ {
+		chId := id.NewIdFromUInt(uint64(i), id.User, t)
+		nickname := "nickname#" + strconv.Itoa(i)
+		err := nm.SetNickname(nickname, chId)
+		if err != nil {
+			t.Fatalf("SetNickname error when setting %s: %+v", nickname, err)
+		}
+	}
+
+	nm2 := loadOrNewNicknameManager(kv)
+
+	for i := 0; i < numTests; i++ {
+		chId := id.NewIdFromUInt(uint64(i), id.User, t)
+		nick, exists := nm2.GetNickname(chId)
+		if !exists {
+			t.Fatalf("Nickname %d not found  ", i)
+		}
+		expected := "nickname#" + strconv.Itoa(i)
+		if nick != expected {
+			t.Fatalf("Nickname %d not found, expected: %s, received: %s ", i, expected, nick)
+		}
+	}
+}
+
+// Error case: Tests that nicknameManager.GetNickname returns a false boolean
+// if no nickname has been set with the channel ID.
+func TestNicknameManager_GetNickname_Error(t *testing.T) {
+	kv := versioned.NewKV(ekv.MakeMemstore())
+	nm := loadOrNewNicknameManager(kv)
+
+	for i := 0; i < numTests; i++ {
+		chId := id.NewIdFromUInt(uint64(i), id.User, t)
+		_, exists := nm.GetNickname(chId)
+		if exists {
+			t.Fatalf("GetNickname expected error case: " +
+				"This should not retrieve nicknames for channel IDs " +
+				"that are not set.")
+		}
+	}
+}
+
+// Unit test. Check that once you SetNickname and DeleteNickname,
+// GetNickname returns a false boolean.
+func TestNicknameManager_DeleteNickname(t *testing.T) {
+	kv := versioned.NewKV(ekv.MakeMemstore())
+	nm := loadOrNewNicknameManager(kv)
+
+	for i := 0; i < numTests; i++ {
+		chId := id.NewIdFromUInt(uint64(i), id.User, t)
+		nickname := "nickname#" + strconv.Itoa(i)
+		err := nm.SetNickname(nickname, chId)
+		if err != nil {
+			t.Fatalf("SetNickname error when setting %s: %+v", nickname, err)
+		}
+
+		err = nm.DeleteNickname(chId)
+		if err != nil {
+			t.Fatalf("DeleteNickname error: %+v", err)
+		}
+
+		_, exists := nm.GetNickname(chId)
+		if exists {
+			t.Fatalf("GetNickname expected error case: " +
+				"This should not retrieve nicknames for channel IDs " +
+				"that are not set.")
+		}
+	}
+
+}
diff --git a/channels/readme.md b/channels/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..9327e9635afd9c109054bf7b486d24c97a53c0bc
--- /dev/null
+++ b/channels/readme.md
@@ -0,0 +1,21 @@
+Channels provides a channels implementation on top of broadcast which is capable of handing the user facing features of
+channels, including replies, reactions, and eventually admin commands.
+
+on sending, data propagates as follows:
+Send function (Example: SendMessage) - > SendGeneric ->
+Broadcast.BroadcastWithAssembler -> cmix.SendWithAssembler
+
+on receiving messages propagate as follows:
+cmix message pickup (by service)- > broadcast.Processor ->
+userListener ->  events.triggerEvent ->
+messageTypeHandler (example: Text) ->
+eventModel (example: ReceiveMessage)
+
+on sendingAdmin, data propagates as follows:
+Send function - > SendAdminGeneric ->
+Broadcast.BroadcastAsymmetricWithAssembler -> cmix.SendWithAssembler
+
+on receiving admin messages propagate as follows:
+cmix message pickup (by service)- > broadcast.Processor -> adminListener ->
+events.triggerAdminEvent -> messageTypeHandler (example: Text) ->
+eventModel (example: ReceiveMessage)
\ No newline at end of file
diff --git a/channels/send.go b/channels/send.go
new file mode 100644
index 0000000000000000000000000000000000000000..0e508d4cc6d6c952554d90849541ba6514f46463
--- /dev/null
+++ b/channels/send.go
@@ -0,0 +1,374 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"crypto/ed25519"
+	"encoding/base64"
+	"fmt"
+	"time"
+
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/cmix"
+	"gitlab.com/elixxir/client/cmix/rounds"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/rsa"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/id/ephemeral"
+	"gitlab.com/xx_network/primitives/netTime"
+	"golang.org/x/crypto/blake2b"
+	"google.golang.org/protobuf/proto"
+)
+
+const (
+	cmixChannelTextVersion     = 0
+	cmixChannelReactionVersion = 0
+	SendMessageTag             = "ChMessage"
+	SendReplyTag               = "ChReply"
+	SendReactionTag            = "ChReaction"
+)
+
+// The size of the nonce used in the message ID.
+const messageNonceSize = 4
+
+// SendGeneric is used to send a raw message over a channel. In general, it
+// should be wrapped in a function which defines the wire protocol
+// If the final message, before being sent over the wire, is too long, this will
+// return an error. Due to the underlying encoding using compression, it isn't
+// possible to define the largest payload that can be sent, but
+// it will always be possible to send a payload of 802 bytes at minimum
+func (m *manager) SendGeneric(channelID *id.ID, messageType MessageType,
+	msg []byte, validUntil time.Duration, params cmix.CMIXParams) (
+	cryptoChannel.MessageID, rounds.Round, ephemeral.Id, error) {
+
+	// Note: We log sends on exit, and append what happened to the message
+	// this cuts down on clutter in the log.
+	sendPrint := fmt.Sprintf("[%s] Sending ch %s type %d at %s",
+		params.DebugTag, channelID, messageType,
+		netTime.Now())
+	defer jww.INFO.Println(sendPrint)
+
+	//find the channel
+	ch, err := m.getChannel(channelID)
+	if err != nil {
+		return cryptoChannel.MessageID{}, rounds.Round{},
+			ephemeral.Id{}, err
+	}
+
+	nickname, _ := m.GetNickname(channelID)
+
+	var msgId cryptoChannel.MessageID
+
+	chMsg := &ChannelMessage{
+		Lease:          validUntil.Nanoseconds(),
+		PayloadType:    uint32(messageType),
+		Payload:        msg,
+		Nickname:       nickname,
+		Nonce:          make([]byte, messageNonceSize),
+		LocalTimestamp: netTime.Now().UnixNano(),
+	}
+
+	// Generate random nonce to be used for message ID
+	// generation. This makes it so two identical messages sent on
+	// the same round have different message IDs
+	rng := m.rng.GetStream()
+	n, err := rng.Read(chMsg.Nonce)
+	rng.Close()
+	if err != nil {
+		sendPrint += fmt.Sprintf(", failed to generate nonce: %+v", err)
+		return cryptoChannel.MessageID{}, rounds.Round{},
+			ephemeral.Id{},
+			errors.Errorf("Failed to generate nonce: %+v", err)
+	} else if n != messageNonceSize {
+		sendPrint += fmt.Sprintf(", got %d bytes for %d-byte nonce", n,
+			messageNonceSize)
+		return cryptoChannel.MessageID{}, rounds.Round{},
+			ephemeral.Id{},
+			errors.Errorf(
+				"Generated %d bytes for %d-byte nonce", n,
+				messageNonceSize)
+	}
+
+	usrMsg := &UserMessage{
+		ECCPublicKey: m.me.PubKey,
+	}
+
+	//Note: we are not checking if message is too long before trying to
+	//find a round
+
+	//Build the function pointer that will build the message
+	assemble := func(rid id.Round) ([]byte, error) {
+
+		//Build the message
+		chMsg.RoundID = uint64(rid)
+
+		//Serialize the message
+		chMsgSerial, err := proto.Marshal(chMsg)
+		if err != nil {
+			return nil, err
+		}
+
+		//make the messageID
+		msgId = cryptoChannel.MakeMessageID(chMsgSerial, channelID)
+
+		//Sign the message
+		messageSig := ed25519.Sign(*m.me.Privkey, chMsgSerial)
+
+		usrMsg.Message = chMsgSerial
+		usrMsg.Signature = messageSig
+
+		//Serialize the user message
+		usrMsgSerial, err := proto.Marshal(usrMsg)
+		if err != nil {
+			return nil, err
+		}
+
+		return usrMsgSerial, nil
+	}
+
+	sendPrint += fmt.Sprintf(", pending send %s", netTime.Now())
+	uuid, err := m.st.denotePendingSend(channelID, &userMessageInternal{
+		userMessage:    usrMsg,
+		channelMessage: chMsg,
+		messageID:      msgId,
+	})
+	if err != nil {
+		sendPrint += fmt.Sprintf(", pending send failed %s",
+			err.Error())
+		return cryptoChannel.MessageID{}, rounds.Round{},
+			ephemeral.Id{}, err
+	}
+
+	sendPrint += fmt.Sprintf(", broadcasting message %s", netTime.Now())
+	r, ephid, err := ch.broadcast.BroadcastWithAssembler(assemble, params)
+	if err != nil {
+		sendPrint += fmt.Sprintf(", broadcast failed %s, %s",
+			netTime.Now(), err.Error())
+		errDenote := m.st.failedSend(uuid)
+		if errDenote != nil {
+			sendPrint += fmt.Sprintf(", failed to denote failed "+
+				"broadcast: %s", err.Error())
+		}
+		return cryptoChannel.MessageID{}, rounds.Round{},
+			ephemeral.Id{}, err
+	}
+	sendPrint += fmt.Sprintf(", broadcast succeeded %s, success!",
+		netTime.Now())
+	err = m.st.send(uuid, msgId, r)
+	if err != nil {
+		sendPrint += fmt.Sprintf(", broadcast failed: %s ", err.Error())
+	}
+	return msgId, r, ephid, err
+}
+
+// SendAdminGeneric is used to send a raw message over a channel encrypted
+// with admin keys, identifying it as sent by the admin. In general, it
+// should be wrapped in a function which defines the wire protocol
+// If the final message, before being sent over the wire, is too long, this will
+// return an error. The message must be at most 510 bytes long.
+func (m *manager) SendAdminGeneric(privKey rsa.PrivateKey, channelID *id.ID,
+	messageType MessageType, msg []byte, validUntil time.Duration,
+	params cmix.CMIXParams) (cryptoChannel.MessageID, rounds.Round,
+	ephemeral.Id, error) {
+
+	// Note: We log sends on exit, and append what happened to the message
+	// this cuts down on clutter in the log.
+	sendPrint := fmt.Sprintf("[%s] Admin sending ch %s type %d at %s",
+		params.DebugTag, channelID, messageType,
+		netTime.Now())
+	defer jww.INFO.Println(sendPrint)
+
+	//find the channel
+	ch, err := m.getChannel(channelID)
+	if err != nil {
+		return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, err
+	}
+
+	var msgId cryptoChannel.MessageID
+	chMsg := &ChannelMessage{
+		Lease:          validUntil.Nanoseconds(),
+		PayloadType:    uint32(messageType),
+		Payload:        msg,
+		Nickname:       AdminUsername,
+		Nonce:          make([]byte, messageNonceSize),
+		LocalTimestamp: netTime.Now().UnixNano(),
+	}
+
+	// Generate random nonce to be used for message ID generation. This makes it
+	// so two identical messages sent on the same round have different message IDs
+	rng := m.rng.GetStream()
+	n, err := rng.Read(chMsg.Nonce)
+	rng.Close()
+	if err != nil {
+		return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{},
+			errors.Errorf("Failed to generate nonce: %+v", err)
+	} else if n != messageNonceSize {
+		return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{},
+			errors.Errorf(
+				"Generated %d bytes for %-byte nonce", n, messageNonceSize)
+	}
+
+	// Note: we are not checking if message is too long before trying to
+	// find a round
+
+	//Build the function pointer that will build the message
+	assemble := func(rid id.Round) ([]byte, error) {
+
+		//Build the message
+		chMsg.RoundID = uint64(rid)
+
+		//Serialize the message
+		chMsgSerial, err := proto.Marshal(chMsg)
+		if err != nil {
+			return nil, err
+		}
+
+		msgId = cryptoChannel.MakeMessageID(chMsgSerial, channelID)
+
+		//check if the message is too long
+		if len(chMsgSerial) > ch.broadcast.MaxRSAToPublicPayloadSize() {
+			return nil, MessageTooLongErr
+		}
+
+		return chMsgSerial, nil
+	}
+
+	sendPrint += fmt.Sprintf(", pending send %s", netTime.Now())
+	uuid, err := m.st.denotePendingAdminSend(channelID, chMsg)
+	if err != nil {
+		sendPrint += fmt.Sprintf(", pending send failed %s",
+			err.Error())
+		return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, err
+	}
+
+	sendPrint += fmt.Sprintf(", broadcasting message %s", netTime.Now())
+	r, ephid, err := ch.broadcast.BroadcastRSAToPublicWithAssembler(privKey,
+		assemble, params)
+	if err != nil {
+		sendPrint += fmt.Sprintf(", broadcast failed %s, %s",
+			netTime.Now(), err.Error())
+		errDenote := m.st.failedSend(uuid)
+		if errDenote != nil {
+			sendPrint += fmt.Sprintf(", failed to denote failed "+
+				"broadcast: %s", err.Error())
+			jww.ERROR.Printf("Failed to update for a failed send to "+
+				"%s: %+v", channelID, err)
+		}
+		return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, err
+	}
+	sendPrint += fmt.Sprintf(", broadcast succeeded %s, success!",
+		netTime.Now())
+	err = m.st.send(uuid, msgId, r)
+	if err != nil {
+		sendPrint += fmt.Sprintf(", broadcast failed: %s ", err.Error())
+	}
+	return msgId, r, ephid, err
+}
+
+// SendMessage is used to send a formatted message over a channel.
+// Due to the underlying encoding using compression, it isn't
+// possible to define the largest payload that can be sent, but
+// it will always be possible to send a payload of 798 bytes at minimum
+func (m *manager) SendMessage(channelID *id.ID, msg string,
+	validUntil time.Duration, params cmix.CMIXParams) (
+	cryptoChannel.MessageID, rounds.Round, ephemeral.Id, error) {
+	tag := makeChaDebugTag(channelID, m.me.PubKey, []byte(msg), SendMessageTag)
+	jww.INFO.Printf("[%s]SendMessage(%s)", tag, channelID)
+
+	txt := &CMIXChannelText{
+		Version:        cmixChannelTextVersion,
+		Text:           msg,
+		ReplyMessageID: nil,
+	}
+
+	params = params.SetDebugTag(tag)
+
+	txtMarshaled, err := proto.Marshal(txt)
+	if err != nil {
+		return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, err
+	}
+
+	return m.SendGeneric(channelID, Text, txtMarshaled, validUntil,
+		params)
+}
+
+// SendReply is used to send a formatted message over a channel.
+// Due to the underlying encoding using compression, it isn't
+// possible to define the largest payload that can be sent, but
+// it will always be possible to send a payload of 766 bytes at minimum.
+// If the message ID the reply is sent to doesnt exist, the other side will
+// post the message as a normal message and not a reply.
+func (m *manager) SendReply(channelID *id.ID, msg string,
+	replyTo cryptoChannel.MessageID, validUntil time.Duration,
+	params cmix.CMIXParams) (cryptoChannel.MessageID, rounds.Round,
+	ephemeral.Id, error) {
+	tag := makeChaDebugTag(channelID, m.me.PubKey, []byte(msg), SendReplyTag)
+	jww.INFO.Printf("[%s]SendReply(%s, to %s)", tag, channelID, replyTo)
+	txt := &CMIXChannelText{
+		Version:        cmixChannelTextVersion,
+		Text:           msg,
+		ReplyMessageID: replyTo[:],
+	}
+
+	params = params.SetDebugTag(tag)
+
+	txtMarshaled, err := proto.Marshal(txt)
+	if err != nil {
+		return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, err
+	}
+
+	return m.SendGeneric(channelID, Text, txtMarshaled, validUntil,
+		params)
+}
+
+// SendReaction is used to send a reaction to a message over a channel.
+// The reaction must be a single emoji with no other characters, and will
+// be rejected otherwise.
+// Clients will drop the reaction if they do not recognize the reactTo message
+func (m *manager) SendReaction(channelID *id.ID, reaction string,
+	reactTo cryptoChannel.MessageID, params cmix.CMIXParams) (
+	cryptoChannel.MessageID, rounds.Round, ephemeral.Id, error) {
+	tag := makeChaDebugTag(channelID, m.me.PubKey, []byte(reaction), SendReactionTag)
+	jww.INFO.Printf("[%s]SendReply(%s, to %s)", tag, channelID, reactTo)
+
+	if err := ValidateReaction(reaction); err != nil {
+		return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, err
+	}
+
+	react := &CMIXChannelReaction{
+		Version:           cmixChannelReactionVersion,
+		Reaction:          reaction,
+		ReactionMessageID: reactTo[:],
+	}
+
+	params = params.SetDebugTag(tag)
+
+	reactMarshaled, err := proto.Marshal(react)
+	if err != nil {
+		return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, err
+	}
+
+	return m.SendGeneric(channelID, Reaction, reactMarshaled, ValidForever,
+		params)
+}
+
+// makeChaDebugTag is a debug helper that creates non-unique msg identifier
+// This is set as the debug tag on messages and enables some level
+// of tracing a message (if it's contents/chan/type are unique)
+func makeChaDebugTag(channelID *id.ID, id ed25519.PublicKey,
+	msg []byte, baseTag string) string {
+
+	h, _ := blake2b.New256(nil)
+	h.Write(channelID[:])
+	h.Write(msg)
+	h.Write(id)
+
+	tripcode := base64.RawStdEncoding.EncodeToString(h.Sum(nil))[:12]
+	return fmt.Sprintf("%s-%s", baseTag, tripcode)
+}
diff --git a/channels/sendTracker.go b/channels/sendTracker.go
new file mode 100644
index 0000000000000000000000000000000000000000..f75819fe2ccea74308cce60ee9087a3b1fc40320
--- /dev/null
+++ b/channels/sendTracker.go
@@ -0,0 +1,505 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"encoding/json"
+	"errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/cmix"
+	"gitlab.com/elixxir/client/cmix/identity/receptionID"
+	"gitlab.com/elixxir/client/cmix/rounds"
+	"gitlab.com/elixxir/client/storage/versioned"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	"gitlab.com/elixxir/primitives/states"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/netTime"
+	"sync"
+	"time"
+)
+
+const (
+	sendTrackerStorageKey     = "sendTrackerStorageKey"
+	sendTrackerStorageVersion = 0
+
+	sendTrackerUnsentStorageKey     = "sendTrackerUnsentStorageKey"
+	sendTrackerUnsentStorageVersion = 0
+
+	getRoundResultsTimeout = 60 * time.Second
+	// number of times it will attempt to get round status before the round
+	// is assumed to have failed. Tracking per round does not persist across
+	// runs
+	maxChecks = 3
+
+	oneSecond = 1000 * time.Millisecond
+)
+
+type tracked struct {
+	MsgID     cryptoChannel.MessageID
+	ChannelID *id.ID
+	RoundID   id.Round
+	UUID      uint64
+}
+
+type trackedList struct {
+	List           []*tracked
+	RoundCompleted bool
+}
+
+// the sendTracker tracks outbound messages and denotes when they are delivered
+// to the event model. It also captures incoming messages and in the event they
+// were sent by this user diverts them as status updates on the previously sent
+// messages
+type sendTracker struct {
+	byRound map[id.Round]trackedList
+
+	byMessageID map[cryptoChannel.MessageID]*tracked
+
+	unsent map[uint64]*tracked
+
+	mux sync.RWMutex
+
+	trigger      triggerEventFunc
+	adminTrigger triggerAdminEventFunc
+	updateStatus updateStatusFunc
+
+	net Client
+
+	kv *versioned.KV
+
+	rngSrc *fastRNG.StreamGenerator
+}
+
+// messageReceiveFunc is a function type for sendTracker.MessageReceive so it
+// can be mocked for testing where used
+type messageReceiveFunc func(messageID cryptoChannel.MessageID, r rounds.Round) bool
+
+// loadSendTracker loads a sent tracker, restoring from disk. It will register a
+// function with the cmix client, delayed on when the network goes healthy,
+// which will attempt to discover the status of all rounds that are outstanding.
+func loadSendTracker(net Client, kv *versioned.KV, trigger triggerEventFunc,
+	adminTrigger triggerAdminEventFunc, updateStatus updateStatusFunc,
+	rngSource *fastRNG.StreamGenerator) *sendTracker {
+	st := &sendTracker{
+		byRound:      make(map[id.Round]trackedList),
+		byMessageID:  make(map[cryptoChannel.MessageID]*tracked),
+		unsent:       make(map[uint64]*tracked),
+		trigger:      trigger,
+		adminTrigger: adminTrigger,
+		updateStatus: updateStatus,
+		net:          net,
+		kv:           kv,
+		rngSrc:       rngSource,
+	}
+
+	/*if err := st.load(); !kv.Exists(err){
+		jww.FATAL.Panicf("failed to load sent tracker: %+v", err)
+	}*/
+	st.load()
+
+	//denote all unsent messages as failed and clear
+	for uuid, t := range st.unsent {
+		updateStatus(uuid, t.MsgID,
+			time.Time{}, rounds.Round{}, Failed)
+	}
+	st.unsent = make(map[uint64]*tracked)
+
+	//register to check all outstanding rounds when the network becomes healthy
+	var callBackID uint64
+	callBackID = net.AddHealthCallback(func(f bool) {
+		if !f {
+			return
+		}
+		net.RemoveHealthCallback(callBackID)
+		for rid, oldTracked := range st.byRound {
+
+			if oldTracked.RoundCompleted {
+				continue
+			}
+
+			rr := &roundResults{
+				round: rid,
+				st:    st,
+			}
+			st.net.GetRoundResults(getRoundResultsTimeout, rr.callback, rr.round)
+		}
+	})
+
+	return st
+}
+
+// store writes the list of rounds that have been
+func (st *sendTracker) store() error {
+
+	if err := st.storeSent(); err != nil {
+		return err
+	}
+
+	return st.storeUnsent()
+}
+
+func (st *sendTracker) storeSent() error {
+
+	//save sent messages
+	data, err := json.Marshal(&st.byRound)
+	if err != nil {
+		return err
+	}
+	return st.kv.Set(sendTrackerStorageKey, &versioned.Object{
+		Version:   sendTrackerStorageVersion,
+		Timestamp: netTime.Now(),
+		Data:      data,
+	})
+}
+
+// store writes the list of rounds that have been
+func (st *sendTracker) storeUnsent() error {
+	//save unsent messages
+	data, err := json.Marshal(&st.unsent)
+	if err != nil {
+		return err
+	}
+
+	return st.kv.Set(sendTrackerUnsentStorageKey, &versioned.Object{
+		Version:   sendTrackerUnsentStorageVersion,
+		Timestamp: netTime.Now(),
+		Data:      data,
+	})
+}
+
+// load will get the stored rounds to be checked from disk and builds
+// internal datastructures
+func (st *sendTracker) load() error {
+	obj, err := st.kv.Get(sendTrackerStorageKey, sendTrackerStorageVersion)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(obj.Data, &st.byRound)
+	if err != nil {
+		return err
+	}
+
+	for rid := range st.byRound {
+		roundList := st.byRound[rid].List
+		for j := range roundList {
+			st.byMessageID[roundList[j].MsgID] = roundList[j]
+		}
+	}
+
+	obj, err = st.kv.Get(sendTrackerUnsentStorageKey, sendTrackerUnsentStorageVersion)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(obj.Data, &st.unsent)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// denotePendingSend is called before the pending send. It tracks the send
+// internally and notifies the UI of the send
+func (st *sendTracker) denotePendingSend(channelID *id.ID,
+	umi *userMessageInternal) (uint64, error) {
+	// for a timestamp for the message, use 1 second from now to
+	// approximate the lag due to round submission
+	ts := netTime.Now().Add(oneSecond)
+
+	// create a random message id so there will not be collisions in a database
+	// that requires a unique message ID
+	stream := st.rngSrc.GetStream()
+	umi.messageID = cryptoChannel.MessageID{}
+	num, err := stream.Read(umi.messageID[:])
+	if num != len(umi.messageID[:]) || err != nil {
+		jww.FATAL.Panicf("failed to get a random message ID, read "+
+			"len: %d, err: %+v", num, err)
+	}
+	stream.Close()
+
+	// submit the message to the UI
+	uuid, err := st.trigger(channelID, umi, ts, receptionID.EphemeralIdentity{},
+		rounds.Round{}, Unsent)
+	if err != nil {
+		return 0, err
+	}
+
+	// track the message on disk
+	st.handleDenoteSend(uuid, channelID, umi.messageID,
+		rounds.Round{})
+	return uuid, nil
+}
+
+// denotePendingAdminSend is called before the pending admin send. It tracks the
+// send internally and notifies the UI of the send
+func (st *sendTracker) denotePendingAdminSend(channelID *id.ID,
+	cm *ChannelMessage) (uint64, error) {
+	// for a timestamp for the message, use 1 second from now to
+	// approximate the lag due to round submission
+	ts := netTime.Now().Add(oneSecond)
+
+	// create a random message id so there will not be collisions in a database
+	// that requires a unique message ID
+	stream := st.rngSrc.GetStream()
+	randMid := cryptoChannel.MessageID{}
+	num, err := stream.Read(randMid[:])
+	if num != len(randMid[:]) || err != nil {
+		jww.FATAL.Panicf("failed to get a random message ID, read "+
+			"len: %d, err: %+v", num, err)
+	}
+	stream.Close()
+
+	// submit the message to the UI
+	uuid, err := st.adminTrigger(channelID, cm, ts, randMid,
+		receptionID.EphemeralIdentity{},
+		rounds.Round{}, Unsent)
+
+	if err != nil {
+		return 0, err
+	}
+
+	// track the message on disk
+	st.handleDenoteSend(uuid, channelID, randMid,
+		rounds.Round{})
+	return uuid, nil
+}
+
+// handleDenoteSend does the nity gritty of editing internal structures
+func (st *sendTracker) handleDenoteSend(uuid uint64, channelID *id.ID,
+	messageID cryptoChannel.MessageID, round rounds.Round) {
+	st.mux.Lock()
+	defer st.mux.Unlock()
+
+	//skip if already added
+	_, existsMessage := st.unsent[uuid]
+	if existsMessage {
+		return
+	}
+
+	st.unsent[uuid] = &tracked{messageID, channelID, round.ID, uuid}
+
+	err := st.storeUnsent()
+	if err != nil {
+		jww.FATAL.Panicf(err.Error())
+	}
+}
+
+// send tracks a generic send message
+func (st *sendTracker) send(uuid uint64, msgID cryptoChannel.MessageID,
+	round rounds.Round) error {
+
+	// update the on disk message status
+	t, err := st.handleSend(uuid, msgID, round)
+	if err != nil {
+		return err
+	}
+
+	// Modify the timestamp to reduce the chance message order will be ambiguous
+	ts := mutateTimestamp(round.Timestamps[states.QUEUED], msgID)
+
+	//update the message on the UI
+	go st.updateStatus(t.UUID, msgID, ts, round, Sent)
+	return nil
+}
+
+// send tracks a generic send message
+func (st *sendTracker) failedSend(uuid uint64) error {
+
+	// update the on disk message status
+	t, err := st.handleSendFailed(uuid)
+	if err != nil {
+		return err
+	}
+
+	//update the message on the UI
+	go st.updateStatus(t.UUID, cryptoChannel.MessageID{}, time.Time{}, rounds.Round{}, Failed)
+	return nil
+}
+
+// handleSend does the nity gritty of editing internal structures
+func (st *sendTracker) handleSend(uuid uint64,
+	messageID cryptoChannel.MessageID, round rounds.Round) (*tracked, error) {
+	st.mux.Lock()
+	defer st.mux.Unlock()
+
+	//check if in unsent
+	t, exists := st.unsent[uuid]
+	if !exists {
+		return nil, errors.New("cannot handle send on an unprepared message")
+	}
+
+	_, existsMessage := st.byMessageID[messageID]
+	if existsMessage {
+		return nil, errors.New("cannot handle send on a message which was " +
+			"already sent")
+	}
+
+	t.MsgID = messageID
+	t.RoundID = round.ID
+
+	//add the roundID
+	roundsList, existsRound := st.byRound[round.ID]
+	roundsList.List = append(roundsList.List, t)
+	st.byRound[round.ID] = roundsList
+
+	//add the round
+	st.byMessageID[messageID] = t
+
+	if !existsRound {
+		rr := &roundResults{
+			round: round.ID,
+			st:    st,
+		}
+		st.net.GetRoundResults(getRoundResultsTimeout, rr.callback, rr.round)
+	}
+
+	delete(st.unsent, uuid)
+
+	//store the changed list to disk
+	err := st.store()
+	if err != nil {
+		jww.FATAL.Panicf(err.Error())
+	}
+
+	return t, nil
+}
+
+// handleSendFailed does the nity gritty of editing internal structures
+func (st *sendTracker) handleSendFailed(uuid uint64) (*tracked, error) {
+	st.mux.Lock()
+	defer st.mux.Unlock()
+
+	//check if in unsent
+	t, exists := st.unsent[uuid]
+	if !exists {
+		return nil, errors.New("cannot handle send on an unprepared message")
+	}
+
+	delete(st.unsent, uuid)
+
+	//store the changed list to disk
+	err := st.storeUnsent()
+	if err != nil {
+		jww.FATAL.Panicf(err.Error())
+	}
+
+	return t, nil
+}
+
+// MessageReceive is used when a message is received to check if the message
+// was sent by this user. If it was, the correct signal is sent to the event
+// model and the function returns true, notifying the caller to not process
+// the message
+func (st *sendTracker) MessageReceive(messageID cryptoChannel.MessageID, round rounds.Round) bool {
+	st.mux.RLock()
+
+	//skip if already added
+	_, existsMessage := st.byMessageID[messageID]
+	st.mux.RUnlock()
+	if !existsMessage {
+		return false
+	}
+
+	st.mux.Lock()
+	defer st.mux.Unlock()
+	msgData, existsMessage := st.byMessageID[messageID]
+	if !existsMessage {
+		return false
+	}
+
+	delete(st.byMessageID, messageID)
+
+	roundList := st.byRound[msgData.RoundID]
+	if len(roundList.List) == 1 {
+		delete(st.byRound, msgData.RoundID)
+	} else {
+		newRoundList := make([]*tracked, 0, len(roundList.List)-1)
+		for i := range roundList.List {
+			if !roundList.List[i].MsgID.Equals(messageID) {
+				newRoundList = append(newRoundList, roundList.List[i])
+			}
+		}
+		st.byRound[msgData.RoundID] = trackedList{
+			List:           newRoundList,
+			RoundCompleted: roundList.RoundCompleted,
+		}
+	}
+
+	ts := mutateTimestamp(round.Timestamps[states.QUEUED], messageID)
+	go st.updateStatus(msgData.UUID, messageID, ts,
+		round, Delivered)
+
+	if err := st.storeSent(); err != nil {
+		jww.FATAL.Panicf("failed to store the updated sent list: %+v", err)
+	}
+
+	return true
+}
+
+// roundResults represents a round which results are waiting on from the cmix layer
+type roundResults struct {
+	round     id.Round
+	st        *sendTracker
+	numChecks uint
+}
+
+// callback is called when results are known about a round. it will re-trigger
+// the wait if it fails up to 'maxChecks' times.
+func (rr *roundResults) callback(allRoundsSucceeded, timedOut bool, results map[id.Round]cmix.RoundResult) {
+
+	rr.st.mux.Lock()
+
+	//if the message was already handled, do nothing
+	registered, existsRound := rr.st.byRound[rr.round]
+	if !existsRound {
+		rr.st.mux.Unlock()
+		return
+	}
+
+	status := Delivered
+	if !allRoundsSucceeded {
+		status = Failed
+	}
+
+	if timedOut {
+		if rr.numChecks >= maxChecks {
+			jww.WARN.Printf("Channel messages sent on %d assumed to "+
+				"have failed after %d attempts to get round status", rr.round,
+				maxChecks)
+			status = Failed
+		} else {
+			rr.numChecks++
+
+			rr.st.mux.Unlock()
+
+			//retry if timed out
+			go rr.st.net.GetRoundResults(getRoundResultsTimeout, rr.callback, []id.Round{rr.round}...)
+			return
+		}
+
+	}
+
+	registered.RoundCompleted = true
+	rr.st.byRound[rr.round] = registered
+	if err := rr.st.store(); err != nil {
+		jww.FATAL.Panicf("failed to store update after "+
+			"finalizing delivery of sent messages: %+v", err)
+	}
+
+	rr.st.mux.Unlock()
+	if status == Failed {
+		for i := range registered.List {
+			round := results[rr.round].Round
+			go rr.st.updateStatus(registered.List[i].UUID, registered.List[i].MsgID, time.Time{},
+				round, Failed)
+		}
+	}
+}
diff --git a/channels/sendTracker_test.go b/channels/sendTracker_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..6948df3beee69e5ffeed5a119bda3a9240972131
--- /dev/null
+++ b/channels/sendTracker_test.go
@@ -0,0 +1,354 @@
+package channels
+
+import (
+	"gitlab.com/elixxir/client/cmix"
+	"gitlab.com/elixxir/client/cmix/identity/receptionID"
+	"gitlab.com/elixxir/client/cmix/message"
+	"gitlab.com/elixxir/client/cmix/rounds"
+	"gitlab.com/elixxir/client/storage/versioned"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	"gitlab.com/elixxir/ekv"
+	"gitlab.com/elixxir/primitives/states"
+	"gitlab.com/xx_network/crypto/csprng"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/id/ephemeral"
+	"gitlab.com/xx_network/primitives/netTime"
+	"testing"
+	"time"
+)
+
+type mockClient struct{}
+
+func (mc *mockClient) GetMaxMessageLength() int {
+	return 2048
+}
+func (mc *mockClient) SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler,
+	cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
+	return rounds.Round{}, ephemeral.Id{}, nil
+}
+func (mc *mockClient) IsHealthy() bool {
+	return true
+}
+func (mc *mockClient) AddIdentity(id *id.ID, validUntil time.Time, persistent bool) {}
+func (mc *mockClient) AddService(clientID *id.ID, newService message.Service,
+	response message.Processor) {
+}
+func (mc *mockClient) DeleteClientService(clientID *id.ID) {}
+func (mc *mockClient) RemoveIdentity(id *id.ID)            {}
+func (mc *mockClient) GetRoundResults(timeout time.Duration, roundCallback cmix.RoundEventCallback,
+	roundList ...id.Round) {
+}
+func (mc *mockClient) AddHealthCallback(f func(bool)) uint64 {
+	return 0
+}
+func (mc *mockClient) RemoveHealthCallback(uint64) {}
+
+// Test MessageReceive basic logic
+func TestSendTracker_MessageReceive(t *testing.T) {
+	kv := versioned.NewKV(ekv.MakeMemstore())
+	uuidNum := uint64(0)
+	rid := id.Round(2)
+
+	r := rounds.Round{
+		ID:         rid,
+		Timestamps: make(map[states.Round]time.Time),
+	}
+	r.Timestamps[states.QUEUED] = time.Now()
+	trigger := func(chID *id.ID, umi *userMessageInternal, ts time.Time,
+		receptionID receptionID.EphemeralIdentity, round rounds.Round,
+		status SentStatus) (uint64, error) {
+		oldUUID := uuidNum
+		uuidNum++
+		return oldUUID, nil
+	}
+
+	updateStatus := func(uuid uint64, messageID cryptoChannel.MessageID,
+		timestamp time.Time, round rounds.Round, status SentStatus) {
+	}
+
+	cid := id.NewIdFromString("channel", id.User, t)
+
+	crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG)
+
+	st := loadSendTracker(&mockClient{}, kv, trigger, nil, updateStatus, crng)
+
+	mid := cryptoChannel.MakeMessageID([]byte("hello"), cid)
+	process := st.MessageReceive(mid, r)
+	if process {
+		t.Fatalf("Did not receive expected result from MessageReceive")
+	}
+
+	uuid, err := st.denotePendingSend(cid, &userMessageInternal{
+		userMessage: &UserMessage{},
+		channelMessage: &ChannelMessage{
+			Lease:       netTime.Now().UnixNano(),
+			RoundID:     uint64(rid),
+			PayloadType: 0,
+			Payload:     []byte("hello"),
+		}})
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	err = st.send(uuid, mid, rounds.Round{
+		ID:    rid,
+		State: 1,
+	})
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+	process = st.MessageReceive(mid, r)
+	if !process {
+		t.Fatalf("Did not receive expected result from MessageReceive")
+	}
+
+	cid2 := id.NewIdFromString("channel two", id.User, t)
+	uuid2, err := st.denotePendingSend(cid2, &userMessageInternal{
+		userMessage: &UserMessage{},
+		channelMessage: &ChannelMessage{
+			Lease:       netTime.Now().UnixNano(),
+			RoundID:     uint64(rid),
+			PayloadType: 0,
+			Payload:     []byte("hello again"),
+		}})
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	err = st.send(uuid2, mid, rounds.Round{
+		ID:    rid,
+		State: 1,
+	})
+	process = st.MessageReceive(mid, r)
+	if !process {
+		t.Fatalf("Did not receive expected result from MessageReceive")
+	}
+}
+
+// Test failedSend function, confirming that data is stored appropriately
+// and callbacks are called
+func TestSendTracker_failedSend(t *testing.T) {
+	triggerCh := make(chan SentStatus)
+
+	kv := versioned.NewKV(ekv.MakeMemstore())
+
+	adminTrigger := func(chID *id.ID, cm *ChannelMessage, ts time.Time,
+		messageID cryptoChannel.MessageID, receptionID receptionID.EphemeralIdentity,
+		round rounds.Round, status SentStatus) (uint64, error) {
+		return 0, nil
+	}
+
+	updateStatus := func(uuid uint64, messageID cryptoChannel.MessageID,
+		timestamp time.Time, round rounds.Round, status SentStatus) {
+		triggerCh <- status
+	}
+
+	crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG)
+
+	st := loadSendTracker(&mockClient{}, kv, nil, adminTrigger, updateStatus, crng)
+
+	cid := id.NewIdFromString("channel", id.User, t)
+	mid := cryptoChannel.MakeMessageID([]byte("hello"), cid)
+	rid := id.Round(2)
+	uuid, err := st.denotePendingAdminSend(cid, &ChannelMessage{
+		Lease:       0,
+		RoundID:     uint64(rid),
+		PayloadType: 0,
+		Payload:     []byte("hello"),
+	})
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	err = st.failedSend(uuid)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	timeout := time.NewTicker(time.Second * 5)
+	select {
+	case s := <-triggerCh:
+		if s != Failed {
+			t.Fatalf("Did not receive failed from failed message")
+		}
+		t.Log("Received over trigger chan")
+	case <-timeout.C:
+		t.Fatal("Timed out waiting for trigger chan")
+	}
+
+	trackedRound, ok := st.byRound[rid]
+	if ok {
+		t.Fatal("Should not have found a tracked round")
+	}
+	if len(trackedRound.List) != 0 {
+		t.Fatal("Did not find expected number of trackedRounds")
+	}
+
+	_, ok = st.byMessageID[mid]
+	if ok {
+		t.Error("Should not have found tracked message")
+	}
+
+	_, ok = st.unsent[uuid]
+	if ok {
+		t.Fatal("Should not have found an unsent")
+	}
+}
+
+// Test send tracker send function, confirming that data is stored appropriately
+//// and callbacks are called
+func TestSendTracker_send(t *testing.T) {
+	triggerCh := make(chan bool)
+
+	kv := versioned.NewKV(ekv.MakeMemstore())
+	trigger := func(chID *id.ID, umi *userMessageInternal, ts time.Time,
+		receptionID receptionID.EphemeralIdentity, round rounds.Round, status SentStatus) (uint64, error) {
+		return 0, nil
+	}
+
+	updateStatus := func(uuid uint64, messageID cryptoChannel.MessageID,
+		timestamp time.Time, round rounds.Round, status SentStatus) {
+		triggerCh <- true
+	}
+
+	crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG)
+
+	st := loadSendTracker(&mockClient{}, kv, trigger, nil, updateStatus, crng)
+
+	cid := id.NewIdFromString("channel", id.User, t)
+	mid := cryptoChannel.MakeMessageID([]byte("hello"), cid)
+	rid := id.Round(2)
+	uuid, err := st.denotePendingSend(cid, &userMessageInternal{
+		userMessage: &UserMessage{},
+		channelMessage: &ChannelMessage{
+			Lease:       0,
+			RoundID:     uint64(rid),
+			PayloadType: 0,
+			Payload:     []byte("hello"),
+		},
+		messageID: mid,
+	})
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	err = st.send(uuid, mid, rounds.Round{
+		ID:    rid,
+		State: 2,
+	})
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	timeout := time.NewTicker(time.Second * 5)
+	select {
+	case <-triggerCh:
+		t.Log("Received over trigger chan")
+	case <-timeout.C:
+		t.Fatal("Timed out waiting for trigger chan")
+	}
+
+	trackedRound, ok := st.byRound[rid]
+	if !ok {
+		t.Fatal("Should have found a tracked round")
+	}
+	if len(trackedRound.List) != 1 {
+		t.Fatal("Did not find expected number of trackedRounds")
+	}
+	if trackedRound.List[0].MsgID != mid {
+		t.Fatalf("Did not find expected message ID in trackedRounds")
+	}
+
+	trackedMsg, ok := st.byMessageID[mid]
+	if !ok {
+		t.Error("Should have found tracked message")
+	}
+	if trackedMsg.MsgID != mid {
+		t.Fatalf("Did not find expected message ID in byMessageID")
+	}
+}
+
+// Test loading stored byRound map from storage
+func TestSendTracker_load_store(t *testing.T) {
+	kv := versioned.NewKV(ekv.MakeMemstore())
+
+	crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG)
+
+	st := loadSendTracker(&mockClient{}, kv, nil, nil, nil, crng)
+	cid := id.NewIdFromString("channel", id.User, t)
+	mid := cryptoChannel.MakeMessageID([]byte("hello"), cid)
+	rid := id.Round(2)
+	st.byRound[rid] = trackedList{
+		List:           []*tracked{{MsgID: mid, ChannelID: cid, RoundID: rid}},
+		RoundCompleted: false,
+	}
+	err := st.store()
+	if err != nil {
+		t.Fatalf("Failed to store byRound: %+v", err)
+	}
+
+	st2 := loadSendTracker(&mockClient{}, kv, nil, nil, nil, crng)
+	if len(st2.byRound) != len(st.byRound) {
+		t.Fatalf("byRound was not properly loaded")
+	}
+}
+
+func TestRoundResult_callback(t *testing.T) {
+	kv := versioned.NewKV(ekv.MakeMemstore())
+	triggerCh := make(chan bool)
+	update := func(uuid uint64, messageID cryptoChannel.MessageID,
+		timestamp time.Time, round rounds.Round, status SentStatus) {
+		triggerCh <- true
+	}
+	trigger := func(chID *id.ID, umi *userMessageInternal, ts time.Time,
+		receptionID receptionID.EphemeralIdentity, round rounds.Round,
+		status SentStatus) (uint64, error) {
+		return 0, nil
+	}
+
+	crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG)
+
+	st := loadSendTracker(&mockClient{}, kv, trigger, nil, update, crng)
+
+	cid := id.NewIdFromString("channel", id.User, t)
+	mid := cryptoChannel.MakeMessageID([]byte("hello"), cid)
+	rid := id.Round(2)
+	uuid, err := st.denotePendingSend(cid, &userMessageInternal{
+		userMessage: &UserMessage{},
+		channelMessage: &ChannelMessage{
+			Lease:       0,
+			RoundID:     uint64(rid),
+			PayloadType: 0,
+			Payload:     []byte("hello"),
+		},
+		messageID: mid,
+	})
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	err = st.send(uuid, mid, rounds.Round{
+		ID:    rid,
+		State: 2,
+	})
+
+	rr := roundResults{
+		round:     rid,
+		st:        st,
+		numChecks: 0,
+	}
+
+	rr.callback(true, false, map[id.Round]cmix.RoundResult{rid: {cmix.Succeeded, rounds.Round{
+		ID:    rid,
+		State: 0,
+	}}})
+
+	timeout := time.NewTicker(time.Second * 5)
+	select {
+	case <-triggerCh:
+		t.Log("Received trigger")
+	case <-timeout.C:
+		t.Fatal("Did not receive update")
+	}
+}
diff --git a/channels/send_test.go b/channels/send_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..7c3205115a1947cf3f5a20b81d79f794959d57dd
--- /dev/null
+++ b/channels/send_test.go
@@ -0,0 +1,618 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"bytes"
+	"crypto/ed25519"
+	"math/rand"
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/golang/protobuf/proto"
+	"gitlab.com/elixxir/client/cmix/identity/receptionID"
+	"gitlab.com/elixxir/client/cmix/rounds"
+	"gitlab.com/elixxir/client/storage/versioned"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	"gitlab.com/elixxir/crypto/rsa"
+	"gitlab.com/elixxir/ekv"
+	"gitlab.com/xx_network/crypto/csprng"
+	"gitlab.com/xx_network/primitives/netTime"
+
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/id/ephemeral"
+
+	"gitlab.com/elixxir/client/broadcast"
+	"gitlab.com/elixxir/client/cmix"
+	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
+)
+
+const returnedRound = 42
+
+type mockBroadcastChannel struct {
+	hasRun bool
+
+	payload []byte
+	params  cmix.CMIXParams
+
+	pk rsa.PrivateKey
+
+	crypto *cryptoBroadcast.Channel
+}
+
+func (m *mockBroadcastChannel) MaxPayloadSize() int {
+	return 1024
+}
+
+func (m *mockBroadcastChannel) MaxRSAToPublicPayloadSize() int {
+	return 512
+}
+
+func (m *mockBroadcastChannel) Get() *cryptoBroadcast.Channel {
+	return m.crypto
+}
+
+func (m *mockBroadcastChannel) Broadcast(payload []byte, cMixParams cmix.CMIXParams) (
+	rounds.Round, ephemeral.Id, error) {
+
+	m.hasRun = true
+
+	m.payload = payload
+	m.params = cMixParams
+
+	return rounds.Round{ID: 123}, ephemeral.Id{}, nil
+}
+
+func (m *mockBroadcastChannel) BroadcastWithAssembler(assembler broadcast.Assembler, cMixParams cmix.CMIXParams) (
+	rounds.Round, ephemeral.Id, error) {
+	m.hasRun = true
+
+	var err error
+
+	m.payload, err = assembler(returnedRound)
+	m.params = cMixParams
+
+	return rounds.Round{ID: 123}, ephemeral.Id{}, err
+}
+
+func (m *mockBroadcastChannel) BroadcastRSAtoPublic(pk rsa.PrivateKey, payload []byte,
+	cMixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
+	m.hasRun = true
+
+	m.payload = payload
+	m.params = cMixParams
+
+	m.pk = pk
+	return rounds.Round{ID: 123}, ephemeral.Id{}, nil
+}
+
+func (m *mockBroadcastChannel) BroadcastRSAToPublicWithAssembler(
+	pk rsa.PrivateKey, assembler broadcast.Assembler,
+	cMixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
+
+	m.hasRun = true
+
+	var err error
+
+	m.payload, err = assembler(returnedRound)
+	m.params = cMixParams
+
+	m.pk = pk
+
+	return rounds.Round{ID: 123}, ephemeral.Id{}, err
+}
+
+func (m *mockBroadcastChannel) RegisterListener(listenerCb broadcast.ListenerFunc, method broadcast.Method) error {
+	return nil
+}
+
+func (m *mockBroadcastChannel) Stop() {
+}
+
+type mockNameService struct {
+	validChMsg bool
+}
+
+func (m *mockNameService) GetUsername() string {
+	return "Alice"
+}
+
+func (m *mockNameService) GetChannelValidationSignature() (signature []byte, lease time.Time) {
+	return []byte("fake validation sig"), netTime.Now()
+}
+
+func (m *mockNameService) GetChannelPubkey() ed25519.PublicKey {
+	return []byte("fake pubkey")
+}
+
+func (m *mockNameService) SignChannelMessage(message []byte) (signature []byte, err error) {
+	return []byte("fake sig"), nil
+}
+
+func (m *mockNameService) ValidateChannelMessage(username string, lease time.Time,
+	pubKey ed25519.PublicKey, authorIDSignature []byte) bool {
+	return m.validChMsg
+}
+
+func TestSendGeneric(t *testing.T) {
+
+	nameService := new(mockNameService)
+	nameService.validChMsg = true
+
+	rng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(rng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG)
+
+	m := &manager{
+		me:       pi,
+		channels: make(map[id.ID]*joinedChannel),
+		mux:      sync.RWMutex{},
+		rng:      fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG),
+		nicknameManager: &nicknameManager{
+			byChannel: make(map[id.ID]string),
+			kv:        nil,
+		},
+		st: loadSendTracker(&mockBroadcastClient{},
+			versioned.NewKV(ekv.MakeMemstore()), func(chID *id.ID,
+				umi *userMessageInternal, ts time.Time,
+				receptionID receptionID.EphemeralIdentity,
+				round rounds.Round, status SentStatus) (uint64, error) {
+				return 0, nil
+			}, func(chID *id.ID, cm *ChannelMessage, ts time.Time,
+				messageID cryptoChannel.MessageID, receptionID receptionID.EphemeralIdentity,
+				round rounds.Round, status SentStatus) (uint64, error) {
+				return 0, nil
+			}, func(uuid uint64, messageID cryptoChannel.MessageID,
+				timestamp time.Time, round rounds.Round, status SentStatus) {
+			}, crng),
+	}
+
+	channelID := new(id.ID)
+	messageType := Text
+	msg := []byte("hello world")
+	validUntil := time.Hour
+	params := new(cmix.CMIXParams)
+
+	mbc := &mockBroadcastChannel{}
+
+	m.channels[*channelID] = &joinedChannel{
+		broadcast: mbc,
+	}
+
+	messageId, roundId, ephemeralId, err := m.SendGeneric(
+		channelID,
+		messageType,
+		msg,
+		validUntil,
+		*params)
+	if err != nil {
+		t.Logf("ERROR %v", err)
+		t.Fail()
+	}
+	t.Logf("messageId %v, roundId %v, ephemeralId %v", messageId, roundId, ephemeralId)
+
+	// verify the message was handled correctly
+
+	// decode the user message
+	umi, err := unmarshalUserMessageInternal(mbc.payload, channelID)
+	if err != nil {
+		t.Fatalf("Failed to decode the user message: %s", err)
+	}
+
+	// do checks of the data
+	if !umi.GetMessageID().Equals(messageId) {
+		t.Errorf("The message IDs do not match. %s vs %s ",
+			umi.messageID, messageId)
+	}
+
+	if !bytes.Equal(umi.GetChannelMessage().Payload, msg) {
+		t.Errorf("The payload does not match. %s vs %s ",
+			umi.GetChannelMessage().Payload, msg)
+	}
+
+	if MessageType(umi.GetChannelMessage().PayloadType) != messageType {
+		t.Fatalf("Message types do not match, %s vs %s",
+			MessageType(umi.GetChannelMessage().PayloadType), messageType)
+	}
+
+	if umi.GetChannelMessage().RoundID != returnedRound {
+		t.Errorf("The returned round is incorrect, %d vs %d",
+			umi.GetChannelMessage().RoundID, returnedRound)
+	}
+
+}
+
+func TestAdminGeneric(t *testing.T) {
+
+	prng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(prng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG)
+
+	m := &manager{
+		channels: make(map[id.ID]*joinedChannel),
+		nicknameManager: &nicknameManager{
+			byChannel: make(map[id.ID]string),
+			kv:        nil,
+		},
+		me:  pi,
+		rng: fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG),
+		st: loadSendTracker(&mockBroadcastClient{},
+			versioned.NewKV(ekv.MakeMemstore()), func(chID *id.ID,
+				umi *userMessageInternal, ts time.Time,
+				receptionID receptionID.EphemeralIdentity,
+				round rounds.Round, status SentStatus) (uint64, error) {
+				return 0, nil
+			}, func(chID *id.ID, cm *ChannelMessage, ts time.Time,
+				messageID cryptoChannel.MessageID, receptionID receptionID.EphemeralIdentity,
+				round rounds.Round, status SentStatus) (uint64, error) {
+				return 0, nil
+			}, func(uuid uint64, messageID cryptoChannel.MessageID,
+				timestamp time.Time, round rounds.Round, status SentStatus) {
+			}, crng),
+	}
+
+	messageType := Text
+	msg := []byte("hello world")
+	validUntil := time.Hour
+
+	rng := &csprng.SystemRNG{}
+	ch, priv, err := cryptoBroadcast.NewChannel(
+		"test", "test", cryptoBroadcast.Public, 1000, rng)
+	if err != nil {
+		t.Fatalf("Failed to generate channel: %+v", err)
+	}
+
+	mbc := &mockBroadcastChannel{crypto: ch}
+
+	m.channels[*ch.ReceptionID] = &joinedChannel{
+		broadcast: mbc,
+	}
+
+	messageId, roundId, ephemeralId, err := m.SendAdminGeneric(priv,
+		ch.ReceptionID, messageType, msg, validUntil,
+		cmix.GetDefaultCMIXParams())
+	if err != nil {
+		t.Fatalf("Failed to SendAdminGeneric: %v", err)
+	}
+	t.Logf("messageId %v, roundId %v, ephemeralId %v", messageId, roundId, ephemeralId)
+
+	// verify the message was handled correctly
+
+	msgID := cryptoChannel.MakeMessageID(mbc.payload, ch.ReceptionID)
+
+	if !msgID.Equals(messageId) {
+		t.Errorf("The message IDs do not match. %s vs %s ",
+			msgID, messageId)
+	}
+
+	// decode the channel message
+	chMgs := &ChannelMessage{}
+	err = proto.Unmarshal(mbc.payload, chMgs)
+	if err != nil {
+		t.Fatalf("Failed to decode the channel message: %s", err)
+	}
+
+	if !bytes.Equal(chMgs.Payload, msg) {
+		t.Errorf("Messages do not match, %s vs %s", chMgs.Payload, msg)
+	}
+
+	if MessageType(chMgs.PayloadType) != messageType {
+		t.Errorf("Message types do not match, %s vs %s",
+			MessageType(chMgs.PayloadType), messageType)
+	}
+
+	if chMgs.RoundID != returnedRound {
+		t.Errorf("The returned round is incorrect, %d vs %d",
+			chMgs.RoundID, returnedRound)
+	}
+}
+
+func TestSendMessage(t *testing.T) {
+
+	nameService := new(mockNameService)
+	nameService.validChMsg = true
+
+	prng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(prng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG)
+
+	m := &manager{
+		me:       pi,
+		channels: make(map[id.ID]*joinedChannel),
+		nicknameManager: &nicknameManager{
+			byChannel: make(map[id.ID]string),
+			kv:        nil,
+		},
+		rng: fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG),
+		st: loadSendTracker(&mockBroadcastClient{},
+			versioned.NewKV(ekv.MakeMemstore()), func(chID *id.ID,
+				umi *userMessageInternal, ts time.Time,
+				receptionID receptionID.EphemeralIdentity,
+				round rounds.Round, status SentStatus) (uint64, error) {
+				return 0, nil
+			}, func(chID *id.ID, cm *ChannelMessage, ts time.Time,
+				messageID cryptoChannel.MessageID, receptionID receptionID.EphemeralIdentity,
+				round rounds.Round, status SentStatus) (uint64, error) {
+				return 0, nil
+			}, func(uuid uint64, messageID cryptoChannel.MessageID,
+				timestamp time.Time, round rounds.Round, status SentStatus) {
+			}, crng),
+	}
+
+	channelID := new(id.ID)
+	messageType := Text
+	msg := "hello world"
+	validUntil := time.Hour
+	params := new(cmix.CMIXParams)
+
+	mbc := &mockBroadcastChannel{}
+
+	m.channels[*channelID] = &joinedChannel{
+		broadcast: mbc,
+	}
+
+	messageId, roundId, ephemeralId, err := m.SendMessage(
+		channelID,
+		msg,
+		validUntil,
+		*params)
+	if err != nil {
+		t.Logf("ERROR %v", err)
+		t.Fail()
+	}
+	t.Logf("messageId %v, roundId %v, ephemeralId %v", messageId, roundId, ephemeralId)
+
+	// verify the message was handled correctly
+
+	// decode the user message
+	umi, err := unmarshalUserMessageInternal(mbc.payload, channelID)
+	if err != nil {
+		t.Fatalf("Failed to decode the user message: %s", err)
+	}
+
+	// do checks of the data
+	if !umi.GetMessageID().Equals(messageId) {
+		t.Errorf("The message IDs do not match. %s vs %s ",
+			umi.messageID, messageId)
+	}
+
+	if MessageType(umi.GetChannelMessage().PayloadType) != messageType {
+		t.Fatalf("Message types do not match, %s vs %s",
+			MessageType(umi.GetChannelMessage().PayloadType), messageType)
+	}
+
+	if umi.GetChannelMessage().RoundID != returnedRound {
+		t.Errorf("The returned round is incorrect, %d vs %d",
+			umi.GetChannelMessage().RoundID, returnedRound)
+	}
+
+	// decode the text message
+	txt := &CMIXChannelText{}
+	err = proto.Unmarshal(umi.GetChannelMessage().Payload, txt)
+	if err != nil {
+		t.Fatalf("Could not decode cmix channel text: %s", err)
+	}
+
+	if txt.Text != msg {
+		t.Errorf("Content of message is incorrect: %s vs %s", txt.Text, msg)
+	}
+
+	if txt.ReplyMessageID != nil {
+		t.Errorf("Reply ID on a text message is not nil")
+	}
+}
+
+func TestSendReply(t *testing.T) {
+
+	prng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(prng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG)
+
+	m := &manager{
+		me:       pi,
+		channels: make(map[id.ID]*joinedChannel),
+		nicknameManager: &nicknameManager{
+			byChannel: make(map[id.ID]string),
+			kv:        nil,
+		},
+		rng: fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG),
+		st: loadSendTracker(&mockBroadcastClient{},
+			versioned.NewKV(ekv.MakeMemstore()), func(chID *id.ID,
+				umi *userMessageInternal, ts time.Time,
+				receptionID receptionID.EphemeralIdentity,
+				round rounds.Round, status SentStatus) (uint64, error) {
+				return 0, nil
+			}, func(chID *id.ID, cm *ChannelMessage, ts time.Time,
+				messageID cryptoChannel.MessageID, receptionID receptionID.EphemeralIdentity,
+				round rounds.Round, status SentStatus) (uint64, error) {
+				return 0, nil
+			}, func(uuid uint64, messageID cryptoChannel.MessageID,
+				timestamp time.Time, round rounds.Round, status SentStatus) {
+			}, crng),
+	}
+
+	channelID := new(id.ID)
+	messageType := Text
+	msg := "hello world"
+	validUntil := time.Hour
+	params := new(cmix.CMIXParams)
+
+	replyMsgID := cryptoChannel.MessageID{}
+	replyMsgID[0] = 69
+
+	mbc := &mockBroadcastChannel{}
+
+	m.channels[*channelID] = &joinedChannel{
+		broadcast: mbc,
+	}
+
+	messageId, roundId, ephemeralId, err := m.SendReply(
+		channelID, msg, replyMsgID, validUntil, *params)
+	if err != nil {
+		t.Logf("ERROR %v", err)
+		t.Fail()
+	}
+	t.Logf("messageId %v, roundId %v, ephemeralId %v", messageId, roundId, ephemeralId)
+
+	// verify the message was handled correctly
+
+	// decode the user message
+	umi, err := unmarshalUserMessageInternal(mbc.payload, channelID)
+	if err != nil {
+		t.Fatalf("Failed to decode the user message: %s", err)
+	}
+
+	// do checks of the data
+	if !umi.GetMessageID().Equals(messageId) {
+		t.Errorf("The message IDs do not match. %s vs %s ",
+			umi.messageID, messageId)
+	}
+
+	if MessageType(umi.GetChannelMessage().PayloadType) != messageType {
+		t.Fatalf("Message types do not match, %s vs %s",
+			MessageType(umi.GetChannelMessage().PayloadType), messageType)
+	}
+
+	if umi.GetChannelMessage().RoundID != returnedRound {
+		t.Errorf("The returned round is incorrect, %d vs %d",
+			umi.GetChannelMessage().RoundID, returnedRound)
+	}
+
+	// decode the text message
+	txt := &CMIXChannelText{}
+	err = proto.Unmarshal(umi.GetChannelMessage().Payload, txt)
+	if err != nil {
+		t.Fatalf("Could not decode cmix channel text: %s", err)
+	}
+
+	if txt.Text != msg {
+		t.Errorf("Content of message is incorrect: %s vs %s", txt.Text, msg)
+	}
+
+	if !bytes.Equal(txt.ReplyMessageID, replyMsgID[:]) {
+		t.Errorf("The reply message ID is not what was passed in")
+	}
+}
+
+func TestSendReaction(t *testing.T) {
+
+	prng := rand.New(rand.NewSource(64))
+
+	pi, err := cryptoChannel.GenerateIdentity(prng)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG)
+
+	m := &manager{
+		me: pi,
+		nicknameManager: &nicknameManager{
+			byChannel: make(map[id.ID]string),
+			kv:        nil,
+		},
+		rng:      fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG),
+		channels: make(map[id.ID]*joinedChannel),
+		st: loadSendTracker(&mockBroadcastClient{},
+			versioned.NewKV(ekv.MakeMemstore()), func(chID *id.ID,
+				umi *userMessageInternal, ts time.Time,
+				receptionID receptionID.EphemeralIdentity,
+				round rounds.Round, status SentStatus) (uint64, error) {
+				return 0, nil
+			}, func(chID *id.ID, cm *ChannelMessage, ts time.Time,
+				messageID cryptoChannel.MessageID, receptionID receptionID.EphemeralIdentity,
+				round rounds.Round, status SentStatus) (uint64, error) {
+				return 0, nil
+			}, func(uuid uint64, messageID cryptoChannel.MessageID,
+				timestamp time.Time, round rounds.Round, status SentStatus) {
+			}, crng),
+	}
+
+	channelID := new(id.ID)
+	messageType := Reaction
+	msg := "🍆"
+	params := new(cmix.CMIXParams)
+
+	replyMsgID := cryptoChannel.MessageID{}
+	replyMsgID[0] = 69
+
+	mbc := &mockBroadcastChannel{}
+
+	m.channels[*channelID] = &joinedChannel{
+		broadcast: mbc,
+	}
+
+	messageId, roundId, ephemeralId, err := m.SendReaction(
+		channelID, msg, replyMsgID, *params)
+	if err != nil {
+		t.Logf("ERROR %v", err)
+		t.Fail()
+	}
+	t.Logf("messageId %v, roundId %v, ephemeralId %v", messageId, roundId, ephemeralId)
+
+	// verify the message was handled correctly
+
+	// decode the user message
+	umi, err := unmarshalUserMessageInternal(mbc.payload, channelID)
+	if err != nil {
+		t.Fatalf("Failed to decode the user message: %s", err)
+	}
+
+	// do checks of the data
+	if !umi.GetMessageID().Equals(messageId) {
+		t.Errorf("The message IDs do not match. %s vs %s ",
+			umi.messageID, messageId)
+	}
+
+	if MessageType(umi.GetChannelMessage().PayloadType) != messageType {
+		t.Fatalf("Message types do not match, %s vs %s",
+			MessageType(umi.GetChannelMessage().PayloadType), messageType)
+	}
+
+	if umi.GetChannelMessage().RoundID != returnedRound {
+		t.Errorf("The returned round is incorrect, %d vs %d",
+			umi.GetChannelMessage().RoundID, returnedRound)
+	}
+
+	// decode the text message
+	txt := &CMIXChannelReaction{}
+	err = proto.Unmarshal(umi.GetChannelMessage().Payload, txt)
+	if err != nil {
+		t.Fatalf("Could not decode cmix channel text: %s", err)
+	}
+
+	if txt.Reaction != msg {
+		t.Errorf("Content of message is incorrect: %s vs %s", txt.Reaction, msg)
+	}
+
+	if !bytes.Equal(txt.ReactionMessageID, replyMsgID[:]) {
+		t.Errorf("The reply message ID is not what was passed in")
+	}
+}
diff --git a/channels/text.pb.go b/channels/text.pb.go
new file mode 100644
index 0000000000000000000000000000000000000000..5eea7e0ed48b0a5decd88d14d002fbf24c03b39a
--- /dev/null
+++ b/channels/text.pb.go
@@ -0,0 +1,259 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// 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.27.1
+// 	protoc        v3.15.6
+// source: text.proto
+
+package channels
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+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)
+)
+
+// CMIXChannelText is the payload for sending normal text messages to channels
+// the replyMessageID is nil when it is not a reply
+type CMIXChannelText struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Version        uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
+	Text           string `protobuf:"bytes,2,opt,name=text,proto3" json:"text,omitempty"`
+	ReplyMessageID []byte `protobuf:"bytes,3,opt,name=replyMessageID,proto3" json:"replyMessageID,omitempty"`
+}
+
+func (x *CMIXChannelText) Reset() {
+	*x = CMIXChannelText{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_text_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CMIXChannelText) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CMIXChannelText) ProtoMessage() {}
+
+func (x *CMIXChannelText) ProtoReflect() protoreflect.Message {
+	mi := &file_text_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)
+}
+
+// Deprecated: Use CMIXChannelText.ProtoReflect.Descriptor instead.
+func (*CMIXChannelText) Descriptor() ([]byte, []int) {
+	return file_text_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *CMIXChannelText) GetVersion() uint32 {
+	if x != nil {
+		return x.Version
+	}
+	return 0
+}
+
+func (x *CMIXChannelText) GetText() string {
+	if x != nil {
+		return x.Text
+	}
+	return ""
+}
+
+func (x *CMIXChannelText) GetReplyMessageID() []byte {
+	if x != nil {
+		return x.ReplyMessageID
+	}
+	return nil
+}
+
+// CMIXChannelReaction is the payload for reactions. The reaction must be a
+// single emoji and the reactionMessageID must be non nil and a real message
+// in the channel
+type CMIXChannelReaction struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Version           uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
+	Reaction          string `protobuf:"bytes,2,opt,name=reaction,proto3" json:"reaction,omitempty"`
+	ReactionMessageID []byte `protobuf:"bytes,3,opt,name=reactionMessageID,proto3" json:"reactionMessageID,omitempty"`
+}
+
+func (x *CMIXChannelReaction) Reset() {
+	*x = CMIXChannelReaction{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_text_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CMIXChannelReaction) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CMIXChannelReaction) ProtoMessage() {}
+
+func (x *CMIXChannelReaction) ProtoReflect() protoreflect.Message {
+	mi := &file_text_proto_msgTypes[1]
+	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)
+}
+
+// Deprecated: Use CMIXChannelReaction.ProtoReflect.Descriptor instead.
+func (*CMIXChannelReaction) Descriptor() ([]byte, []int) {
+	return file_text_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *CMIXChannelReaction) GetVersion() uint32 {
+	if x != nil {
+		return x.Version
+	}
+	return 0
+}
+
+func (x *CMIXChannelReaction) GetReaction() string {
+	if x != nil {
+		return x.Reaction
+	}
+	return ""
+}
+
+func (x *CMIXChannelReaction) GetReactionMessageID() []byte {
+	if x != nil {
+		return x.ReactionMessageID
+	}
+	return nil
+}
+
+var File_text_proto protoreflect.FileDescriptor
+
+var file_text_proto_rawDesc = []byte{
+	0x0a, 0x0a, 0x74, 0x65, 0x78, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x63, 0x68,
+	0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x22, 0x67, 0x0a, 0x0f, 0x43, 0x4d, 0x49, 0x58, 0x43, 0x68,
+	0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x54, 0x65, 0x78, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72,
+	0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73,
+	0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x70, 0x6c, 0x79,
+	0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52,
+	0x0e, 0x72, 0x65, 0x70, 0x6c, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x49, 0x44, 0x22,
+	0x79, 0x0a, 0x13, 0x43, 0x4d, 0x49, 0x58, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65,
+	0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f,
+	0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
+	0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2c, 0x0a, 0x11,
+	0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x49,
+	0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f,
+	0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x49, 0x44, 0x42, 0x24, 0x5a, 0x22, 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, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73,
+	0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_text_proto_rawDescOnce sync.Once
+	file_text_proto_rawDescData = file_text_proto_rawDesc
+)
+
+func file_text_proto_rawDescGZIP() []byte {
+	file_text_proto_rawDescOnce.Do(func() {
+		file_text_proto_rawDescData = protoimpl.X.CompressGZIP(file_text_proto_rawDescData)
+	})
+	return file_text_proto_rawDescData
+}
+
+var file_text_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_text_proto_goTypes = []interface{}{
+	(*CMIXChannelText)(nil),     // 0: channels.CMIXChannelText
+	(*CMIXChannelReaction)(nil), // 1: channels.CMIXChannelReaction
+}
+var file_text_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_text_proto_init() }
+func file_text_proto_init() {
+	if File_text_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_text_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*CMIXChannelText); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_text_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*CMIXChannelReaction); 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_text_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   2,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_text_proto_goTypes,
+		DependencyIndexes: file_text_proto_depIdxs,
+		MessageInfos:      file_text_proto_msgTypes,
+	}.Build()
+	File_text_proto = out.File
+	file_text_proto_rawDesc = nil
+	file_text_proto_goTypes = nil
+	file_text_proto_depIdxs = nil
+}
diff --git a/channels/text.proto b/channels/text.proto
new file mode 100644
index 0000000000000000000000000000000000000000..26cc59ca76ed8b888b9969730ad9fe2e1fe80d8b
--- /dev/null
+++ b/channels/text.proto
@@ -0,0 +1,29 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+syntax = "proto3";
+
+option go_package = "gitlab.com/elixxir/client/channels";
+
+package channels;
+
+// CMIXChannelText is the payload for sending normal text messages to channels
+// the replyMessageID is nil when it is not a reply
+message CMIXChannelText {
+  uint32 version = 1;
+  string text = 2;
+  bytes replyMessageID = 3;
+}
+
+// CMIXChannelReaction is the payload for reactions. The reaction must be a
+// single emoji and the reactionMessageID must be non nil and a real message
+// in the channel
+message CMIXChannelReaction {
+  uint32 version = 1;
+  string reaction = 2;
+  bytes reactionMessageID = 3;
+}
\ No newline at end of file
diff --git a/channels/userListener.go b/channels/userListener.go
new file mode 100644
index 0000000000000000000000000000000000000000..202e5fa7d93b4ba15485cc2549806e0b8b014dea
--- /dev/null
+++ b/channels/userListener.go
@@ -0,0 +1,82 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"crypto/ed25519"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/cmix/identity/receptionID"
+	"gitlab.com/elixxir/client/cmix/rounds"
+	"gitlab.com/elixxir/primitives/states"
+	"gitlab.com/xx_network/primitives/id"
+	"time"
+)
+
+// the userListener adheres to the [broadcast.ListenerFunc] interface and is
+// used when user messages are received on the channel
+type userListener struct {
+	name      NameService
+	chID      *id.ID
+	trigger   triggerEventFunc
+	checkSent messageReceiveFunc
+}
+
+// Listen is called when a message is received for the user listener
+func (ul *userListener) Listen(payload []byte,
+	receptionID receptionID.EphemeralIdentity, round rounds.Round) {
+
+	//Decode the message as a user message
+	umi, err := unmarshalUserMessageInternal(payload, ul.chID)
+	if err != nil {
+		jww.WARN.Printf("Failed to unmarshal User Message on "+
+			"channel %s", ul.chID)
+		return
+	}
+
+	um := umi.GetUserMessage()
+	cm := umi.GetChannelMessage()
+	msgID := umi.GetMessageID()
+
+	//check if we sent the message, ignore triggering if we sent
+	if ul.checkSent(msgID, round) {
+		return
+	}
+
+	/*CRYPTOGRAPHICALLY RELEVANT CHECKS*/
+
+	// check the round to ensure the message is not a replay
+	if id.Round(cm.RoundID) != round.ID {
+		jww.WARN.Printf("The round message %s send on %d referenced "+
+			"(%d) was not the same as the round the message was found on (%d)",
+			msgID, ul.chID, cm.RoundID, round.ID)
+		return
+	}
+
+	// check that the user properly signed the message
+	if !ed25519.Verify(um.ECCPublicKey, um.Message, um.Signature) {
+		jww.WARN.Printf("Message %s on channel %s purportedly from %s "+
+			"failed its user signature with signature %v", msgID,
+			ul.chID, cm.Nickname, um.Signature)
+		return
+	}
+
+	// Replace the timestamp on the message if it is outside of the
+	// allowable range
+	ts := vetTimestamp(time.Unix(0, cm.LocalTimestamp), round.Timestamps[states.QUEUED], msgID)
+
+	//TODO: Processing of the message relative to admin commands will be here
+
+	//Submit the message to the event model for listening
+	if uuid, err := ul.trigger(ul.chID, umi, ts, receptionID, round,
+		Delivered); err != nil {
+		jww.WARN.Printf("Error in passing off trigger for "+
+			"message (UUID: %d): %+v", uuid, err)
+	}
+
+	return
+}
diff --git a/channels/userListener_test.go b/channels/userListener_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..23d8ae8c1188ef7c717680f4b6a980dace81e0cc
--- /dev/null
+++ b/channels/userListener_test.go
@@ -0,0 +1,358 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package channels
+
+import (
+	"bytes"
+	"crypto/ed25519"
+	"gitlab.com/xx_network/primitives/netTime"
+	"math/rand"
+	"testing"
+	"time"
+
+	"github.com/golang/protobuf/proto"
+
+	"gitlab.com/elixxir/client/cmix/identity/receptionID"
+	"gitlab.com/elixxir/client/cmix/rounds"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/primitives/states"
+	"gitlab.com/xx_network/primitives/id"
+)
+
+type triggerEventDummy struct {
+	gotData bool
+
+	chID        *id.ID
+	umi         *userMessageInternal
+	msgID       cryptoChannel.MessageID
+	receptionID receptionID.EphemeralIdentity
+	round       rounds.Round
+}
+
+func (ted *triggerEventDummy) triggerEvent(chID *id.ID, umi *userMessageInternal,
+	ts time.Time, receptionID receptionID.EphemeralIdentity, round rounds.Round,
+	sent SentStatus) (uint64, error) {
+	ted.gotData = true
+
+	ted.chID = chID
+	ted.umi = umi
+	ted.receptionID = receptionID
+	ted.round = round
+	ted.msgID = umi.GetMessageID()
+
+	return 0, nil
+}
+
+// Tests the happy path
+func TestUserListener_Listen(t *testing.T) {
+
+	//build inputs
+	chID := &id.ID{}
+	chID[0] = 1
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	rng := rand.New(rand.NewSource(42))
+	pub, priv, err := ed25519.GenerateKey(rng)
+	if err != nil {
+		t.Fatalf("failed to generate ed25519 keypair, cant run test")
+	}
+
+	cm := &ChannelMessage{
+		Lease:       int64(time.Hour),
+		RoundID:     uint64(r.ID),
+		PayloadType: 42,
+		Payload:     []byte("blarg"),
+	}
+
+	cmSerial, err := proto.Marshal(cm)
+	if err != nil {
+		t.Fatalf("Failed to marshal proto: %+v", err)
+	}
+
+	msgID := cryptoChannel.MakeMessageID(cmSerial, chID)
+
+	sig := ed25519.Sign(priv, cmSerial)
+	ns := &mockNameService{validChMsg: true}
+
+	um := &UserMessage{
+		Message:      cmSerial,
+		Signature:    sig,
+		ECCPublicKey: pub,
+	}
+
+	umSerial, err := proto.Marshal(um)
+	if err != nil {
+		t.Fatalf("Failed to marshal proto: %+v", err)
+	}
+
+	//build the listener
+	dummy := &triggerEventDummy{}
+
+	al := userListener{
+		chID:      chID,
+		name:      ns,
+		trigger:   dummy.triggerEvent,
+		checkSent: func(messageID cryptoChannel.MessageID, r rounds.Round) bool { return false },
+	}
+
+	//call the listener
+	al.Listen(umSerial, receptionID.EphemeralIdentity{}, r)
+
+	//check the results
+	if !dummy.gotData {
+		t.Fatalf("No data returned after valid listen")
+	}
+
+	if !dummy.chID.Cmp(chID) {
+		t.Errorf("Channel ID not correct: %s vs %s", dummy.chID, chID)
+	}
+
+	if !bytes.Equal(um.Message, dummy.umi.userMessage.Message) {
+		t.Errorf("message not correct: %s vs %s", um.Message,
+			dummy.umi.userMessage.Message)
+	}
+
+	if !msgID.Equals(dummy.msgID) {
+		t.Errorf("messageIDs not correct: %s vs %s", msgID,
+			dummy.msgID)
+	}
+
+	if r.ID != dummy.round.ID {
+		t.Errorf("rounds not correct: %s vs %s", r.ID,
+			dummy.round.ID)
+	}
+}
+
+//tests that the message is rejected when the user signature is invalid
+func TestUserListener_Listen_BadUserSig(t *testing.T) {
+
+	//build inputs
+	chID := &id.ID{}
+	chID[0] = 1
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	rng := rand.New(rand.NewSource(42))
+	pub, _, err := ed25519.GenerateKey(rng)
+	if err != nil {
+		t.Fatalf("failed to generate ed25519 keypair, cant run test")
+	}
+
+	cm := &ChannelMessage{
+		Lease:       int64(time.Hour),
+		RoundID:     uint64(r.ID),
+		PayloadType: 42,
+		Payload:     []byte("blarg"),
+	}
+
+	cmSerial, err := proto.Marshal(cm)
+	if err != nil {
+		t.Fatalf("Failed to marshal proto: %+v", err)
+	}
+
+	_, badpriv, err := ed25519.GenerateKey(rng)
+	if err != nil {
+		t.Fatalf("failed to generate ed25519 keypair, cant run test")
+	}
+
+	sig := ed25519.Sign(badpriv, cmSerial)
+	ns := &mockNameService{validChMsg: true}
+
+	um := &UserMessage{
+		Message:      cmSerial,
+		Signature:    sig,
+		ECCPublicKey: pub,
+	}
+
+	umSerial, err := proto.Marshal(um)
+	if err != nil {
+		t.Fatalf("Failed to marshal proto: %+v", err)
+	}
+
+	//build the listener
+	dummy := &triggerEventDummy{}
+
+	al := userListener{
+		chID:      chID,
+		name:      ns,
+		trigger:   dummy.triggerEvent,
+		checkSent: func(messageID cryptoChannel.MessageID, r rounds.Round) bool { return false },
+	}
+
+	//call the listener
+	al.Listen(umSerial, receptionID.EphemeralIdentity{}, r)
+
+	//check the results
+	if dummy.gotData {
+		t.Fatalf("Data returned after invalid listen")
+	}
+}
+
+//tests that the message is rejected when the round in the message does not
+//match the round passed in
+func TestUserListener_Listen_BadRound(t *testing.T) {
+
+	//build inputs
+	chID := &id.ID{}
+	chID[0] = 1
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	rng := rand.New(rand.NewSource(42))
+	pub, priv, err := ed25519.GenerateKey(rng)
+	if err != nil {
+		t.Fatalf("failed to generate ed25519 keypair, cant run test")
+	}
+
+	cm := &ChannelMessage{
+		Lease: int64(time.Hour),
+		//make the round not match
+		RoundID:     69,
+		PayloadType: 42,
+		Payload:     []byte("blarg"),
+	}
+
+	cmSerial, err := proto.Marshal(cm)
+	if err != nil {
+		t.Fatalf("Failed to marshal proto: %+v", err)
+	}
+
+	sig := ed25519.Sign(priv, cmSerial)
+	ns := &mockNameService{validChMsg: true}
+
+	um := &UserMessage{
+		Message:      cmSerial,
+		Signature:    sig,
+		ECCPublicKey: pub,
+	}
+
+	umSerial, err := proto.Marshal(um)
+	if err != nil {
+		t.Fatalf("Failed to marshal proto: %+v", err)
+	}
+
+	//build the listener
+	dummy := &triggerEventDummy{}
+
+	al := userListener{
+		chID:      chID,
+		name:      ns,
+		trigger:   dummy.triggerEvent,
+		checkSent: func(messageID cryptoChannel.MessageID, r rounds.Round) bool { return false },
+	}
+
+	//call the listener
+	al.Listen(umSerial, receptionID.EphemeralIdentity{}, r)
+
+	//check the results
+	if dummy.gotData {
+		t.Fatalf("Data returned after invalid listen")
+	}
+}
+
+//tests that the message is rejected when the user message is malformed
+func TestUserListener_Listen_BadMessage(t *testing.T) {
+
+	//build inputs
+	chID := &id.ID{}
+	chID[0] = 1
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	ns := &mockNameService{validChMsg: true}
+
+	umSerial := []byte("malformed")
+
+	//build the listener
+	dummy := &triggerEventDummy{}
+
+	al := userListener{
+		chID:      chID,
+		name:      ns,
+		trigger:   dummy.triggerEvent,
+		checkSent: func(messageID cryptoChannel.MessageID, r rounds.Round) bool { return false },
+	}
+
+	//call the listener
+	al.Listen(umSerial, receptionID.EphemeralIdentity{}, r)
+
+	//check the results
+	if dummy.gotData {
+		t.Fatalf("Data returned after invalid listen")
+	}
+}
+
+//tests that the message is rejected when the sized broadcast is malformed
+func TestUserListener_Listen_BadSizedBroadcast(t *testing.T) {
+
+	//build inputs
+	chID := &id.ID{}
+	chID[0] = 1
+
+	r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)}
+	r.Timestamps[states.QUEUED] = netTime.Now()
+
+	rng := rand.New(rand.NewSource(42))
+	pub, priv, err := ed25519.GenerateKey(rng)
+	if err != nil {
+		t.Fatalf("failed to generate ed25519 keypair, cant run test")
+	}
+
+	cm := &ChannelMessage{
+		Lease: int64(time.Hour),
+		//make the round not match
+		RoundID:     69,
+		PayloadType: 42,
+		Payload:     []byte("blarg"),
+	}
+
+	cmSerial, err := proto.Marshal(cm)
+	if err != nil {
+		t.Fatalf("Failed to marshal proto: %+v", err)
+	}
+
+	sig := ed25519.Sign(priv, cmSerial)
+	ns := &mockNameService{validChMsg: true}
+
+	um := &UserMessage{
+		Message:      cmSerial,
+		Signature:    sig,
+		ECCPublicKey: pub,
+	}
+
+	umSerial, err := proto.Marshal(um)
+	if err != nil {
+		t.Fatalf("Failed to marshal proto: %+v", err)
+	}
+
+	//remove half the sized broadcast to make it malformed
+	umSerial = umSerial[:len(umSerial)/2]
+
+	//build the listener
+	dummy := &triggerEventDummy{}
+
+	al := userListener{
+		chID:      chID,
+		name:      ns,
+		trigger:   dummy.triggerEvent,
+		checkSent: func(messageID cryptoChannel.MessageID, r rounds.Round) bool { return false },
+	}
+
+	//call the listener
+	al.Listen(umSerial, receptionID.EphemeralIdentity{}, r)
+
+	//check the results
+	if dummy.gotData {
+		t.Fatalf("Data returned after invalid listen")
+	}
+}
diff --git a/cmd/broadcast.go b/cmd/broadcast.go
index 9b757490df44dac7493b6522a698b9ae17d7fc99..df52c82f13ae29ae780670a9a502d68be86e2142 100644
--- a/cmd/broadcast.go
+++ b/cmd/broadcast.go
@@ -20,7 +20,7 @@ import (
 	"gitlab.com/elixxir/client/cmix/identity/receptionID"
 	"gitlab.com/elixxir/client/cmix/rounds"
 	crypto "gitlab.com/elixxir/crypto/broadcast"
-	"gitlab.com/xx_network/crypto/signature/rsa"
+	rsa2 "gitlab.com/elixxir/crypto/rsa"
 	"gitlab.com/xx_network/primitives/utils"
 	"sync"
 )
@@ -56,7 +56,7 @@ var broadcastCmd = &cobra.Command{
 		waitUntilConnected(connected)
 		/* Set up underlying crypto broadcast.Channel */
 		var channel *crypto.Channel
-		var pk *rsa.PrivateKey
+		var pk rsa2.PrivateKey
 		keyPath := viper.GetString(broadcastKeyPathFlag)
 		path, err := utils.ExpandPath(viper.GetString(broadcastChanPathFlag))
 		if utils.Exists(path) {
@@ -81,23 +81,26 @@ var broadcastCmd = &cobra.Command{
 
 			if viper.GetBool(broadcastNewFlag) {
 				// Create a new broadcast channel
-				channel, pk, err = crypto.NewChannel(name, desc, user.GetRng().GetStream())
+				channel, pk, err = crypto.NewChannel(name, desc, crypto.Public,
+					user.GetCmix().GetMaxMessageLength(), user.GetRng().GetStream())
 				if err != nil {
 					jww.FATAL.Panicf("Failed to create new channel: %+v", err)
 				}
 
 				if keyPath != "" {
-					err = utils.WriteFile(keyPath, rsa.CreatePrivateKeyPem(pk), os.ModePerm, os.ModeDir)
+					err = utils.WriteFile(keyPath, pk.MarshalPem(), os.ModePerm, os.ModeDir)
 					if err != nil {
 						jww.ERROR.Printf("Failed to write private key to path %s: %+v", path, err)
 					}
 				} else {
-					fmt.Printf("Private key generated for channel: %+v", rsa.CreatePrivateKeyPem(pk))
+					fmt.Printf("Private key generated for channel: %+v", pk.MarshalPem())
 				}
 				fmt.Printf("New broadcast channel generated")
 			} else {
+				//fixme: redo channels, should be using pretty print over cli
+
 				// Read rest of info from config & build object manually
-				pubKeyBytes := []byte(viper.GetString(broadcastRsaPubFlag))
+				/*pubKeyBytes := []byte(viper.GetString(broadcastRsaPubFlag))
 				pubKey, err := rsa.LoadPublicKeyFromPem(pubKeyBytes)
 				if err != nil {
 					jww.FATAL.Panicf("Failed to load public key at path: %+v", err)
@@ -115,7 +118,7 @@ var broadcastCmd = &cobra.Command{
 					Description: desc,
 					Salt:        salt,
 					RsaPubKey:   pubKey,
-				}
+				}*/
 			}
 
 			// Save channel to disk
@@ -142,7 +145,8 @@ var broadcastCmd = &cobra.Command{
 				if err != nil {
 					jww.ERROR.Printf("Failed to read private key from %s: %+v", ep, err)
 				}
-				pk, err = rsa.LoadPrivateKeyFromPem(keyBytes)
+
+				pk, err = rsa2.GetScheme().UnmarshalPrivateKeyPEM(keyBytes)
 				if err != nil {
 					jww.ERROR.Printf("Failed to load private key %+v: %+v", keyBytes, err)
 				}
@@ -158,7 +162,7 @@ var broadcastCmd = &cobra.Command{
 		asymmetric := viper.GetString(broadcastAsymmetricFlag)
 
 		// Connect to broadcast channel
-		bcl, err := broadcast.NewBroadcastChannel(*channel, user.GetCmix(), user.GetRng())
+		bcl, err := broadcast.NewBroadcastChannel(channel, user.GetCmix(), user.GetRng())
 
 		// Create & register symmetric receiver callback
 		receiveChan := make(chan []byte, 100)
@@ -179,7 +183,7 @@ var broadcastCmd = &cobra.Command{
 			jww.INFO.Printf("Received asymmetric message from %s over round %d", receptionID, round.ID)
 			asymmetricReceiveChan <- payload
 		}
-		err = bcl.RegisterListener(acb, broadcast.Asymmetric)
+		err = bcl.RegisterListener(acb, broadcast.RSAToPublic)
 		if err != nil {
 			jww.FATAL.Panicf("Failed to register asymmetric listener: %+v", err)
 		}
@@ -205,39 +209,30 @@ var broadcastCmd = &cobra.Command{
 
 					/* Send symmetric broadcast */
 					if symmetric != "" {
-						// Create properly sized broadcast message
-						broadcastMessage, err := broadcast.NewSizedBroadcast(bcl.MaxPayloadSize(), []byte(symmetric))
-						if err != nil {
-							jww.FATAL.Panicf("Failed to create sized broadcast: %+v", err)
-						}
-						rid, eid, err := bcl.Broadcast(broadcastMessage, cmix.GetDefaultCMIXParams())
+						rid, eid, err := bcl.Broadcast([]byte(symmetric), cmix.GetDefaultCMIXParams())
 						if err != nil {
 							jww.ERROR.Printf("Failed to send symmetric broadcast message: %+v", err)
 							retries++
 							continue
 						}
 						fmt.Printf("Sent symmetric broadcast message: %s", symmetric)
-						jww.INFO.Printf("Sent symmetric broadcast message to %s over round %d", eid, rid)
+						jww.INFO.Printf("Sent symmetric broadcast message to %s over round %d", eid, rid.ID)
 					}
 
 					/* Send asymmetric broadcast */
 					if asymmetric != "" {
 						// Create properly sized broadcast message
-						broadcastMessage, err := broadcast.NewSizedBroadcast(bcl.MaxAsymmetricPayloadSize(), []byte(asymmetric))
-						if err != nil {
-							jww.FATAL.Panicf("Failed to create sized broadcast: %+v", err)
-						}
 						if pk == nil {
 							jww.FATAL.Panicf("CANNOT SEND ASYMMETRIC BROADCAST WITHOUT PRIVATE KEY")
 						}
-						rid, eid, err := bcl.BroadcastAsymmetric(pk, broadcastMessage, cmix.GetDefaultCMIXParams())
+						rid, eid, err := bcl.BroadcastRSAtoPublic(pk, []byte(asymmetric), cmix.GetDefaultCMIXParams())
 						if err != nil {
 							jww.ERROR.Printf("Failed to send asymmetric broadcast message: %+v", err)
 							retries++
 							continue
 						}
 						fmt.Printf("Sent asymmetric broadcast message: %s", asymmetric)
-						jww.INFO.Printf("Sent asymmetric broadcast message to %s over round %d", eid, rid)
+						jww.INFO.Printf("Sent asymmetric broadcast message to %s over round %d", eid, rid.ID)
 					}
 
 					wg.Done()
@@ -261,23 +256,13 @@ var broadcastCmd = &cobra.Command{
 			select {
 			case receivedPayload := <-asymmetricReceiveChan:
 				receivedCount++
-				receivedBroadcast, err := broadcast.DecodeSizedBroadcast(receivedPayload)
-				if err != nil {
-					jww.ERROR.Printf("Failed to decode sized broadcast: %+v", err)
-					continue
-				}
-				fmt.Printf("Asymmetric broadcast message received: %s\n", string(receivedBroadcast))
+				fmt.Printf("Asymmetric broadcast message received: %s\n", string(receivedPayload))
 				if receivedCount == expectedCnt {
 					done = true
 				}
 			case receivedPayload := <-receiveChan:
 				receivedCount++
-				receivedBroadcast, err := broadcast.DecodeSizedBroadcast(receivedPayload)
-				if err != nil {
-					jww.ERROR.Printf("Failed to decode sized broadcast: %+v", err)
-					continue
-				}
-				fmt.Printf("Symmetric broadcast message received: %s\n", string(receivedBroadcast))
+				fmt.Printf("Symmetric broadcast message received: %s\n", string(receivedPayload))
 				if receivedCount == expectedCnt {
 					done = true
 				}
diff --git a/cmd/group.go b/cmd/group.go
index 8f8bb6a3932450fd9f68af0649ddb3b1ecef8f8a..0cf296b4cca2bdc8f2206eb40b4a15367df3a167 100644
--- a/cmd/group.go
+++ b/cmd/group.go
@@ -237,7 +237,7 @@ func sendGroup(groupIdString string, msg []byte, gm groupChat.GroupChat) {
 	}
 
 	jww.INFO.Printf("[GC] Sent to group %s on round %d at %s",
-		groupID, rid, timestamp)
+		groupID, rid.ID, timestamp)
 	fmt.Printf("Sent message %q to group.\n", msg)
 }
 
diff --git a/cmd/root.go b/cmd/root.go
index 33ae2745e7a35176a14d1b1bc39f057cbaa85de6..81d197451d72494298a87613c71ec24a44c86d5f 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -671,14 +671,8 @@ func acceptChannelVerified(user *xxdk.E2e, recipientID *id.ID,
 		rid := acceptChannel(user, recipientID)
 
 		// Monitor rounds for results
-		err := user.GetCmix().GetRoundResults(roundTimeout,
+		user.GetCmix().GetRoundResults(roundTimeout,
 			makeVerifySendsCallback(retryChan, done), rid)
-		if err != nil {
-			jww.DEBUG.Printf("Could not verify "+
-				"confirmation message for relationship with %s were sent "+
-				"successfully, resending messages...", recipientID)
-			continue
-		}
 
 		select {
 		case <-retryChan:
@@ -712,14 +706,9 @@ func requestChannelVerified(user *xxdk.E2e,
 		}
 
 		// Monitor rounds for results
-		err = user.GetCmix().GetRoundResults(roundTimeout,
+		user.GetCmix().GetRoundResults(roundTimeout,
 			makeVerifySendsCallback(retryChan, done),
 			rid)
-		if err != nil {
-			jww.DEBUG.Printf("Could not verify auth request was sent " +
-				"successfully, resending...")
-			continue
-		}
 
 		select {
 		case <-retryChan:
@@ -751,14 +740,9 @@ func resetChannelVerified(user *xxdk.E2e, recipientContact contact.Contact,
 		}
 
 		// Monitor rounds for results
-		err = user.GetCmix().GetRoundResults(roundTimeout,
+		user.GetCmix().GetRoundResults(roundTimeout,
 			makeVerifySendsCallback(retryChan, done),
 			rid)
-		if err != nil {
-			jww.DEBUG.Printf("Could not verify auth request was sent " +
-				"successfully, resending...")
-			continue
-		}
 
 		select {
 		case <-retryChan:
diff --git a/cmd/utils.go b/cmd/utils.go
index 3752dcc7e9e548181b3982db440c39c1a5d3abb5..ad6de14de40c1b265dca23bb9b996c0e8f4b5e9d 100644
--- a/cmd/utils.go
+++ b/cmd/utils.go
@@ -53,13 +53,8 @@ func verifySendSuccess(user *xxdk.E2e, paramsE2E e2e.Params,
 	}
 
 	// Monitor rounds for results
-	err := user.GetCmix().GetRoundResults(
+	user.GetCmix().GetRoundResults(
 		paramsE2E.CMIXParams.Timeout, f, roundIDs...)
-	if err != nil {
-		jww.DEBUG.Printf("Could not verify messages were sent " +
-			"successfully, resending messages...")
-		return false
-	}
 
 	select {
 	case <-retryChan:
diff --git a/cmd/version.go b/cmd/version.go
index 328c9cb0e5bd41b410b01bf498bf0f59997e041c..fcfa3b927bbe2f5669ee881fce0dfd30beb3d20a 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 = "4.2.0"
+const currentVersion = "4.3.0"
 
 func Version() string {
 	out := fmt.Sprintf("Elixxir Client v%s -- %s\n\n", xxdk.SEMVER,
diff --git a/cmix/attempts/histrogram.go b/cmix/attempts/histrogram.go
new file mode 100644
index 0000000000000000000000000000000000000000..37e7292592aeae1cdda482a82b0fdcf5530933d9
--- /dev/null
+++ b/cmix/attempts/histrogram.go
@@ -0,0 +1,116 @@
+package attempts
+
+import (
+	"fmt"
+	"sort"
+	"strconv"
+	"strings"
+	"sync"
+	"sync/atomic"
+)
+
+const (
+	maxHistogramSize            = 100
+	minElements                 = 3
+	percentileNumerator         = 66
+	percentileDenominator       = 99
+	percentileDenominatorOffset = 49
+	optimalAttemptsInitValue    = -1
+)
+
+// SendAttemptTracker tracks the number of attempts it took to send a cMix
+// message in order to predict how many attempt are needed.
+type SendAttemptTracker interface {
+	// SubmitProbeAttempt feeds the number of attempts it took to send a cMix
+	// message into the tracker and updates the optimal number of attempts.
+	SubmitProbeAttempt(numAttemptsUntilSuccessful int)
+
+	// GetOptimalNumAttempts returns the number of optimal sends. If there is
+	// insufficient data to calculate, then ready is false.
+	GetOptimalNumAttempts() (attempts int, ready bool)
+}
+
+// sendAttempts tracks the number of attempts to send a cMix message.
+type sendAttempts struct {
+	optimalAttempts *int32
+	isFull          bool
+	currentIndex    int
+	numAttempts     []int
+	lock            sync.Mutex
+}
+
+// NewSendAttempts initialises a new SendAttemptTracker.
+func NewSendAttempts() SendAttemptTracker {
+	optimalAttempts := int32(optimalAttemptsInitValue)
+	sa := &sendAttempts{
+		optimalAttempts: &optimalAttempts,
+		isFull:          false,
+		currentIndex:    0,
+		numAttempts:     make([]int, maxHistogramSize),
+	}
+
+	return sa
+}
+
+// SubmitProbeAttempt feeds the number of attempts it took to send a cMix
+// message into the tracker and updates the optimal number of attempts.
+func (sa *sendAttempts) SubmitProbeAttempt(numAttemptsUntilSuccessful int) {
+	sa.lock.Lock()
+	defer sa.lock.Unlock()
+
+	sa.numAttempts[sa.currentIndex] = numAttemptsUntilSuccessful
+	sa.currentIndex++
+
+	if sa.currentIndex == len(sa.numAttempts) {
+		sa.currentIndex = 0
+		sa.isFull = true
+	}
+
+	sa.computeOptimalUnsafe()
+}
+
+// GetOptimalNumAttempts returns the number of optimal sends. If there is
+// insufficient data to calculate, then ready is false.
+func (sa *sendAttempts) GetOptimalNumAttempts() (attempts int, ready bool) {
+	optimalAttempts := atomic.LoadInt32(sa.optimalAttempts)
+
+	if optimalAttempts == optimalAttemptsInitValue {
+		return 0, false
+	}
+
+	return int(optimalAttempts), true
+}
+
+// computeOptimalUnsafe updates the optimal send attempts.
+func (sa *sendAttempts) computeOptimalUnsafe() {
+	toCopy := maxHistogramSize
+	if !sa.isFull {
+		if sa.currentIndex < minElements {
+			return
+		}
+		toCopy = sa.currentIndex
+	}
+
+	histogramCopy := make([]int, toCopy)
+	copy(histogramCopy, sa.numAttempts[:toCopy])
+	sort.Ints(histogramCopy)
+
+	i := ((toCopy * percentileNumerator) + percentileDenominatorOffset) /
+		percentileDenominator
+	optimal := histogramCopy[i]
+	atomic.StoreInt32(sa.optimalAttempts, int32(optimal))
+}
+
+// String prints the values in the sendAttempts in a human-readable form for
+// debugging and logging purposes. This function adheres to the fmt.Stringer
+// interface.
+func (sa *sendAttempts) String() string {
+	fields := []string{
+		"optimalAttempts:" + strconv.Itoa(int(atomic.LoadInt32(sa.optimalAttempts))),
+		"isFull:" + strconv.FormatBool(sa.isFull),
+		"currentIndex:" + strconv.Itoa(sa.currentIndex),
+		"numAttempts:" + fmt.Sprintf("%d", sa.numAttempts),
+	}
+
+	return "{" + strings.Join(fields, " ") + "}"
+}
diff --git a/cmix/attempts/histrogram_test.go b/cmix/attempts/histrogram_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..6934b1f332e44cd17dd5cf2030932a8ec364fbd3
--- /dev/null
+++ b/cmix/attempts/histrogram_test.go
@@ -0,0 +1,89 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+package attempts
+
+import (
+	"math/rand"
+	"reflect"
+	"testing"
+)
+
+// Tests that NewSendAttempts returns a new sendAttempts with the expected
+// fields.
+func TestNewSendAttempts(t *testing.T) {
+	optimalAttempts := int32(optimalAttemptsInitValue)
+	expected := &sendAttempts{
+		optimalAttempts: &optimalAttempts,
+		isFull:          false,
+		currentIndex:    0,
+		numAttempts:     make([]int, maxHistogramSize),
+	}
+
+	sa := NewSendAttempts()
+
+	if !reflect.DeepEqual(expected, sa) {
+		t.Errorf("New SendAttemptTracker does not match expected."+
+			"\nexpected: %+v\nreceivedL %+v", expected, sa)
+	}
+}
+
+// Tests that sendAttempts.SubmitProbeAttempt properly increments and stores the
+// attempts.
+func Test_sendAttempts_SubmitProbeAttempt(t *testing.T) {
+	sa := NewSendAttempts().(*sendAttempts)
+
+	for i := 0; i < maxHistogramSize+20; i++ {
+		sa.SubmitProbeAttempt(i)
+
+		if sa.currentIndex != (i+1)%maxHistogramSize {
+			t.Errorf("Incorrect currentIndex (%d).\nexpected: %d\nreceived: %d",
+				i, (i+1)%maxHistogramSize, sa.currentIndex)
+		} else if sa.numAttempts[i%maxHistogramSize] != i {
+			t.Errorf("Incorrect numAttempts at %d.\nexpected: %d\nreceived: %d",
+				i, i, sa.numAttempts[i%maxHistogramSize])
+		} else if i > maxHistogramSize && !sa.isFull {
+			t.Errorf("Should be marked full when numAttempts > %d.",
+				maxHistogramSize)
+		}
+	}
+}
+
+// Tests sendAttempts.GetOptimalNumAttempts returns numbers close to 70% of the
+// average of attempts feeding in.
+func Test_sendAttempts_GetOptimalNumAttempts(t *testing.T) {
+	prng := rand.New(rand.NewSource(42))
+	sa := NewSendAttempts().(*sendAttempts)
+
+	attempts, ready := sa.GetOptimalNumAttempts()
+	if ready {
+		t.Errorf("Marked ready when no attempts have been made.")
+	} else if attempts != 0 {
+		t.Errorf("Incorrect number of attempt.\nexpected: %d\nreceived: %d",
+			0, attempts)
+	}
+
+	const n = 100
+	factor := (n * 7) / 10
+	for i := 0; i < 500; i++ {
+		sa.SubmitProbeAttempt(prng.Intn(n))
+		attempts, ready = sa.GetOptimalNumAttempts()
+
+		if (sa.currentIndex < minElements && !sa.isFull) && ready {
+			t.Errorf("Ready when less than %d attempts made (%d).",
+				minElements, i)
+		} else if sa.currentIndex >= minElements {
+			if !ready {
+				t.Errorf("Not ready when more than %d attempts made (%d).",
+					minElements, i)
+			} else if attempts < factor-25 || attempts > factor+25 {
+				t.Errorf("Attempts is not close to average (%d)."+
+					"\naverage:  %d\nattempts: %d", i, factor, attempts)
+			}
+		}
+	}
+}
diff --git a/cmix/client.go b/cmix/client.go
index d6acf56eb258bb7dfb8b3b31ddb7446c6101e2f6..bc7eaeaecc7a6a8fdb28f189e2d9ad9488870f2d 100644
--- a/cmix/client.go
+++ b/cmix/client.go
@@ -11,6 +11,9 @@ package cmix
 // and intra-client state are accessible through the context object.
 
 import (
+	"gitlab.com/elixxir/client/cmix/attempts"
+	"gitlab.com/elixxir/client/cmix/clockSkew"
+	"gitlab.com/xx_network/primitives/netTime"
 	"math"
 	"strconv"
 	"sync/atomic"
@@ -57,6 +60,8 @@ type client struct {
 	comms *commClient.Comms
 	// Contains the network instance
 	instance *commNetwork.Instance
+	//contains the clock skew tracker
+	skewTracker clockSkew.Tracker
 
 	// Parameters of the network
 	param Params
@@ -70,7 +75,8 @@ type client struct {
 	address.Space
 	identity.Tracker
 	health.Monitor
-	crit *critical
+	crit           *critical
+	attemptTracker attempts.SendAttemptTracker
 
 	// Earliest tracked round
 	earliestRound *uint64
@@ -97,16 +103,20 @@ func NewClient(params Params, comms *commClient.Comms, session storage.Session,
 	tracker := uint64(0)
 	earliest := uint64(0)
 
+	netTime.SetTimeSource(localTime{})
+
 	// Create client object
 	c := &client{
-		param:         params,
-		tracker:       &tracker,
-		events:        events,
-		earliestRound: &earliest,
-		session:       session,
-		rng:           rng,
-		comms:         comms,
-		maxMsgLen:     tmpMsg.ContentsSize(),
+		param:          params,
+		tracker:        &tracker,
+		events:         events,
+		earliestRound:  &earliest,
+		session:        session,
+		rng:            rng,
+		comms:          comms,
+		maxMsgLen:      tmpMsg.ContentsSize(),
+		skewTracker:    clockSkew.New(params.ClockSkewClamp),
+		attemptTracker: attempts.NewSendAttempts(),
 	}
 
 	if params.VerboseRoundTracking {
@@ -188,10 +198,15 @@ func (c *client) initialize(ndf *ndf.NetworkDefinition) error {
 
 	// Set up critical message tracking (sendCmix only)
 	critSender := func(msg format.Message, recipient *id.ID, params CMIXParams,
-	) (id.Round, ephemeral.Id, error) {
-		return sendCmixHelper(c.Sender, msg, recipient, params, c.instance,
+	) (rounds.Round, ephemeral.Id, error) {
+		compiler := func(round id.Round) (format.Message, error) {
+			return msg, nil
+		}
+		r, eid, _, sendErr := sendCmixHelper(c.Sender, compiler, recipient, params, c.instance,
 			c.session.GetCmixGroup(), c.Registrar, c.rng, c.events,
-			c.session.GetTransmissionID(), c.comms)
+			c.session.GetTransmissionID(), c.comms, c.attemptTracker)
+		return r, eid, sendErr
+
 	}
 
 	c.crit = newCritical(c.session.GetKV(), c.Monitor,
diff --git a/cmix/clockSkew/timeTracker.go b/cmix/clockSkew/timeTracker.go
new file mode 100644
index 0000000000000000000000000000000000000000..0d8e79a7e7d483bca198cddabe1444ce6e77899d
--- /dev/null
+++ b/cmix/clockSkew/timeTracker.go
@@ -0,0 +1,153 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+// package clockSkew tracks local clock skew relative to gateways.
+package clockSkew
+
+import (
+	jww "github.com/spf13/jwalterweatherman"
+	"sync"
+	"time"
+
+	"gitlab.com/xx_network/primitives/id"
+)
+
+const maxHistogramSize = 50
+const day = 24 * time.Hour
+
+// Tracker tracks local clock skew relative to various
+// gateways.
+type Tracker interface {
+	// Add additional data to our aggregate clock skews.
+	Add(gwID *id.ID, startTime, rTs time.Time, rtt, gwD time.Duration)
+
+	// Aggregate returns the average of the last n offsets.
+	Aggregate() time.Duration
+}
+
+// gatewayDelays is a helper type used by the timeOffsetTracker below
+// to keep track of the last maxHistogramSize number of durations.
+type gatewayDelays struct {
+	lock         sync.RWMutex
+	delays       []*time.Duration
+	currentIndex int
+}
+
+func newGatewayDelays() *gatewayDelays {
+	return &gatewayDelays{
+		delays:       make([]*time.Duration, maxHistogramSize),
+		currentIndex: 0,
+	}
+}
+
+func (g *gatewayDelays) Add(d time.Duration) {
+	g.lock.Lock()
+	defer g.lock.Unlock()
+
+	g.delays[g.currentIndex] = &d
+	g.currentIndex += 1
+	if g.currentIndex == len(g.delays) {
+		g.currentIndex = 0
+	}
+}
+
+func (g *gatewayDelays) Average() time.Duration {
+	g.lock.RLock()
+	defer g.lock.RUnlock()
+	return average(g.delays)
+}
+
+// timeOffsetTracker implements the Tracker
+type timeOffsetTracker struct {
+	gatewayClockDelays *sync.Map // id.ID -> *gatewayDelays
+
+	lock         sync.RWMutex
+	offsets      []*time.Duration
+	currentIndex int
+	clamp        time.Duration
+}
+
+// New returns an implementation of Tracker.
+func New(clamp time.Duration) Tracker {
+	t := &timeOffsetTracker{
+		gatewayClockDelays: new(sync.Map),
+		offsets:            make([]*time.Duration, maxHistogramSize),
+		currentIndex:       0,
+		clamp:              clamp,
+	}
+	return t
+}
+
+// Add implements the Add method of the Tracker interface.
+func (t *timeOffsetTracker) Add(gwID *id.ID, startTime, rTs time.Time, rtt, gwD time.Duration) {
+	if abs(startTime.Sub(rTs)) > day {
+		jww.WARN.Printf("Time data from %s dropped, more than an day off from"+
+			" local time; local: %s, remote: %s", gwID, startTime, rTs)
+		return
+	}
+
+	delay := (rtt - gwD) / 2
+
+	delays, _ := t.gatewayClockDelays.LoadOrStore(*gwID, newGatewayDelays())
+
+	gwdelays := delays.(*gatewayDelays)
+	gwdelays.Add(delay)
+	gwDelay := gwdelays.Average()
+
+	offset := startTime.Sub(rTs.Add(-gwDelay))
+	t.addOffset(offset)
+}
+
+func abs(duration time.Duration) time.Duration {
+	if duration < 0 {
+		return -duration
+	}
+	return duration
+}
+
+func (t *timeOffsetTracker) addOffset(offset time.Duration) {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	t.offsets[t.currentIndex] = &offset
+	t.currentIndex += 1
+	if t.currentIndex == len(t.offsets) {
+		t.currentIndex = 0
+	}
+}
+
+// Aggregate implements the Aggregate method fo the Tracker interface.
+func (t *timeOffsetTracker) Aggregate() time.Duration {
+	t.lock.RLock()
+	defer t.lock.RUnlock()
+
+	avg := average(t.offsets)
+	if avg < (-t.clamp) || avg > t.clamp {
+		return avg
+	} else {
+		return 0
+	}
+
+}
+
+func average(durations []*time.Duration) time.Duration {
+	sum := int64(0)
+	count := int64(0)
+	for i := 0; i < len(durations); i++ {
+		if durations[i] == nil {
+			break
+		}
+		sum += int64(*durations[i])
+		count += 1
+	}
+
+	if count == 0 {
+		return 0
+	}
+
+	return time.Duration(sum / count)
+}
diff --git a/cmix/clockSkew/timeTracker_test.go b/cmix/clockSkew/timeTracker_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..40872721592f13ee877c96e8c264f4f473e8ca5e
--- /dev/null
+++ b/cmix/clockSkew/timeTracker_test.go
@@ -0,0 +1,84 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+// package clockSkew tracks local clock skew relative to gateways.
+package clockSkew
+
+import (
+	"crypto/rand"
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+
+	"gitlab.com/xx_network/primitives/id"
+)
+
+func TestTimeTrackerSmokeTest(t *testing.T) {
+	tracker := New(0)
+	gwID := &id.ID{}
+	_, err := rand.Read(gwID[:])
+	require.NoError(t, err)
+
+	startTime := time.Now().AddDate(0, 0, -1) // this time yesterday
+	rTs := startTime.Add(time.Second * 10)
+	rtt := time.Second * 10
+	gwD := time.Second * 3
+
+	tracker.Add(gwID, startTime, rTs, rtt, gwD)
+	tracker.Add(gwID, startTime, rTs, rtt, gwD)
+	tracker.Add(gwID, startTime, rTs, rtt, gwD)
+
+	aggregate := tracker.Aggregate()
+
+	t.Logf("aggregate: %v", aggregate)
+}
+
+func TestAverage(t *testing.T) {
+	t1 := time.Duration(int64(10))
+	t2 := time.Duration(int64(20))
+	t3 := time.Duration(int64(30))
+	t4 := time.Duration(int64(1000))
+	durations := make([]*time.Duration, 100)
+	durations[0] = &t1
+	durations[1] = &t2
+	durations[2] = &t3
+	durations[3] = &t4
+	avg := average(durations)
+	require.Equal(t, int(avg), 265)
+}
+
+func TestGatewayDelayAverage(t *testing.T) {
+	t1 := time.Duration(int64(10))
+	t2 := time.Duration(int64(20))
+	t3 := time.Duration(int64(30))
+	t4 := time.Duration(int64(1000))
+	gwDelays := newGatewayDelays()
+	gwDelays.Add(t1)
+	gwDelays.Add(t2)
+	gwDelays.Add(t3)
+	gwDelays.Add(t4)
+	avg := gwDelays.Average()
+	require.Equal(t, int(avg), 265)
+}
+
+func TestAddOffset(t *testing.T) {
+	tracker := &timeOffsetTracker{
+		gatewayClockDelays: new(sync.Map),
+		offsets:            make([]*time.Duration, maxHistogramSize),
+		currentIndex:       0,
+	}
+	offset := time.Second * 10
+
+	for i := 0; i < maxHistogramSize-1; i++ {
+		tracker.addOffset(offset)
+		require.Equal(t, i+1, tracker.currentIndex)
+	}
+	tracker.addOffset(offset)
+	require.Equal(t, 0, tracker.currentIndex)
+}
diff --git a/cmix/critical.go b/cmix/critical.go
index 92de10bb0736c13a130c99735393cc5598e13071..6e9dd64e5a30d95a0933818c9b79e763ec67fe05 100644
--- a/cmix/critical.go
+++ b/cmix/critical.go
@@ -8,6 +8,7 @@
 package cmix
 
 import (
+	"gitlab.com/elixxir/client/cmix/rounds"
 	"time"
 
 	jww "github.com/spf13/jwalterweatherman"
@@ -35,7 +36,7 @@ type roundEventRegistrar interface {
 // anonymous function to include the structures from client that critical is
 // not aware of.
 type criticalSender func(msg format.Message, recipient *id.ID,
-	params CMIXParams) (id.Round, ephemeral.Id, error)
+	params CMIXParams) (rounds.Round, ephemeral.Id, error)
 
 // critical is a structure that allows the auto resending of messages that must
 // be received.
@@ -134,7 +135,7 @@ func (c *critical) evaluate(stop *stoppable.Single) {
 			round, _, err := c.send(msg, recipient, params)
 
 			// Pass to the handler
-			c.handle(msg, recipient, round, err)
+			c.handle(msg, recipient, round.ID, err)
 		}(msg, localRid, params)
 	}
 
diff --git a/cmix/follow.go b/cmix/follow.go
index ef21f5dfcf941f6619c154e681f0781508bd1009..051438d4f5e5456bce0f3338169ac855b99a3f29 100644
--- a/cmix/follow.go
+++ b/cmix/follow.go
@@ -26,6 +26,8 @@ import (
 	"bytes"
 	"encoding/binary"
 	"fmt"
+	"gitlab.com/elixxir/client/cmix/identity/receptionID"
+	"sync"
 	"sync/atomic"
 	"time"
 
@@ -55,7 +57,7 @@ const (
 type followNetworkComms interface {
 	GetHost(hostId *id.ID) (*connect.Host, bool)
 	SendPoll(host *connect.Host, message *pb.GatewayPoll) (
-		*pb.GatewayPollResponse, error)
+		*pb.GatewayPollResponse, time.Time, time.Duration, error)
 	RequestMessages(host *connect.Host, message *pb.GetMessages) (
 		*pb.GetMessagesResponse, error)
 }
@@ -68,7 +70,10 @@ func (c *client) followNetwork(report ClientErrorReport,
 	TrackTicker := time.NewTicker(debugTrackPeriod)
 	rng := c.rng.GetStream()
 
+	// abandon tracks rounds which data was not found out about in
+	// the verbose rounds debugging mode
 	abandon := func(round id.Round) { return }
+	dummyAbandon := func(round id.Round) { return }
 	if c.verboseRounds != nil {
 		abandon = func(round id.Round) {
 			c.verboseRounds.denote(round, Abandoned)
@@ -82,7 +87,57 @@ func (c *client) followNetwork(report ClientErrorReport,
 			stop.ToStopped()
 			return
 		case <-ticker.C:
-			c.follow(report, rng, c.comms, stop, abandon)
+			operator := func(toTrack []receptionID.IdentityUse) error {
+
+				// set up tracking tools
+				wg := &sync.WaitGroup{}
+				wg.Add(len(toTrack))
+
+				// trigger the first separately because it will get network state
+				// updates
+				go func() {
+					c.follow(toTrack[0], report, rng, c.comms, stop, abandon,
+						true)
+					wg.Done()
+				}()
+
+				//trigger all others without getting network state updates
+				for i := 1; i < len(toTrack); i++ {
+					go func(index int) {
+						c.follow(toTrack[index], report, rng, c.comms, stop,
+							dummyAbandon, false)
+						wg.Done()
+					}(i)
+				}
+
+				//wait for all to complete
+				wg.Wait()
+				return nil
+			}
+
+			//denote the execution
+			atomic.AddUint64(c.tracker, 1)
+
+			// track the message on every identity
+			stream := c.rng.GetStream()
+			err := c.Tracker.ForEach(
+				int(c.param.MaxParallelIdentityTracks),
+				stream,
+				c.Space.GetAddressSpaceWithoutWait(),
+				operator)
+			stream.Close()
+
+			//update clock skew
+			estimatedSkew := c.skewTracker.Aggregate()
+			// invert the skew because we need to reverse it
+			netTime.SetOffset(-estimatedSkew)
+
+			if err != nil {
+				jww.ERROR.Printf("failed to operate on identities to "+
+					"track: %s", err)
+				continue
+			}
+
 		case <-TrackTicker.C:
 			numPolls := atomic.SwapUint64(c.tracker, 0)
 			if c.numLatencies != 0 {
@@ -108,18 +163,10 @@ func (c *client) followNetwork(report ClientErrorReport,
 	}
 }
 
-// follow executes each iteration of the follower.
-func (c *client) follow(report ClientErrorReport, rng csprng.Source,
-	comms followNetworkComms, stop *stoppable.Single,
-	abandon func(round id.Round)) {
-
-	// Get the identity we will poll for
-	identity, err := c.GetEphemeralIdentity(
-		rng, c.Space.GetAddressSpaceWithoutWait())
-	if err != nil {
-		jww.FATAL.Panicf(
-			"Failed to get an identity, this should be impossible: %+v", err)
-	}
+// follow executes an iteration of the follower for a specific identity
+func (c *client) follow(identity receptionID.IdentityUse,
+	report ClientErrorReport, rng csprng.Source, comms followNetworkComms,
+	stop *stoppable.Single, abandon func(round id.Round), getUpdates bool) {
 
 	// While polling with a fake identity, it is necessary to have populated
 	// earliestRound data. However, as with fake identities, we want the values
@@ -130,8 +177,6 @@ func (c *client) follow(report ClientErrorReport, rng csprng.Source,
 		identity.ER = fakeEr
 	}
 
-	atomic.AddUint64(c.tracker, 1)
-
 	// Get client version for poll
 	version := c.session.GetClientVersion()
 
@@ -147,14 +192,23 @@ func (c *client) follow(report ClientErrorReport, rng csprng.Source,
 		ClientVersion:  []byte(version.String()),
 		FastPolling:    c.param.FastPolling,
 		LastRound:      uint64(identity.ER.Get()),
+		DisableUpdates: !getUpdates,
 	}
 
+	var rtt time.Duration
+	var sendTo *id.ID
+	var startTime time.Time
+
 	result, err := c.SendToAny(func(host *connect.Host) (interface{}, error) {
 		jww.DEBUG.Printf("Executing poll for %v(%s) range: %s-%s(%s) from %s",
 			identity.EphId.Int64(), identity.Source, identity.StartValid,
 			identity.EndValid, identity.EndValid.Sub(identity.StartValid),
 			host.GetId())
-		return comms.SendPoll(host, &pollReq)
+		var err error
+		var response *pb.GatewayPollResponse
+		response, startTime, rtt, err = comms.SendPoll(host, &pollReq)
+		sendTo = host.GetId()
+		return response, err
 	}, stop)
 
 	// Exit if the thread has been stopped
@@ -181,6 +235,11 @@ func (c *client) follow(report ClientErrorReport, rng csprng.Source,
 
 	pollResp := result.(*pb.GatewayPollResponse)
 
+	//execute clock skew update
+	c.skewTracker.Add(sendTo, startTime,
+		time.Unix(0, pollResp.ReceivedTs),
+		rtt, time.Duration(pollResp.GatewayDelay))
+
 	// ---- Process Network State Update Data ----
 	gwRoundsState := &knownRounds.KnownRounds{}
 	err = gwRoundsState.Unmarshal(pollResp.KnownRounds)
@@ -299,6 +358,7 @@ func (c *client) follow(report ClientErrorReport, rng csprng.Source,
 		if !hasMessage && c.verboseRounds != nil {
 			c.verboseRounds.denote(rid, RoundState(NoMessageAvailable))
 		}
+		//jww.INFO.Printf("[LOOKUP] round %d checked for %d, has message: %v", rid, identity.EphId.Int64(), hasMessage)
 		return hasMessage
 	}
 
@@ -349,10 +409,10 @@ func (c *client) follow(report ClientErrorReport, rng csprng.Source,
 		gwRoundsState.RangeUnchecked(
 			updatedEarliestRound, c.param.KnownRoundsThreshold, roundChecker)
 
-	jww.DEBUG.Printf("Processed RangeUnchecked, Oldest: %d, "+
+	jww.DEBUG.Printf("Processed RangeUnchecked for %d, Oldest: %d, "+
 		"firstUnchecked: %d, last Checked: %d, threshold: %d, "+
 		"NewEarliestRemaining: %d, NumWithMessages: %d, NumUnknown: %d",
-		updatedEarliestRound, gwRoundsState.GetFirstUnchecked(),
+		identity.EphId.Int64(), updatedEarliestRound, gwRoundsState.GetFirstUnchecked(),
 		gwRoundsState.GetLastChecked(), c.param.KnownRoundsThreshold,
 		earliestRemaining, len(roundsWithMessages), len(roundsUnknown))
 
diff --git a/cmix/gateway/defaults.go b/cmix/gateway/defaults.go
new file mode 100644
index 0000000000000000000000000000000000000000..9e19c49cf3577888da2c31daf5bed9d8eabf52fd
--- /dev/null
+++ b/cmix/gateway/defaults.go
@@ -0,0 +1,9 @@
+//go:build !js || !wasm
+// +build !js !wasm
+
+// This file is compiled for all architectures except WebAssembly.
+package gateway
+
+const (
+	MaxPoolSize = 20
+)
diff --git a/cmix/gateway/defaults_js.go b/cmix/gateway/defaults_js.go
new file mode 100644
index 0000000000000000000000000000000000000000..8eb93858dc95aefd6506ab7bed520f83ab274632
--- /dev/null
+++ b/cmix/gateway/defaults_js.go
@@ -0,0 +1,5 @@
+package gateway
+
+const (
+	MaxPoolSize = 7
+)
diff --git a/cmix/gateway/hostPool.go b/cmix/gateway/hostPool.go
index 49875776c2ff735520e7662eea9a14cdb3a44811..31f9ba3ca400705a2ddf82268bc6e0b5f4d1c266 100644
--- a/cmix/gateway/hostPool.go
+++ b/cmix/gateway/hostPool.go
@@ -48,6 +48,7 @@ var errorsList = []string{
 	"Host is in cool down",
 	grpc.ErrClientConnClosing.Error(),
 	connect.TooManyProxyError,
+	"Failed to fetch",
 }
 
 // HostManager Interface allowing storage and retrieval of Host objects
@@ -127,7 +128,7 @@ type poolParamsDisk struct {
 // DefaultPoolParams returns a default set of PoolParams.
 func DefaultPoolParams() PoolParams {
 	p := PoolParams{
-		MaxPoolSize:     30,
+		MaxPoolSize:     MaxPoolSize,
 		ProxyAttempts:   5,
 		PoolSize:        0,
 		MaxPings:        0,
@@ -247,7 +248,7 @@ func newHostPool(poolParams PoolParams, rng *fastRNG.StreamGenerator,
 		}
 	} else {
 		jww.WARN.Printf(
-			"Building new HostPool because no HostList stored: %+v", err)
+			"Building new HostPool because no HostList stored: %s", err.Error())
 	}
 
 	// Build the initial HostPool and return
@@ -307,25 +308,22 @@ func (h *HostPool) initialize(startIdx uint32) error {
 		id      *id.ID
 		latency time.Duration
 	}
+
 	numGatewaysToTry := h.poolParams.MaxPings
 	numGateways := uint32(len(randomGateways))
 	if numGatewaysToTry > numGateways {
 		numGatewaysToTry = numGateways
 	}
+
 	resultList := make([]gatewayDuration, 0, numGatewaysToTry)
 
 	// Begin trying gateways
 	c := make(chan gatewayDuration, numGatewaysToTry)
-	exit := false
-	i := uint32(0)
-	for !exit {
-		for ; i < numGateways; i++ {
-			// Ran out of Hosts to try
-			if i >= numGateways {
-				exit = true
-				break
-			}
 
+	i := 0
+	for exit := false; !exit; {
+		triedHosts := uint32(0)
+		for ; triedHosts < numGateways && i < len(randomGateways); i++ {
 			// Select a gateway not yet selected
 			gwId, err := randomGateways[i].GetGatewayId()
 			if err != nil {
@@ -334,10 +332,9 @@ func (h *HostPool) initialize(startIdx uint32) error {
 
 			// Skip if already in HostPool
 			if _, ok := h.hostMap[*gwId]; ok {
-				// Try another Host instead
-				numGatewaysToTry++
 				continue
 			}
+			triedHosts++
 
 			go func() {
 				// Obtain that GwId's Host object
@@ -358,19 +355,22 @@ func (h *HostPool) initialize(startIdx uint32) error {
 		// Collect ping results
 		pingTimeout := 2 * h.poolParams.HostParams.PingTimeout
 		timer := time.NewTimer(pingTimeout)
+
+		newAppends := uint32(0)
 	innerLoop:
 		for {
 			select {
 			case gw := <-c:
 				// Only add successful pings
 				if gw.latency > 0 {
+					newAppends++
 					resultList = append(resultList, gw)
 					jww.DEBUG.Printf("Adding HostPool result %d/%d: %s: %d",
 						len(resultList), numGatewaysToTry, gw.id, gw.latency)
 				}
 
 				// Break if we have all needed slots
-				if uint32(len(resultList)) == numGatewaysToTry {
+				if newAppends == triedHosts {
 					exit = true
 					timer.Stop()
 					break innerLoop
@@ -382,6 +382,10 @@ func (h *HostPool) initialize(startIdx uint32) error {
 				break innerLoop
 			}
 		}
+
+		if i >= len(randomGateways) {
+			exit = true
+		}
 	}
 
 	// Sort the resultList by lowest latency
diff --git a/cmix/gateway/sender.go b/cmix/gateway/sender.go
index 98dffa6a2de35328d4b42f27d920144663de4095..3919fad03137a65591836be7a4358de14b0e9f68 100644
--- a/cmix/gateway/sender.go
+++ b/cmix/gateway/sender.go
@@ -77,11 +77,11 @@ func (s *sender) SendToAny(sendFunc func(*connect.Host) (interface{}, error),
 					"with error %s", proxies[proxy].GetId(), err.Error())
 			} else {
 				if checkReplaceErr != nil {
-					jww.WARN.Printf("Unable to SendToAny via %s: %s."+
+					jww.WARN.Printf("Unable to SendToAny via %s: %s. "+
 						"Unable to replace host: %+v",
 						proxies[proxy].GetId(), err.Error(), checkReplaceErr)
 				} else {
-					jww.WARN.Printf("Unable to SendToAny via %s: %s."+
+					jww.WARN.Printf("Unable to SendToAny via %s: %s. "+
 						"Did not replace host.",
 						proxies[proxy].GetId(), err.Error())
 				}
diff --git a/cmix/health/callback.go b/cmix/health/callback.go
new file mode 100644
index 0000000000000000000000000000000000000000..26dc504327d2f649875df0cb4c0a6d965001051c
--- /dev/null
+++ b/cmix/health/callback.go
@@ -0,0 +1,53 @@
+package health
+
+import "sync"
+
+type trackerCallback struct {
+	funcs   map[uint64]func(isHealthy bool)
+	funcsID uint64
+
+	mux sync.RWMutex
+}
+
+func initTrackerCallback() *trackerCallback {
+	return &trackerCallback{
+		funcs:   map[uint64]func(isHealthy bool){},
+		funcsID: 0,
+	}
+}
+
+// addHealthCallback adds a function to the list of tracker functions such that
+// each function can be run after network changes. Returns a unique ID for the
+// function.
+func (t *trackerCallback) addHealthCallback(f func(isHealthy bool), health bool) uint64 {
+	var currentID uint64
+
+	t.mux.Lock()
+	t.funcs[t.funcsID] = f
+	currentID = t.funcsID
+	t.funcsID++
+	t.mux.Unlock()
+
+	go f(health)
+
+	return currentID
+}
+
+// RemoveHealthCallback removes the function with the given ID from the list of
+// tracker functions so that it will no longer be run.
+func (t *trackerCallback) RemoveHealthCallback(chanID uint64) {
+	t.mux.Lock()
+	delete(t.funcs, chanID)
+	t.mux.Unlock()
+}
+
+// callback calls every function with the new health state
+func (t *trackerCallback) callback(health bool) {
+	t.mux.Lock()
+	defer t.mux.Unlock()
+
+	// Run all listening functions
+	for _, f := range t.funcs {
+		go f(health)
+	}
+}
diff --git a/cmix/health/tracker.go b/cmix/health/tracker.go
index 537ec320cae8341b341653e4af07c93fc37c50ce..1476b788ce6feba441c6b2facb97ea884482bcae 100644
--- a/cmix/health/tracker.go
+++ b/cmix/health/tracker.go
@@ -11,8 +11,7 @@
 package health
 
 import (
-	"errors"
-	"sync"
+	"sync/atomic"
 	"time"
 
 	jww "github.com/spf13/jwalterweatherman"
@@ -29,108 +28,130 @@ type Monitor interface {
 }
 
 type tracker struct {
+	// timeout parameter describes how long
+	// without good news until the network is considered unhealthy
 	timeout time.Duration
 
+	// channel on which new status updates are received from the network handler
 	heartbeat chan network.Heartbeat
 
-	funcs      map[uint64]func(isHealthy bool)
-	channelsID uint64
-	funcsID    uint64
-
-	running bool
-
-	// Determines the current health status
-	isHealthy bool
+	// denotes the last time news was heard. Both hold ns since unix epoc
+	// in an atomic
+	lastCompletedRound *int64
+	lastWaitingRound   *int64
 
 	// Denotes that the past health status wasHealthy is true if isHealthy has
-	// ever been true
-	wasHealthy bool
-	mux        sync.RWMutex
+	// ever been true in an atomic.
+	wasHealthy *uint32
+
+	// stores registered callbacks to receive event updates
+	*trackerCallback
 }
 
 // Init creates a single HealthTracker thread, starts it, and returns a tracker
 // and a stoppable.
 func Init(instance *network.Instance, timeout time.Duration) Monitor {
-	tracker := newTracker(timeout)
-	instance.SetNetworkHealthChan(tracker.heartbeat)
 
-	return tracker
+	trkr := newTracker(timeout)
+	instance.SetNetworkHealthChan(trkr.heartbeat)
+
+	return trkr
 }
 
 // newTracker builds and returns a new tracker object given a Context.
 func newTracker(timeout time.Duration) *tracker {
-	return &tracker{
-		timeout:   timeout,
-		funcs:     map[uint64]func(isHealthy bool){},
-		heartbeat: make(chan network.Heartbeat, 100),
-		isHealthy: false,
-		running:   false,
-	}
-}
 
-// AddHealthCallback adds a function to the list of tracker functions such that
-// each function can be run after network changes. Returns a unique ID for the
-// function.
-func (t *tracker) AddHealthCallback(f func(isHealthy bool)) uint64 {
-	var currentID uint64
+	lastCompletedRound := int64(0)
+	lastWaitingRound := int64(0)
 
-	t.mux.Lock()
-	t.funcs[t.funcsID] = f
-	currentID = t.funcsID
-	t.funcsID++
-	t.mux.Unlock()
+	wasHealthy := uint32(0)
 
-	go f(t.IsHealthy())
+	t := &tracker{
+		timeout:            timeout,
+		heartbeat:          make(chan network.Heartbeat, 100),
+		lastCompletedRound: &lastCompletedRound,
+		lastWaitingRound:   &lastWaitingRound,
+		wasHealthy:         &wasHealthy,
+	}
+	t.trackerCallback = initTrackerCallback()
+	return t
+}
 
-	return currentID
+// getLastCompletedRoundTimestamp atomically loads the completed round timestamp
+// and converts it to a time object, then returns it
+func (t *tracker) getLastCompletedRoundTimestamp() time.Time {
+	return time.Unix(0, atomic.LoadInt64(t.lastCompletedRound))
 }
 
-// RemoveHealthCallback removes the function with the given ID from the list of
-// tracker functions so that it will no longer be run.
-func (t *tracker) RemoveHealthCallback(chanID uint64) {
-	t.mux.Lock()
-	delete(t.funcs, chanID)
-	t.mux.Unlock()
+// getLastWaitingRoundTimestamp atomically loads the waiting round timestamp
+// and converts it to a time object, then returns it
+func (t *tracker) getLastWaitingRoundTimestamp() time.Time {
+	return time.Unix(0, atomic.LoadInt64(t.lastWaitingRound))
 }
 
+// IsHealthy returns true if the network is healthy, which is
+// defined as the client having knowledge of both valid queued rounds
+// and completed rounds within the last tracker.timeout seconds
 func (t *tracker) IsHealthy() bool {
-	t.mux.RLock()
-	defer t.mux.RUnlock()
+	// use the system time instead of netTime.Now() which can
+	// include an offset because local monotonicity is what
+	// matters here, not correctness relative to absolute time
+	now := time.Now()
+
+	completedRecently := false
+	if now.Sub(t.getLastCompletedRoundTimestamp()) < t.timeout {
+		completedRecently = true
+	}
 
-	return t.isHealthy
+	waitingRecently := false
+	if now.Sub(t.getLastWaitingRoundTimestamp()) < t.timeout {
+		waitingRecently = true
+	}
+
+	return completedRecently && waitingRecently
 }
 
-// WasHealthy returns true if isHealthy has ever been true.
-func (t *tracker) WasHealthy() bool {
-	t.mux.RLock()
-	defer t.mux.RUnlock()
+// updateHealth atomically updates the internal
+// timestamps to now if there are new waiting / completed
+// rounds
+func (t *tracker) updateHealth(hasWaiting, hasCompleted bool) {
+	// use the system time instead of netTime.Now() which can
+	// include an offset because local monotonicity is what
+	// matters here, not correctness relative to absolute time
+	now := time.Now().UnixNano()
+
+	if hasWaiting {
+		atomic.StoreInt64(t.lastWaitingRound, now)
+	}
 
-	return t.wasHealthy
+	if hasCompleted {
+		atomic.StoreInt64(t.lastCompletedRound, now)
+	}
 }
 
-func (t *tracker) setHealth(h bool) {
-	t.mux.Lock()
-	// Only set wasHealthy to true if either
-	//  wasHealthy is true or
-	//  wasHealthy is false but h value is true
-	t.wasHealthy = t.wasHealthy || h
-	t.isHealthy = h
-	t.mux.Unlock()
+// forceUnhealthy cleats the internal timestamps, forcing the
+// tracker into unhealthy
+func (t *tracker) forceUnhealthy() {
+	atomic.StoreInt64(t.lastWaitingRound, 0)
+	atomic.StoreInt64(t.lastCompletedRound, 0)
+}
 
-	t.transmit(h)
+// WasHealthy returns true if isHealthy has ever been true.
+func (t *tracker) WasHealthy() bool {
+	return atomic.LoadUint32(t.wasHealthy) == 1
+}
+
+// AddHealthCallback adds a function to the list of tracker functions such that
+// each function can be run after network changes. Returns a unique ID for the
+// function.
+func (t *tracker) AddHealthCallback(f func(isHealthy bool)) uint64 {
+	return t.addHealthCallback(f, t.IsHealthy())
 }
 
+// StartProcesses starts running the
 func (t *tracker) StartProcesses() (stoppable.Stoppable, error) {
-	t.mux.Lock()
-	if t.running {
-		t.mux.Unlock()
-		return nil, errors.New(
-			"cannot start health tracker threads, they are already running")
-	}
-	t.running = true
 
-	t.isHealthy = false
-	t.mux.Unlock()
+	atomic.StoreUint32(t.wasHealthy, 0)
 
 	stop := stoppable.NewSingle("health tracker")
 
@@ -139,45 +160,68 @@ func (t *tracker) StartProcesses() (stoppable.Stoppable, error) {
 	return stop, nil
 }
 
-// start starts a long-running thread used to monitor and report on network
+// start begins a long-running thread used to monitor and report on network
 // health.
 func (t *tracker) start(stop *stoppable.Single) {
+
+	// ensures wasHealthy is only set once
+	hasSetWasHealthy := false
+
+	// denotation of the previous state in order to catch state changes
+	lastState := false
+
+	// flag denoting required exit, allows final signaling
+	quit := false
+
+	//ensured the timeout error is only printed once per timeout period
+	timedOut := true
+
 	for {
-		var heartbeat network.Heartbeat
+
+		/* wait for an event */
 		select {
 		case <-stop.Quit():
-			t.mux.Lock()
-			t.isHealthy = false
-			t.running = false
-			t.mux.Unlock()
+			t.forceUnhealthy()
 
-			t.transmit(false)
-			stop.ToStopped()
+			// flag the quit instead of quitting here so the
+			// joint signaling handler code can be triggered
+			quit = true
 
-			return
-		case heartbeat = <-t.heartbeat:
-			// FIXME: There's no transition to unhealthy here and there needs to
-			//  be after some number of bad polls
-			if healthy(heartbeat) {
-				t.setHealth(true)
-			}
+		case heartbeat := <-t.heartbeat:
+			t.updateHealth(heartbeat.HasWaitingRound, heartbeat.IsRoundComplete)
+			timedOut = false
 		case <-time.After(t.timeout):
-			if !t.isHealthy {
-				jww.WARN.Printf("Network health tracker timed out, network " +
-					"is no longer healthy...")
+			if !timedOut {
+				jww.ERROR.Printf("Network health tracker timed out, network " +
+					"is no longer healthy, follower likely has stopped...")
+			}
+			timedOut = true
+
+			// note: no need to force to unhealthy because by definition the
+			// timestamps will be stale
+		}
+
+		/* handle the state change resulting from an event */
+
+		// send signals if the state has changed
+		newHealthState := t.IsHealthy()
+		if newHealthState != lastState {
+			// set was healthy if we are healthy and it was never set before
+			if newHealthState && !hasSetWasHealthy {
+				atomic.StoreUint32(t.wasHealthy, 1)
+				hasSetWasHealthy = true
 			}
-			t.setHealth(false)
+
+			//trigger downstream events
+			t.callback(newHealthState)
+
+			lastState = newHealthState
 		}
-	}
-}
 
-func (t *tracker) transmit(health bool) {
-	// Run all listening functions
-	for _, f := range t.funcs {
-		go f(health)
+		// quit if required to quit
+		if quit {
+			stop.ToStopped()
+			return
+		}
 	}
 }
-
-func healthy(a network.Heartbeat) bool {
-	return a.IsRoundComplete
-}
diff --git a/cmix/identity/receptionID/store.go b/cmix/identity/receptionID/store.go
index 37dfb8bba2212773c6c2de62fd3604ba3d307c1e..967c22e034270d726b2c4e216d460dc767e0dbd2 100644
--- a/cmix/identity/receptionID/store.go
+++ b/cmix/identity/receptionID/store.go
@@ -13,6 +13,7 @@ import (
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/storage/versioned"
 	"gitlab.com/xx_network/crypto/large"
+	"gitlab.com/xx_network/crypto/shuffle"
 	"gitlab.com/xx_network/primitives/id"
 	"gitlab.com/xx_network/primitives/id/ephemeral"
 	"gitlab.com/xx_network/primitives/netTime"
@@ -28,6 +29,8 @@ const (
 	receptionStoreStorageVersion = 0
 )
 
+var InvalidRequestedNumIdentities = errors.New("cannot get less than one identity(s)")
+
 type Store struct {
 	// Identities which are being actively checked
 	active  []*registration
@@ -61,7 +64,7 @@ func NewOrLoadStore(kv *versioned.KV) *Store {
 	s, err := loadStore(kv)
 	if err != nil {
 		jww.WARN.Printf(
-			"ReceptionID store not found, creating a new one: %+v", err)
+			"ReceptionID store not found, creating a new one: %s", err.Error())
 
 		s = &Store{
 			active:  []*registration{},
@@ -157,7 +160,15 @@ func (s *Store) makeStoredReferences() []storedReference {
 	return identities[:i]
 }
 
-func (s *Store) GetIdentity(rng io.Reader, addressSize uint8) (IdentityUse, error) {
+// ForEach operates on 'n' identities randomly in a random order.
+// if no identities exist, it will operate on a single fake identity
+func (s *Store) ForEach(n int, rng io.Reader,
+	addressSize uint8, operate func([]IdentityUse) error) error {
+
+	if n < 1 {
+		return InvalidRequestedNumIdentities
+	}
+
 	s.mux.Lock()
 	defer s.mux.Unlock()
 
@@ -166,26 +177,29 @@ func (s *Store) GetIdentity(rng io.Reader, addressSize uint8) (IdentityUse, erro
 	// Remove any now expired identities
 	s.prune(now)
 
-	var identity IdentityUse
-	var err error
+	var identities []IdentityUse
 
 	// If the list is empty, then return a randomly generated identity to poll
 	// with so that we can continue tracking the network and to further
 	// obfuscate network identities.
 	if len(s.active) == 0 {
-		identity, err = generateFakeIdentity(rng, addressSize, now)
+		fakeIdentity, err := generateFakeIdentity(rng, addressSize, now)
 		if err != nil {
 			jww.FATAL.Panicf(
 				"Failed to generate a new ID when none available: %+v", err)
 		}
+		identities = append(identities, fakeIdentity)
+		// otherwise, select identities to return using a fisher-yates
 	} else {
-		identity, err = s.selectIdentity(rng, now)
+		var err error
+		identities, err = s.selectIdentities(n, rng, now)
 		if err != nil {
-			jww.FATAL.Panicf("Failed to select an ID: %+v", err)
+			jww.FATAL.Panicf("Failed to select a list of IDs: %+v", err)
 		}
 	}
 
-	return identity, nil
+	// do the passed operation on all identities
+	return operate(identities)
 }
 
 func (s *Store) AddIdentity(identity Identity) error {
@@ -228,10 +242,11 @@ func (s *Store) RemoveIdentity(ephID ephemeral.Id) {
 	s.mux.Lock()
 	defer s.mux.Unlock()
 
-	for i, inQuestion := range s.active {
+	for i := 0; i < len(s.active); i++ {
+		inQuestion := s.active[i]
 		if inQuestion.EphId == ephID {
 			s.active = append(s.active[:i], s.active[i+1:]...)
-
+			delete(s.present, makeIdHash(inQuestion.EphId, inQuestion.Source))
 			err := inQuestion.Delete()
 			if err != nil {
 				jww.FATAL.Panicf("Failed to delete identity: %+v", err)
@@ -244,6 +259,8 @@ func (s *Store) RemoveIdentity(ephID ephemeral.Id) {
 				}
 			}
 
+			i--
+
 			return
 		}
 	}
@@ -254,16 +271,20 @@ func (s *Store) RemoveIdentities(source *id.ID) {
 	defer s.mux.Unlock()
 
 	doSave := false
-	for i, inQuestion := range s.active {
+	for i := 0; i < len(s.active); i++ {
+		inQuestion := s.active[i]
 		if inQuestion.Source.Cmp(source) {
 			s.active = append(s.active[:i], s.active[i+1:]...)
-
+			delete(s.present, makeIdHash(inQuestion.EphId, inQuestion.Source))
+			jww.INFO.Printf("Removing Identity %s:%d from tracker",
+				inQuestion.Source, inQuestion.EphId.Int64())
 			err := inQuestion.Delete()
 			if err != nil {
 				jww.FATAL.Panicf("Failed to delete identity: %+v", err)
 			}
 
 			doSave = doSave || !inQuestion.Ephemeral
+			i--
 		}
 	}
 	if doSave {
@@ -305,6 +326,7 @@ func (s *Store) prune(now time.Time) {
 			pruned = append(pruned, inQuestion.EphId.Int64())
 
 			s.active = append(s.active[:i], s.active[i+1:]...)
+			delete(s.present, makeIdHash(inQuestion.EphId, inQuestion.Source))
 
 			i--
 		}
@@ -320,6 +342,8 @@ func (s *Store) prune(now time.Time) {
 	}
 }
 
+// selectIdentity returns a random identity in an IdentityUse object and
+// increments its usage if necessary
 func (s *Store) selectIdentity(rng io.Reader, now time.Time) (IdentityUse, error) {
 	// Choose a member from the list
 	var selected *registration
@@ -327,7 +351,7 @@ func (s *Store) selectIdentity(rng io.Reader, now time.Time) (IdentityUse, error
 	if len(s.active) == 1 {
 		selected = s.active[0]
 	} else {
-		seed := make([]byte, 32)
+		seed := make([]byte, 32) //use 256 bits of entropy for the seed
 		if _, err := rng.Read(seed); err != nil {
 			return IdentityUse{}, errors.WithMessage(err, "Failed to choose "+
 				"ID due to RNG failure")
@@ -341,10 +365,6 @@ func (s *Store) selectIdentity(rng io.Reader, now time.Time) (IdentityUse, error
 		selected = s.active[selectedNum.Uint64()]
 	}
 
-	if now.After(selected.End) {
-		selected.ExtraChecks--
-	}
-
 	jww.TRACE.Printf("Selected identity: EphId: %d  ID: %s  End: %s  "+
 		"StartValid: %s  EndValid: %s",
 		selected.EphId.Int64(), selected.Source,
@@ -352,11 +372,64 @@ func (s *Store) selectIdentity(rng io.Reader, now time.Time) (IdentityUse, error
 		selected.StartValid.Format("01/02/06 03:04:05 pm"),
 		selected.EndValid.Format("01/02/06 03:04:05 pm"))
 
+	return useIdentity(selected, now), nil
+}
+
+// selectIdentities returns up to 'n' identities in an IdentityUse object
+// selected via fisher-yates and increments their usage if necessary
+func (s *Store) selectIdentities(n int, rng io.Reader, now time.Time) ([]IdentityUse, error) {
+	// Choose a member from the list
+	selected := make([]IdentityUse, 0, n)
+
+	if len(s.active) == 1 {
+		selected = append(selected, useIdentity(s.active[0], now))
+	} else {
+
+		// make the seed
+		seed := make([]byte, 32) //use 256 bits of entropy for the seed
+		if _, err := rng.Read(seed); err != nil {
+			return nil, errors.WithMessage(err, "Failed to choose "+
+				"ID due to RNG failure")
+		}
+
+		// make the list to shuffle
+		registered := make([]*registration, 0, len(s.active))
+		for i := 0; i < len(s.active); i++ {
+			registered = append(registered, s.active[i])
+		}
+
+		//shuffle the list via fisher-yates
+		registeredProxy := shuffle.SeededShuffle(len(s.active), seed)
+
+		//convert the list to identity use
+		for i := 0; i < len(registered) && (i < n); i++ {
+			selected = append(selected,
+				useIdentity(registered[registeredProxy[i]], now))
+		}
+
+	}
+
+	jww.TRACE.Printf("Selected %d identities, first identity: EphId: %d  ID: %s  End: %s  "+
+		"StartValid: %s  EndValid: %s", len(selected),
+		selected[0].EphId.Int64(), selected[0].Source,
+		selected[0].End.Format("01/02/06 03:04:05 pm"),
+		selected[0].StartValid.Format("01/02/06 03:04:05 pm"),
+		selected[0].EndValid.Format("01/02/06 03:04:05 pm"))
+
+	return selected, nil
+}
+
+// useIdentity makes the public IdentityUse object from a private *registration
+// and deals with denoting the usage in the *registration if nessessay
+func useIdentity(selected *registration, now time.Time) IdentityUse {
+	if now.After(selected.End) {
+		selected.ExtraChecks--
+	}
 	return IdentityUse{
 		Identity: selected.Identity,
 		Fake:     false,
 		UR:       selected.UR,
 		ER:       selected.ER,
 		CR:       selected.CR,
-	}, nil
+	}
 }
diff --git a/cmix/identity/receptionID/store_test.go b/cmix/identity/receptionID/store_test.go
index 68ad9c12b63ed708c1e82bf08ded5cfccac74984..5161469cfd7c044eddade131e8f76b90c6c30678 100644
--- a/cmix/identity/receptionID/store_test.go
+++ b/cmix/identity/receptionID/store_test.go
@@ -9,10 +9,13 @@ package receptionID
 
 import (
 	"bytes"
+	"encoding/binary"
 	"encoding/json"
 	"gitlab.com/elixxir/client/storage/versioned"
+	"gitlab.com/elixxir/crypto/hash"
 	"gitlab.com/elixxir/ekv"
 	"gitlab.com/xx_network/primitives/netTime"
+	"math"
 	"math/rand"
 	"reflect"
 	"testing"
@@ -148,27 +151,226 @@ func TestStore_makeStoredReferences(t *testing.T) {
 	}
 }
 
-func TestStore_GetIdentity(t *testing.T) {
+func TestStore_GetIdentities(t *testing.T) {
 	kv := versioned.NewKV(ekv.MakeMemstore())
 	s := NewOrLoadStore(kv)
 	prng := rand.New(rand.NewSource(42))
-	testID, err := generateFakeIdentity(prng, 15, netTime.Now())
+
+	numToTest := 100
+
+	idsGenerated := make(map[uint64]interface{})
+
+	for i := 0; i < numToTest; i++ {
+		testID, err := generateFakeIdentity(prng, 15, netTime.Now())
+		if err != nil {
+			t.Fatalf("Failed to generate fake ID: %+v", err)
+		}
+		testID.Fake = false
+		if s.AddIdentity(testID.Identity) != nil {
+			t.Errorf("AddIdentity() produced an error: %+v", err)
+		}
+
+		idsGenerated[getIDFp(testID.EphemeralIdentity)] = nil
+
+	}
+
+	//get one
+	var idu []IdentityUse
+	o := func(a []IdentityUse) error {
+		idu = a
+		return nil
+	}
+	err := s.ForEach(1, prng, 15, o)
 	if err != nil {
-		t.Fatalf("Failed to generate fake ID: %+v", err)
+		t.Errorf("GetIdentity() produced an error: %+v", err)
 	}
-	if s.AddIdentity(testID.Identity) != nil {
-		t.Errorf("AddIdentity() produced an error: %+v", err)
+
+	if _, exists := idsGenerated[getIDFp(idu[0].EphemeralIdentity)]; !exists ||
+		idu[0].Fake {
+		t.Errorf("An unknown or fake identity was returned")
+	}
+
+	//get three
+	err = s.ForEach(3, prng, 15, o)
+	if err != nil {
+		t.Errorf("GetIdentity() produced an error: %+v", err)
+	}
+
+	if len(idu) != 3 {
+		t.Errorf("the wrong number of identities was returned")
+	}
+
+	for i := 0; i < len(idu); i++ {
+		if _, exists := idsGenerated[getIDFp(idu[i].EphemeralIdentity)]; !exists ||
+			idu[i].Fake {
+			t.Errorf("An unknown or fake identity was returned")
+		}
+	}
+
+	//get ten
+	err = s.ForEach(10, prng, 15, o)
+	if err != nil {
+		t.Errorf("GetIdentity() produced an error: %+v", err)
+	}
+
+	if len(idu) != 10 {
+		t.Errorf("the wrong number of identities was returned")
+	}
+
+	for i := 0; i < len(idu); i++ {
+		if _, exists := idsGenerated[getIDFp(idu[i].EphemeralIdentity)]; !exists ||
+			idu[i].Fake {
+			t.Errorf("An unknown or fake identity was returned")
+		}
+	}
+
+	//get fifty
+	err = s.ForEach(50, prng, 15, o)
+	if err != nil {
+		t.Errorf("GetIdentity() produced an error: %+v", err)
+	}
+
+	if len(idu) != 50 {
+		t.Errorf("the wrong number of identities was returned")
+	}
+
+	for i := 0; i < len(idu); i++ {
+		if _, exists := idsGenerated[getIDFp(idu[i].EphemeralIdentity)]; !exists ||
+			idu[i].Fake {
+			t.Errorf("An unknown or fake identity was returned")
+		}
 	}
 
-	idu, err := s.GetIdentity(prng, 15)
+	//get 100
+	err = s.ForEach(100, prng, 15, o)
 	if err != nil {
 		t.Errorf("GetIdentity() produced an error: %+v", err)
 	}
 
-	if !testID.Equal(idu.Identity) {
-		t.Errorf("GetIdentity() did not return the expected Identity."+
-			"\nexpected: %s\nreceived: %s", testID, idu)
+	if len(idu) != 100 {
+		t.Errorf("the wrong number of identities was returned")
 	}
+
+	for i := 0; i < len(idu); i++ {
+		if _, exists := idsGenerated[getIDFp(idu[i].EphemeralIdentity)]; !exists ||
+			idu[i].Fake {
+			t.Errorf("An unknown or fake identity was returned")
+		}
+	}
+
+	//get 1000, should only return 100
+	err = s.ForEach(1000, prng, 15, o)
+	if err != nil {
+		t.Errorf("GetIdentity() produced an error: %+v", err)
+	}
+
+	if len(idu) != 100 {
+		t.Errorf("the wrong number of identities was returned")
+	}
+
+	for i := 0; i < len(idu); i++ {
+		if _, exists := idsGenerated[getIDFp(idu[i].EphemeralIdentity)]; !exists ||
+			idu[i].Fake {
+			t.Errorf("An unknown or fake identity was returned")
+		}
+	}
+
+	// get 100 a second time and make sure the order is not the same as a
+	// smoke test that the shuffle is working
+	var idu2 []IdentityUse
+	o2 := func(a []IdentityUse) error {
+		idu2 = a
+		return nil
+	}
+
+	err = s.ForEach(1000, prng, 15, o2)
+	if err != nil {
+		t.Errorf("GetIdentity() produced an error: %+v", err)
+	}
+
+	diferent := false
+	for i := 0; i < len(idu); i++ {
+		if !idu[i].Source.Cmp(idu2[i].Source) {
+			diferent = true
+			break
+		}
+	}
+
+	if !diferent {
+		t.Errorf("The 2 100 shuffels retruned the same result, shuffling" +
+			" is likley not occuring")
+	}
+
+}
+
+func TestStore_GetIdentities_NoIdentities(t *testing.T) {
+	kv := versioned.NewKV(ekv.MakeMemstore())
+	s := NewOrLoadStore(kv)
+	prng := rand.New(rand.NewSource(42))
+
+	var idu []IdentityUse
+	o := func(a []IdentityUse) error {
+		idu = a
+		return nil
+	}
+
+	err := s.ForEach(5, prng, 15, o)
+	if err != nil {
+		t.Errorf("GetIdentities() produced an error: %+v", err)
+	}
+
+	if len(idu) != 1 {
+		t.Errorf("GetIdenties() did not return only one identity " +
+			"when looking for a fake")
+	}
+
+	if !idu[0].Fake {
+		t.Errorf("GetIdenties() did not return a fake identity " +
+			"when only one is avalible")
+	}
+}
+
+func TestStore_GetIdentities_BadNum(t *testing.T) {
+	kv := versioned.NewKV(ekv.MakeMemstore())
+	s := NewOrLoadStore(kv)
+	prng := rand.New(rand.NewSource(42))
+
+	o := func(a []IdentityUse) error {
+		return nil
+	}
+
+	err := s.ForEach(0, prng, 15, o)
+	if err == nil {
+		t.Errorf("GetIdentities() shoud error with bad num value")
+	}
+
+	err = s.ForEach(-1, prng, 15, o)
+	if err == nil {
+		t.Errorf("GetIdentities() shoud error with bad num value")
+	}
+
+	err = s.ForEach(-100, prng, 15, o)
+	if err == nil {
+		t.Errorf("GetIdentities() shoud error with bad num value")
+	}
+
+	err = s.ForEach(-1000000, prng, 15, o)
+	if err == nil {
+		t.Errorf("GetIdentities() shoud error with bad num value")
+	}
+
+	err = s.ForEach(math.MinInt64, prng, 15, o)
+	if err == nil {
+		t.Errorf("GetIdentities() shoud error with bad num value")
+	}
+}
+
+func getIDFp(identity EphemeralIdentity) uint64 {
+	h, _ := hash.NewCMixHash()
+	h.Write(identity.EphId[:])
+	h.Write(identity.Source.Bytes())
+	r := h.Sum(nil)
+	return binary.BigEndian.Uint64(r)
 }
 
 func TestStore_AddIdentity(t *testing.T) {
diff --git a/cmix/identity/tracker.go b/cmix/identity/tracker.go
index f7afdb6d8b6d3221c84817850c65715dec76bc16..7ab08da034fbdd1dbbbfd35ff54b7fc72c273929 100644
--- a/cmix/identity/tracker.go
+++ b/cmix/identity/tracker.go
@@ -49,7 +49,8 @@ type Tracker interface {
 	StartProcesses() stoppable.Stoppable
 	AddIdentity(id *id.ID, validUntil time.Time, persistent bool)
 	RemoveIdentity(id *id.ID)
-	GetEphemeralIdentity(rng io.Reader, addressSize uint8) (receptionID.IdentityUse, error)
+	ForEach(n int, rng io.Reader, addressSize uint8,
+		operator func([]receptionID.IdentityUse) error) error
 	GetIdentity(get *id.ID) (TrackedID, error)
 }
 
@@ -142,10 +143,14 @@ func (t *manager) RemoveIdentity(id *id.ID) {
 	t.deleteIdentity <- id
 }
 
-// GetEphemeralIdentity returns an ephemeral Identity to poll the network with.
-func (t *manager) GetEphemeralIdentity(rng io.Reader, addressSize uint8) (
-	receptionID.IdentityUse, error) {
-	return t.ephemeral.GetIdentity(rng, addressSize)
+// ForEach passes a fisher-yates shuffled list of up to 'num'
+// ephemeral identities into the operation function. It will pass a
+// fake identity if none are available
+// and less than 'num' if less than 'num' are available.
+// 'num' must be positive non-zero
+func (t *manager) ForEach(n int, rng io.Reader, addressSize uint8,
+	operator func([]receptionID.IdentityUse) error) error {
+	return t.ephemeral.ForEach(n, rng, addressSize, operator)
 }
 
 // GetIdentity returns a currently tracked identity
@@ -207,9 +212,11 @@ func (t *manager) track(stop *stoppable.Single) {
 			continue
 
 		case deleteID := <-t.deleteIdentity:
+			removed := false
 			for i := range t.tracked {
 				inQuestion := t.tracked[i]
 				if inQuestion.Source.Cmp(deleteID) {
+					removed = true
 					t.tracked = append(t.tracked[:i], t.tracked[i+1:]...)
 					t.save()
 					// Requires manual deletion in case identity is deleted before expiration
@@ -217,6 +224,9 @@ func (t *manager) track(stop *stoppable.Single) {
 					break
 				}
 			}
+			if !removed {
+				jww.WARN.Printf("Identity %s failed to be removed from tracker", deleteID)
+			}
 		case <-stop.Quit():
 			t.addrSpace.UnregisterAddressSpaceNotification(addressSpaceSizeChanTag)
 			stop.ToStopped()
diff --git a/cmix/interface.go b/cmix/interface.go
index 896b05a02a9f7c68ba67756eae8f66bc04acde93..f6572fb1dbdf6811052cb5d541e05286288c20cb 100644
--- a/cmix/interface.go
+++ b/cmix/interface.go
@@ -58,7 +58,7 @@ type Client interface {
 	// WARNING: Do not roll your own crypto.
 	Send(recipient *id.ID, fingerprint format.Fingerprint,
 		service message.Service, payload, mac []byte, cmixParams CMIXParams) (
-		id.Round, ephemeral.Id, error)
+		rounds.Round, ephemeral.Id, error)
 
 	// SendMany sends many "raw" cMix message payloads to the provided
 	// recipients all in the same round.
@@ -85,7 +85,25 @@ type Client interface {
 	// (along with the reason). Blocks until successful send or err.
 	// WARNING: Do not roll your own crypto.
 	SendMany(messages []TargetedCmixMessage, p CMIXParams) (
-		id.Round, []ephemeral.Id, error)
+		rounds.Round, []ephemeral.Id, error)
+
+	// SendWithAssembler sends a variable cmix payload to the provided recipient.
+	// The payload sent is based on the Complier function passed in, which accepts
+	// a round ID and returns the necessary payload data.
+	// Returns the round ID of the round the payload was sent or an error if it
+	// fails.
+	// This does not have end-to-end encryption on it and is used exclusively as
+	// a send for higher order cryptographic protocols. Do not use unless
+	// implementing a protocol on top.
+	//   recipient - cMix ID of the recipient.
+	//   assembler - MessageAssembler function, accepting round ID and returning
+	//   fingerprint
+	//   format.Fingerprint, service message.Service, payload, mac []byte
+	// Will return an error if the network is unhealthy or if it fails to send
+	// (along with the reason). Blocks until successful sends or errors.
+	// WARNING: Do not roll your own crypto.
+	SendWithAssembler(recipient *id.ID, assembler MessageAssembler,
+		cmixParams CMIXParams) (rounds.Round, ephemeral.Id, error)
 
 	/* === Message Reception ================================================ */
 	/* Identities are all network identities which the client is currently
@@ -236,7 +254,7 @@ type Client interface {
 	// GetRoundResults adjudicates on the rounds requested. Checks if they are
 	// older rounds or in progress rounds.
 	GetRoundResults(timeout time.Duration, roundCallback RoundEventCallback,
-		roundList ...id.Round) error
+		roundList ...id.Round)
 
 	// LookupHistoricalRound looks up the passed historical round on the network.
 	// GetRoundResults does this lookup when needed, generally that is
@@ -297,6 +315,17 @@ type Client interface {
 
 type ClientErrorReport func(source, message, trace string)
 
+// MessageAssembler func accepts a round ID, returning fingerprint, service,
+// payload & mac. This allows users to pass in a paylaod which will contain the
+// round ID over which the message is sent.
+type MessageAssembler func(rid id.Round) (fingerprint format.Fingerprint,
+	service message.Service, payload, mac []byte, err error)
+
+// messageAssembler is an internal wrapper around MessageAssembler which
+// returns a format.message This is necessary to preserve the interaction
+// between sendCmixHelper and critical messages
+type messageAssembler func(rid id.Round) (format.Message, error)
+
 type clientCommsInterface interface {
 	followNetworkComms
 	SendCmixCommsInterface
diff --git a/cmix/localTime.go b/cmix/localTime.go
new file mode 100644
index 0000000000000000000000000000000000000000..b0b51d4bd2034d71e06321a467ff28907adf9980
--- /dev/null
+++ b/cmix/localTime.go
@@ -0,0 +1,19 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+package cmix
+
+import "time"
+
+// describes a local time object which gets time
+// from the local clock in milliseconds
+type localTime struct{}
+
+func (localTime) NowMs() int64 {
+	t := time.Now()
+	return (t.UnixNano() + int64(time.Millisecond)/2 + 1) / int64(time.Millisecond)
+}
diff --git a/cmix/message/bundle.go b/cmix/message/bundle.go
index 88db932017ff4f33b714d782444c5264f5f1800f..7e8b20fb943988e00e39e27bbc9dc68ed3145b14 100644
--- a/cmix/message/bundle.go
+++ b/cmix/message/bundle.go
@@ -8,8 +8,8 @@
 package message
 
 import (
-	"gitlab.com/elixxir/client/cmix/rounds"
 	"gitlab.com/elixxir/client/cmix/identity/receptionID"
+	"gitlab.com/elixxir/client/cmix/rounds"
 	"gitlab.com/elixxir/primitives/format"
 	"gitlab.com/xx_network/primitives/id"
 )
diff --git a/cmix/message/meteredCmixMessageBuffer.go b/cmix/message/meteredCmixMessageBuffer.go
index 79604962bb8cfe8d076e3516096c80f04d3ee42b..826f8cab50f4e645652229830a56afd459fd70ad 100644
--- a/cmix/message/meteredCmixMessageBuffer.go
+++ b/cmix/message/meteredCmixMessageBuffer.go
@@ -11,6 +11,7 @@ import (
 	"encoding/json"
 	"time"
 
+	"github.com/golang/protobuf/proto"
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/cmix/identity/receptionID"
@@ -21,7 +22,6 @@ import (
 	"gitlab.com/elixxir/primitives/states"
 	"gitlab.com/xx_network/primitives/netTime"
 	"golang.org/x/crypto/blake2b"
-	"google.golang.org/protobuf/proto"
 )
 
 type meteredCmixMessageHandler struct{}
diff --git a/cmix/nodes/register.go b/cmix/nodes/register.go
index 5d2249ec617e7d39af16bcdd58cc0bd0d7879558..8cada7d120e663cf9ef075d11fe72c3c409c7859 100644
--- a/cmix/nodes/register.go
+++ b/cmix/nodes/register.go
@@ -134,6 +134,8 @@ func registerWithNode(sender gateway.Sender, comms RegisterNodeCommsInterface,
 	var transmissionKey *cyclic.Int
 	var validUntil uint64
 	var keyId []byte
+
+	start := time.Now()
 	// TODO: should move this to a pre-canned user initialization
 	if s.IsPrecanned() {
 		userNum := int(s.GetTransmissionID().Bytes()[7])
@@ -156,7 +158,8 @@ func registerWithNode(sender gateway.Sender, comms RegisterNodeCommsInterface,
 
 	r.add(nodeID, transmissionKey, validUntil, keyId)
 
-	jww.INFO.Printf("Completed registration with node %s", nodeID)
+	jww.INFO.Printf("Completed registration with node %s,"+
+		" took %d", nodeID, time.Since(start))
 
 	return nil
 }
diff --git a/cmix/nodes/register_test.go b/cmix/nodes/register_test.go
index 7349cd6ec55e61c484251a412576d3e87745b771..8ed9e6c897869e8ad9b5142b20a0e66e992b5621 100644
--- a/cmix/nodes/register_test.go
+++ b/cmix/nodes/register_test.go
@@ -88,4 +88,3 @@ func TestRegisterWithNode(t *testing.T) {
 		t.Fatalf("registerWithNode error: %+v", err)
 	}
 }
-
diff --git a/cmix/nodes/registrar.go b/cmix/nodes/registrar.go
index dc61852f183937b5f4f9eadcda2cdfacd28d20a9..5ca5f108d201de75fcb739c43ea1bf2cea24951b 100644
--- a/cmix/nodes/registrar.go
+++ b/cmix/nodes/registrar.go
@@ -24,15 +24,15 @@ import (
 )
 
 const InputChanLen = 1000
-const maxAttempts = 5
+const maxAttempts = 2
 
 // Backoff for attempting to register with a cMix node.
 var delayTable = [5]time.Duration{
 	0,
-	5 * time.Second,
 	30 * time.Second,
 	60 * time.Second,
 	120 * time.Second,
+	240 * time.Second,
 }
 
 // registrar is an implementation of the Registrar interface.
diff --git a/cmix/nodes/request.go b/cmix/nodes/request.go
index 499acb4550b2ff31ebf2e4a2d4a8f8c0ef1c707b..abee1dfff97fb6621bc7079635332de833f82e00 100644
--- a/cmix/nodes/request.go
+++ b/cmix/nodes/request.go
@@ -8,6 +8,9 @@
 package nodes
 
 import (
+	"io"
+	"time"
+
 	"github.com/golang/protobuf/proto"
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
@@ -24,10 +27,8 @@ import (
 	"gitlab.com/xx_network/crypto/chacha"
 	"gitlab.com/xx_network/crypto/csprng"
 	"gitlab.com/xx_network/crypto/signature/rsa"
-	"gitlab.com/xx_network/crypto/tls"
 	"gitlab.com/xx_network/primitives/id"
 	"gitlab.com/xx_network/primitives/netTime"
-	"io"
 )
 
 // requestKey is a helper function which constructs a ClientKeyRequest message.
@@ -41,9 +42,9 @@ func requestKey(sender gateway.Sender, comms RegisterNodeCommsInterface,
 	// Generate a Diffie-Hellman keypair
 	grp := r.session.GetCmixGroup()
 
+	start := time.Now()
 	prime := grp.GetPBytes()
-	keyLen := len(prime)
-	dhPrivBytes, err := csprng.GenerateInGroup(prime, keyLen, rng)
+	dhPrivBytes, err := csprng.GenerateInGroup(prime, 32, rng)
 	if err != nil {
 		return nil, nil, 0, err
 	}
@@ -66,9 +67,11 @@ func requestKey(sender gateway.Sender, comms RegisterNodeCommsInterface,
 
 	// Request nonce message from gateway
 	jww.INFO.Printf("Register: Requesting client key from "+
-		"gateway %s", gatewayID)
+		"gateway %s, setup took %s", gatewayID, time.Since(start))
 
+	start = time.Now()
 	result, err := sender.SendToAny(func(host *connect.Host) (interface{}, error) {
+		startInternal := time.Now()
 		keyResponse, err2 := comms.SendRequestClientKeyMessage(host, signedKeyReq)
 		if err2 != nil {
 			return nil, errors.WithMessagef(err2,
@@ -78,9 +81,11 @@ func requestKey(sender gateway.Sender, comms RegisterNodeCommsInterface,
 			return nil, errors.WithMessage(err2,
 				"requestKey: clientKeyResponse error")
 		}
+		jww.TRACE.Printf("just comm reg request took %s", time.Since(startInternal))
 
 		return keyResponse, nil
 	}, stop)
+	jww.TRACE.Printf("full reg request took %s", time.Since(start))
 
 	if err != nil {
 		return nil, nil, 0, err
@@ -169,22 +174,8 @@ func processRequestResponse(signedKeyResponse *pb.SignedKeyResponse,
 	h.Write(signedKeyResponse.KeyResponse)
 	hashedResponse := h.Sum(nil)
 
-	// Load nodes certificate
-	gatewayCert, err := tls.LoadCertificate(ngw.Gateway.TlsCertificate)
-	if err != nil {
-		return nil, nil, 0,
-			errors.Errorf("Unable to load nodes's certificate: %+v", err)
-	}
-
-	// Extract public key
-	nodePubKey, err := tls.ExtractPublicKey(gatewayCert)
-	if err != nil {
-		return nil, nil, 0,
-			errors.Errorf("Unable to load node's public key: %v", err)
-	}
-
 	// Verify the response signature
-	err = rsa.Verify(nodePubKey, opts.Hash, hashedResponse,
+	err := verifyNodeSignature(ngw.Gateway.TlsCertificate, opts.Hash, hashedResponse,
 		signedKeyResponse.KeyResponseSignedByGateway.Signature, opts)
 	if err != nil {
 		return nil, nil, 0,
@@ -202,11 +193,14 @@ func processRequestResponse(signedKeyResponse *pb.SignedKeyResponse,
 	// Convert Node DH Public key to a cyclic.Int
 	nodeDHPub := grp.NewIntFromBytes(keyResponse.NodeDHPubKey)
 
+	start := time.Now()
 	// Construct the session key
 	h.Reset()
 	sessionKey := registration.GenerateBaseKey(grp,
 		nodeDHPub, dhPrivKey, h)
 
+	jww.TRACE.Printf("DH for reg took %s", time.Since(start))
+
 	// Verify the HMAC
 	if !registration.VerifyClientHMAC(sessionKey.Bytes(),
 		keyResponse.EncryptedClientKey, opts.Hash.New,
diff --git a/cmix/nodes/verifyNodeSig.go b/cmix/nodes/verifyNodeSig.go
new file mode 100644
index 0000000000000000000000000000000000000000..55ae44c551fceffc76c170ee213d3e7b7db3dc8b
--- /dev/null
+++ b/cmix/nodes/verifyNodeSig.go
@@ -0,0 +1,37 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build !js || !wasm
+
+package nodes
+
+import (
+	"crypto"
+	"github.com/pkg/errors"
+	"gitlab.com/xx_network/crypto/tls"
+
+	"gitlab.com/xx_network/crypto/signature/rsa"
+)
+
+func verifyNodeSignature(certContents string, hash crypto.Hash,
+	hashed []byte, sig []byte, opts *rsa.Options) error {
+
+	// Load nodes certificate
+	gatewayCert, err := tls.LoadCertificate(certContents)
+	if err != nil {
+		return errors.Errorf("Unable to load nodes's certificate: %+v", err)
+	}
+
+	// Extract public key
+	nodePubKey, err := tls.ExtractPublicKey(gatewayCert)
+	if err != nil {
+		return errors.Errorf("Unable to load node's public key: %v", err)
+	}
+
+	// Verify the response signature
+	return rsa.Verify(nodePubKey, hash, hashed, sig, opts)
+}
diff --git a/cmix/nodes/verifyNodeSig_js.go b/cmix/nodes/verifyNodeSig_js.go
new file mode 100644
index 0000000000000000000000000000000000000000..6bc339bd38273160ce8a13515225c944d2d7f3a8
--- /dev/null
+++ b/cmix/nodes/verifyNodeSig_js.go
@@ -0,0 +1,23 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package nodes
+
+import (
+	"crypto"
+
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/xx_network/crypto/signature/rsa"
+)
+
+func verifyNodeSignature(pub string, hash crypto.Hash,
+	hashed []byte, sig []byte, opts *rsa.Options) error {
+	jww.WARN.Printf("node signature checking disabled for wasm")
+	return nil
+}
diff --git a/cmix/params.go b/cmix/params.go
index 95344b5490b6b0e6ae55de290fd04a03712b42c9..3ef813f0d9ca6e7903696f83cb6fea72e62a7962 100644
--- a/cmix/params.go
+++ b/cmix/params.go
@@ -10,6 +10,7 @@ package cmix
 import (
 	"encoding/base64"
 	"encoding/json"
+	"fmt"
 	"time"
 
 	"gitlab.com/elixxir/client/cmix/message"
@@ -58,6 +59,14 @@ type Params struct {
 	// times.
 	ReplayRequests bool
 
+	// MaxParallelIdentityTracks is the maximum number of parallel identities
+	// the system will poll in one iteration of the follower
+	MaxParallelIdentityTracks uint
+
+	// ClockSkewClamp is the window (+/-) in which clock skew is
+	// ignored and local time is used
+	ClockSkewClamp time.Duration
+
 	Rounds     rounds.Params
 	Pickup     pickup.Params
 	Message    message.Params
@@ -80,6 +89,7 @@ type paramsDisk struct {
 	Pickup                    pickup.Params
 	Message                   message.Params
 	Historical                rounds.Params
+	MaxParallelIdentityTracks uint
 }
 
 // GetDefaultParams returns a Params object containing the
@@ -89,13 +99,15 @@ func GetDefaultParams() Params {
 		TrackNetworkPeriod:        100 * time.Millisecond,
 		MaxCheckedRounds:          500,
 		RegNodesBufferLen:         1000,
-		NetworkHealthTimeout:      30 * time.Second,
+		NetworkHealthTimeout:      15 * time.Second,
 		ParallelNodeRegistrations: 20,
 		KnownRoundsThreshold:      1500, // 5 rounds/sec * 60 sec/min * 5 min
 		FastPolling:               true,
 		VerboseRoundTracking:      false,
 		RealtimeOnly:              false,
 		ReplayRequests:            true,
+		MaxParallelIdentityTracks: 20,
+		ClockSkewClamp:            50 * time.Millisecond,
 	}
 	n.Rounds = rounds.GetDefaultParams()
 	n.Pickup = pickup.GetDefaultParams()
@@ -135,6 +147,7 @@ func (p Params) MarshalJSON() ([]byte, error) {
 		Pickup:                    p.Pickup,
 		Message:                   p.Message,
 		Historical:                p.Historical,
+		MaxParallelIdentityTracks: p.MaxParallelIdentityTracks,
 	}
 
 	return json.Marshal(&pDisk)
@@ -163,6 +176,7 @@ func (p *Params) UnmarshalJSON(data []byte) error {
 		Pickup:                    pDisk.Pickup,
 		Message:                   pDisk.Message,
 		Historical:                pDisk.Historical,
+		MaxParallelIdentityTracks: pDisk.MaxParallelIdentityTracks,
 	}
 
 	return nil
@@ -206,6 +220,10 @@ type CMIXParams struct {
 	// should only be used in cases where repeats cannot be different. Only used
 	// in sendCmix, not sendManyCmix.
 	Critical bool
+
+	// Probe tells the client that this send can be used to test network performance,
+	// that outgoing latency is not important
+	Probe bool
 }
 
 // cMixParamsDisk will be the marshal-able and umarshal-able object.
@@ -228,7 +246,8 @@ func GetDefaultCMIXParams() CMIXParams {
 		DebugTag:    DefaultDebugTag,
 		// Unused stoppable so components that require one have a channel to
 		// wait on
-		Stop: stoppable.NewSingle("cmixParamsDefault"),
+		Stop:  stoppable.NewSingle("cmixParamsDefault"),
+		Probe: false,
 	}
 }
 
@@ -282,6 +301,18 @@ func (p *CMIXParams) UnmarshalJSON(data []byte) error {
 	return nil
 }
 
+// SetDebugTag appends the debug tag if one already exists,
+// otherwise it just used the new debug tag
+func (p CMIXParams) SetDebugTag(newTag string) CMIXParams {
+	if p.DebugTag != DefaultDebugTag {
+		p.DebugTag = fmt.Sprintf("%s-%s", p.DebugTag, newTag)
+	} else {
+		p.DebugTag = newTag
+	}
+
+	return p
+}
+
 // NodeMap represents a map of nodes and whether they have been
 // blacklisted. This is designed for use with CMIXParams.BlacklistedNodes
 type NodeMap map[id.ID]bool
diff --git a/cmix/results.go b/cmix/results.go
index 7c510525ea6bb0ba6bd073892327dc2c8f421702..916bdb206610fc2c98ed0509acba705d6c9c1a33 100644
--- a/cmix/results.go
+++ b/cmix/results.go
@@ -65,19 +65,19 @@ type RoundEventCallback func(allRoundsSucceeded, timedOut bool, rounds map[id.Ro
 // GetRoundResults adjudicates on the rounds requested. Checks if they are
 // older rounds or in progress rounds.
 func (c *client) GetRoundResults(timeout time.Duration,
-	roundCallback RoundEventCallback, roundList ...id.Round) error {
+	roundCallback RoundEventCallback, roundList ...id.Round) {
 
 	jww.INFO.Printf("GetRoundResults(%v, %s)", roundList, timeout)
 
 	sendResults := make(chan ds.EventReturn, len(roundList))
 
-	return c.getRoundResults(roundList, timeout, roundCallback,
+	c.getRoundResults(roundList, timeout, roundCallback,
 		sendResults)
 }
 
 // Helper function which does all the logic for GetRoundResults
 func (c *client) getRoundResults(roundList []id.Round, timeout time.Duration,
-	roundCallback RoundEventCallback, sendResults chan ds.EventReturn) error {
+	roundCallback RoundEventCallback, sendResults chan ds.EventReturn) {
 
 	networkInstance := c.GetInstance()
 
@@ -221,6 +221,4 @@ func (c *client) getRoundResults(roundList []id.Round, timeout time.Duration,
 
 		}
 	}()
-
-	return nil
 }
diff --git a/cmix/sendCmix.go b/cmix/sendCmix.go
index 28253c0c26b26beb19345048b668a3684aab5075..87f882333c1ee4a7883a2f6c966e82758d2a1163 100644
--- a/cmix/sendCmix.go
+++ b/cmix/sendCmix.go
@@ -9,6 +9,9 @@ package cmix
 
 import (
 	"fmt"
+	"gitlab.com/elixxir/client/cmix/attempts"
+	"gitlab.com/elixxir/client/cmix/rounds"
+	"gitlab.com/elixxir/primitives/states"
 	"strings"
 	"time"
 
@@ -56,42 +59,87 @@ import (
 // WARNING: Do not roll your own crypto.
 func (c *client) Send(recipient *id.ID, fingerprint format.Fingerprint,
 	service message.Service, payload, mac []byte, cmixParams CMIXParams) (
-	id.Round, ephemeral.Id, error) {
+	rounds.Round, ephemeral.Id, error) {
+	// create an internal assembler function to pass to sendWithAssembler
+	assembler := func(rid id.Round) (format.Fingerprint, message.Service,
+		[]byte, []byte, error) {
+		return fingerprint, service, payload, mac, nil
+	}
+	return c.sendWithAssembler(recipient, assembler, cmixParams)
+}
+
+// SendWithAssembler sends a variable cmix payload to the provided recipient.
+// The payload sent is based on the Complier function passed in, which accepts
+// a round ID and returns the necessary payload data.
+// Returns the round ID of the round the payload was sent or an error if it
+// fails.
+// This does not have end-to-end encryption on it and is used exclusively as
+// a send for higher order cryptographic protocols. Do not use unless
+// implementing a protocol on top.
+//   recipient - cMix ID of the recipient.
+//   assembler - MessageAssembler function, accepting round ID and returning fingerprint
+//   format.Fingerprint, service message.Service, payload, mac []byte
+// Will return an error if the network is unhealthy or if it fails to send
+// (along with the reason). Blocks until successful sends or errors.
+// WARNING: Do not roll your own crypto.
+func (c *client) SendWithAssembler(recipient *id.ID, assembler MessageAssembler, cmixParams CMIXParams) (
+	rounds.Round, ephemeral.Id, error) {
+	// Critical messaging and assembler-based message payloads are not compatible
+	if cmixParams.Critical {
+		return rounds.Round{}, ephemeral.Id{}, errors.New("Cannot send critical messages with a message assembler")
+	}
+	return c.sendWithAssembler(recipient, assembler, cmixParams)
+}
+
+// sendWithAssembler wraps the passed in MessageAssembler in a messageAssembler for sendCmixHelper,
+// and sets up critical message handling where applicable.
+func (c *client) sendWithAssembler(recipient *id.ID, assembler MessageAssembler, cmixParams CMIXParams) (
+	rounds.Round, ephemeral.Id, error) {
 	if !c.Monitor.IsHealthy() {
-		return 0, ephemeral.Id{}, errors.New(
+		return rounds.Round{}, ephemeral.Id{}, errors.New(
 			"Cannot send cmix message when the network is not healthy")
 	}
 
-	if len(payload) != c.maxMsgLen {
-		return 0, ephemeral.Id{}, errors.Errorf(
-			"bad message length (%d, need %d)",
-			len(payload), c.maxMsgLen)
-	}
+	// Create an internal messageAssembler which returns a format.Message
+	assemblerFunc := func(rid id.Round) (format.Message, error) {
+		fingerprint, service, payload, mac, err := assembler(rid)
 
-	// Build message. Will panic if inputs are not correct.
-	msg := format.NewMessage(c.session.GetCmixGroup().GetP().ByteLen())
-	msg.SetContents(payload)
-	msg.SetKeyFP(fingerprint)
-	msg.SetSIH(service.Hash(msg.GetContents()))
-	msg.SetMac(mac)
+		if err != nil {
+			return format.Message{}, err
+		}
 
-	jww.TRACE.Printf("sendCmix Contents: %v, KeyFP: %v, MAC: %v, SIH: %v",
-		msg.GetContents(), msg.GetKeyFP(), msg.GetMac(),
-		msg.GetSIH())
+		if len(payload) != c.maxMsgLen {
+			return format.Message{}, errors.Errorf(
+				"bad message length (%d, need %d)",
+				len(payload), c.maxMsgLen)
+		}
 
-	if cmixParams.Critical {
-		c.crit.AddProcessing(msg, recipient, cmixParams)
+		// Build message. Will panic if inputs are not correct.
+		msg := format.NewMessage(c.session.GetCmixGroup().GetP().ByteLen())
+		msg.SetContents(payload)
+		msg.SetKeyFP(fingerprint)
+		msg.SetSIH(service.Hash(msg.GetContents()))
+		msg.SetMac(mac)
+
+		jww.TRACE.Printf("sendCmix Contents: %v, KeyFP: %v, MAC: %v, SIH: %v",
+			msg.GetContents(), msg.GetKeyFP(), msg.GetMac(),
+			msg.GetSIH())
+
+		if cmixParams.Critical {
+			c.crit.AddProcessing(msg, recipient, cmixParams)
+		}
+		return msg, nil
 	}
 
-	rid, ephID, rtnErr := sendCmixHelper(c.Sender, msg, recipient, cmixParams,
+	r, ephID, msg, rtnErr := sendCmixHelper(c.Sender, assemblerFunc, recipient, cmixParams,
 		c.instance, c.session.GetCmixGroup(), c.Registrar, c.rng, c.events,
-		c.session.GetTransmissionID(), c.comms)
+		c.session.GetTransmissionID(), c.comms, c.attemptTracker)
 
 	if cmixParams.Critical {
-		c.crit.handle(msg, recipient, rid, rtnErr)
+		c.crit.handle(msg, recipient, r.ID, rtnErr)
 	}
 
-	return rid, ephID, rtnErr
+	return r, ephID, rtnErr
 }
 
 // sendCmixHelper is a helper function for client.SendCMIX.
@@ -104,13 +152,13 @@ func (c *client) Send(recipient *id.ID, fingerprint format.Fingerprint,
 // If the message is successfully sent, the ID of the round sent it is returned,
 // which can be registered with the network instance to get a callback on its
 // status.
-func sendCmixHelper(sender gateway.Sender, msg format.Message, recipient *id.ID,
+func sendCmixHelper(sender gateway.Sender, assembler messageAssembler, recipient *id.ID,
 	cmixParams CMIXParams, instance *network.Instance, grp *cyclic.Group,
 	nodes nodes.Registrar, rng *fastRNG.StreamGenerator, events event.Reporter,
-	senderId *id.ID, comms SendCmixCommsInterface) (id.Round, ephemeral.Id, error) {
+	senderId *id.ID, comms SendCmixCommsInterface, attemptTracker attempts.SendAttemptTracker) (rounds.Round, ephemeral.Id, format.Message, error) {
 
 	if cmixParams.RoundTries == 0 {
-		return 0, ephemeral.Id{},
+		return rounds.Round{}, ephemeral.Id{}, format.Message{},
 			errors.Errorf("invalid parameter set, "+
 				"RoundTries cannot be 0: %+v", cmixParams)
 	}
@@ -125,48 +173,57 @@ func sendCmixHelper(sender gateway.Sender, msg format.Message, recipient *id.ID,
 		attempted = excludedRounds.NewSet()
 	}
 
-	jww.INFO.Printf("[Send-%s] Looking for round to send cMix message to "+
-		"%s (msgDigest: %s)", cmixParams.DebugTag, recipient,
-		msg.Digest())
-
 	stream := rng.GetStream()
 	defer stream.Close()
 
-	// Flip leading bits randomly to thwart a tagging attack.
-	// See cmix.SetGroupBits for more info.
-	cmix.SetGroupBits(msg, grp, stream)
+	numAttempts := 0
+	if !cmixParams.Probe {
+		optimalAttempts, ready := attemptTracker.GetOptimalNumAttempts()
+		if ready {
+			numAttempts = optimalAttempts
+			jww.INFO.Printf("[Send-%s] Looking for round to send cMix message to "+
+				"%s, sending non probe with %d optimalAttempts", cmixParams.DebugTag, recipient, numAttempts)
+		} else {
+			numAttempts = 4
+			jww.INFO.Printf("[Send-%s] Looking for round to send cMix message to "+
+				"%s, sending non probe with %d non optimalAttempts, insufficient data",
+				cmixParams.DebugTag, recipient, numAttempts)
+		}
+	} else {
+		jww.INFO.Printf("[Send-%s] Looking for round to send cMix message to "+
+			"%s, sending probe with %d Attempts, insufficient data",
+			cmixParams.DebugTag, recipient, numAttempts)
+		defer attemptTracker.SubmitProbeAttempt(numAttempts)
+	}
 
-	for numRoundTries := uint(
-		0); numRoundTries < cmixParams.RoundTries; numRoundTries++ {
+	for numRoundTries := uint(0); numRoundTries < cmixParams.RoundTries; numRoundTries,
+		numAttempts = numRoundTries+1, numAttempts+1 {
 		elapsed := netTime.Since(timeStart)
 		jww.TRACE.Printf("[Send-%s] try %d, elapsed: %s",
 			cmixParams.DebugTag, numRoundTries, elapsed)
 
 		if elapsed > cmixParams.Timeout {
 			jww.INFO.Printf("[Send-%s] No rounds to send to %s "+
-				"(msgDigest: %s) were found before timeout %s",
-				cmixParams.DebugTag, recipient, msg.Digest(),
-				cmixParams.Timeout)
-			return 0, ephemeral.Id{}, errors.New(
-				"Sending cmix message timed out")
+				"were found before timeout %s",
+				cmixParams.DebugTag, recipient, cmixParams.Timeout)
+			return rounds.Round{}, ephemeral.Id{}, format.Message{}, errors.New("Sending cmix message timed out")
 		}
 
 		if numRoundTries > 0 {
-			jww.INFO.Printf("[Send-%s] Attempt %d to find round"+
-				" to send message to %s (msgDigest: %s)",
-				cmixParams.DebugTag,
-				numRoundTries+1, recipient, msg.Digest())
+			jww.INFO.Printf("[Send-%s] Attempt %d to find round to send "+
+				"message to %s", cmixParams.DebugTag,
+				numRoundTries+1, recipient)
 		}
 
+		startSearch := netTime.Now()
 		// Find the best round to send to, excluding attempted rounds
 		remainingTime := cmixParams.Timeout - elapsed
 		waitingRounds := instance.GetWaitingRounds()
-		bestRound, err := waitingRounds.GetUpcomingRealtime(
-			remainingTime, attempted, sendTimeBuffer)
+		bestRound, _, err := waitingRounds.GetUpcomingRealtime(
+			remainingTime, attempted, numAttempts, sendTimeBuffer)
 		if err != nil {
-			jww.WARN.Printf("[Send-%s] GetUpcomingRealtime failed "+
-				"(msgDigest: %s): %+v", cmixParams.DebugTag,
-				msg.Digest(), err)
+			jww.WARN.Printf("[Send-%s] failed to GetUpcomingRealtime: "+
+				"%+v", cmixParams.DebugTag, err)
 		}
 
 		if bestRound == nil {
@@ -176,8 +233,8 @@ func sendCmixHelper(sender gateway.Sender, msg format.Message, recipient *id.ID,
 			continue
 		}
 
-		jww.TRACE.Printf("[Send-%s] Best round found: %+v",
-			cmixParams.DebugTag, bestRound)
+		jww.DEBUG.Printf("[Send-%s] Best round found, took %s: %d",
+			cmixParams.DebugTag, netTime.Since(startSearch), bestRound.ID)
 
 		// Determine whether the selected round contains any
 		// nodes that are blacklisted by the CMIXParams object
@@ -203,6 +260,16 @@ func sendCmixHelper(sender gateway.Sender, msg format.Message, recipient *id.ID,
 			continue
 		}
 
+		msg, err := assembler(id.Round(bestRound.ID))
+		if err != nil {
+			jww.ERROR.Printf("Failed to compile message: %+v", err)
+			return rounds.Round{}, ephemeral.Id{}, format.Message{}, err
+		}
+
+		// Flip leading bits randomly to thwart a tagging attack.
+		// See cmix.SetGroupBits for more info.
+		cmix.SetGroupBits(msg, grp, stream)
+
 		// Retrieve host and key information from round
 		firstGateway, roundKeys, err := processRound(
 			nodes, bestRound, recipient.String(), msg.Digest())
@@ -219,13 +286,16 @@ func sendCmixHelper(sender gateway.Sender, msg format.Message, recipient *id.ID,
 		wrappedMsg, encMsg, ephID, err := buildSlotMessage(msg, recipient,
 			firstGateway, stream, senderId, bestRound, roundKeys)
 		if err != nil {
-			return 0, ephemeral.Id{}, err
+			return rounds.Round{}, ephemeral.Id{}, format.Message{}, err
 		}
 
+		timeRoundStart := time.Unix(0, int64(bestRound.Timestamps[states.QUEUED]))
+
 		jww.INFO.Printf("[Send-%s] Sending to EphID %d (%s), on round %d "+
-			"(msgDigest: %s, ecrMsgDigest: %s) via gateway %s",
-			cmixParams.DebugTag, ephID.Int64(), recipient, bestRound.ID,
-			msg.Digest(), encMsg.Digest(), firstGateway.String())
+			"(msgDigest: %s, ecrMsgDigest: %s) via gateway %s starting "+
+			"at %s (%s in the future)", cmixParams.DebugTag, ephID.Int64(),
+			recipient, bestRound.ID, msg.Digest(), encMsg.Digest(),
+			firstGateway.String(), timeRoundStart, netTime.Until(timeRoundStart))
 
 		// Send the payload
 		sendFunc := func(host *connect.Host, target *id.ID,
@@ -269,7 +339,7 @@ func sendCmixHelper(sender gateway.Sender, msg format.Message, recipient *id.ID,
 
 		// Exit if the thread has been stopped
 		if stoppable.CheckErr(err) {
-			return 0, ephemeral.Id{}, err
+			return rounds.Round{}, ephemeral.Id{}, format.Message{}, err
 		}
 
 		// If the comm errors or the message fails to send, continue retrying
@@ -278,7 +348,7 @@ func sendCmixHelper(sender gateway.Sender, msg format.Message, recipient *id.ID,
 				jww.ERROR.Printf("[Send-%s] SendCmix failed to send to "+
 					"EphID %d (%s) on round %d: %+v", cmixParams.DebugTag,
 					ephID.Int64(), recipient, bestRound.ID, err)
-				return 0, ephemeral.Id{}, err
+				return rounds.Round{}, ephemeral.Id{}, format.Message{}, err
 			}
 
 			jww.ERROR.Printf("[Send-%s] SendCmix failed to send to "+
@@ -298,7 +368,7 @@ func sendCmixHelper(sender gateway.Sender, msg format.Message, recipient *id.ID,
 			jww.INFO.Print(m)
 			events.Report(1, "MessageSend", "Metric", m)
 
-			return id.Round(bestRound.ID), ephID, nil
+			return rounds.MakeRound(bestRound), ephID, msg, nil
 		} else {
 			jww.FATAL.Panicf("[Send-%s] Gateway %s returned no error, "+
 				"but failed to accept message when sending to EphID %d (%s) "+
@@ -307,6 +377,6 @@ func sendCmixHelper(sender gateway.Sender, msg format.Message, recipient *id.ID,
 		}
 
 	}
-	return 0, ephemeral.Id{},
+	return rounds.Round{}, ephemeral.Id{}, format.Message{},
 		errors.New("failed to send the message, out of round retries")
 }
diff --git a/cmix/sendCmixUtils.go b/cmix/sendCmixUtils.go
index 146e63117d5cde18c1ee8d9dc794156eded72ad1..e2d8ea178920d52c3fd3dcfbd78b4386896d5b75 100644
--- a/cmix/sendCmixUtils.go
+++ b/cmix/sendCmixUtils.go
@@ -41,7 +41,7 @@ type SendCmixCommsInterface interface {
 }
 
 // How much in the future a round needs to be to send to it
-const sendTimeBuffer = 1000 * time.Millisecond
+const sendTimeBuffer = 150 * time.Millisecond
 const unrecoverableError = "failed with an unrecoverable error"
 
 // handlePutMessageError handles errors received from a PutMessage or a
diff --git a/cmix/sendManyCmix.go b/cmix/sendManyCmix.go
index 4fa79c930c231e48a86e7c90b5f6324d4bdc2f25..3dcd12dda6043620ee9c48f8fd33d394a4352ab6 100644
--- a/cmix/sendManyCmix.go
+++ b/cmix/sendManyCmix.go
@@ -9,6 +9,8 @@ package cmix
 
 import (
 	"fmt"
+	"gitlab.com/elixxir/client/cmix/attempts"
+	"gitlab.com/elixxir/client/cmix/rounds"
 	"strings"
 	"time"
 
@@ -67,9 +69,9 @@ type TargetedCmixMessage struct {
 // (along with the reason). Blocks until successful send or err.
 // WARNING: Do not roll your own crypto
 func (c *client) SendMany(messages []TargetedCmixMessage,
-	p CMIXParams) (id.Round, []ephemeral.Id, error) {
+	p CMIXParams) (rounds.Round, []ephemeral.Id, error) {
 	if !c.Monitor.IsHealthy() {
-		return 0, []ephemeral.Id{}, errors.New(
+		return rounds.Round{}, []ephemeral.Id{}, errors.New(
 			"Cannot send cMix message when the network is not healthy")
 	}
 
@@ -89,7 +91,7 @@ func (c *client) SendMany(messages []TargetedCmixMessage,
 
 	return sendManyCmixHelper(c.Sender, acms, p,
 		c.instance, c.session.GetCmixGroup(), c.Registrar, c.rng, c.events,
-		c.session.GetTransmissionID(), c.comms)
+		c.session.GetTransmissionID(), c.comms, c.attemptTracker)
 }
 
 type assembledCmixMessage struct {
@@ -108,12 +110,12 @@ type assembledCmixMessage struct {
 // If the message is successfully sent, the ID of the round sent it is returned,
 // which can be registered with the network instance to get a callback on its
 // status.
-func sendManyCmixHelper(sender gateway.Sender,
-	msgs []assembledCmixMessage, param CMIXParams, instance *network.Instance,
-	grp *cyclic.Group, registrar nodes.Registrar,
-	rng *fastRNG.StreamGenerator, events event.Reporter,
-	senderId *id.ID, comms SendCmixCommsInterface) (
-	id.Round, []ephemeral.Id, error) {
+func sendManyCmixHelper(sender gateway.Sender, msgs []assembledCmixMessage,
+	param CMIXParams, instance *network.Instance, grp *cyclic.Group,
+	registrar nodes.Registrar, rng *fastRNG.StreamGenerator,
+	events event.Reporter, senderId *id.ID, comms SendCmixCommsInterface,
+	attemptTracker attempts.SendAttemptTracker) (
+	rounds.Round, []ephemeral.Id, error) {
 
 	timeStart := netTime.Now()
 	var attempted excludedRounds.ExcludedRounds
@@ -140,14 +142,36 @@ func sendManyCmixHelper(sender gateway.Sender,
 		cmix.SetGroupBits(msgs[i].Message, grp, stream)
 	}
 
-	for numRoundTries := uint(0); numRoundTries < param.RoundTries; numRoundTries++ {
+	numAttempts := 0
+	if !param.Probe {
+		optimalAttempts, ready := attemptTracker.GetOptimalNumAttempts()
+		if ready {
+			numAttempts = optimalAttempts
+			jww.INFO.Printf("[SendMany-%s] Looking for round to send cMix "+
+				"messages to %s, sending non probe with %d optimalAttempts",
+				param.DebugTag, recipientString, numAttempts)
+		} else {
+			numAttempts = 4
+			jww.INFO.Printf("[SendMany-%s] Looking for round to send cMix "+
+				"messages to %s, sending non probe with %d non optimalAttempts, "+
+				"insufficient data", param.DebugTag, recipientString, numAttempts)
+		}
+	} else {
+		jww.INFO.Printf("[SendMany-%s] Looking for round to send cMix messages "+
+			"to %s, sending probe with %d Attempts, insufficient data",
+			param.DebugTag, recipientString, numAttempts)
+		defer attemptTracker.SubmitProbeAttempt(numAttempts)
+	}
+
+	for numRoundTries := uint(0); numRoundTries < param.RoundTries; numRoundTries,
+		numAttempts = numRoundTries+1, numAttempts+1 {
 		elapsed := netTime.Since(timeStart)
 
 		if elapsed > param.Timeout {
 			jww.INFO.Printf("[SendMany-%s] No rounds to send to %s "+
 				"(msgDigest: %s) were found before timeout %s", param.DebugTag,
 				recipientString, msgDigests, param.Timeout)
-			return 0, []ephemeral.Id{},
+			return rounds.Round{}, []ephemeral.Id{},
 				errors.New("sending cMix message timed out")
 		}
 
@@ -160,8 +184,8 @@ func sendManyCmixHelper(sender gateway.Sender,
 		remainingTime := param.Timeout - elapsed
 
 		// Find the best round to send to, excluding attempted rounds
-		bestRound, _ := instance.GetWaitingRounds().GetUpcomingRealtime(
-			remainingTime, attempted, sendTimeBuffer)
+		bestRound, _, _ := instance.GetWaitingRounds().GetUpcomingRealtime(
+			remainingTime, attempted, numAttempts, sendTimeBuffer)
 		if bestRound == nil {
 			continue
 		}
@@ -210,7 +234,7 @@ func sendManyCmixHelper(sender gateway.Sender,
 				stream.Close()
 				jww.INFO.Printf("[SendMany-%s] Error building slot "+
 					"received: %v", param.DebugTag, err)
-				return 0, []ephemeral.Id{}, errors.Errorf("failed to build "+
+				return rounds.Round{}, []ephemeral.Id{}, errors.Errorf("failed to build "+
 					"slot message for %s: %+v", msg.Recipient, err)
 			}
 		}
@@ -259,7 +283,7 @@ func sendManyCmixHelper(sender gateway.Sender,
 
 		// Exit if the thread has been stopped
 		if stoppable.CheckErr(err) {
-			return 0, []ephemeral.Id{}, err
+			return rounds.Round{}, []ephemeral.Id{}, err
 		}
 
 		// If the comm errors or the message fails to send, continue retrying
@@ -276,7 +300,7 @@ func sendManyCmixHelper(sender gateway.Sender,
 				jww.INFO.Printf("[SendMany-%s] Error received: %v",
 					param.DebugTag, err)
 			}
-			return 0, []ephemeral.Id{}, err
+			return rounds.Round{}, []ephemeral.Id{}, err
 		}
 
 		// Return if it sends properly
@@ -288,7 +312,7 @@ func sendManyCmixHelper(sender gateway.Sender,
 				bestRound.ID, msgDigests)
 			jww.INFO.Print(m)
 			events.Report(1, "MessageSendMany", "Metric", m)
-			return id.Round(bestRound.ID), ephemeralIDs, nil
+			return rounds.MakeRound(bestRound), ephemeralIDs, nil
 		} else {
 			jww.FATAL.Panicf("[SendMany-%s] Gateway %s returned no "+
 				"error, but failed to accept message when sending to EphIDs "+
@@ -297,6 +321,6 @@ func sendManyCmixHelper(sender gateway.Sender,
 		}
 	}
 
-	return 0, []ephemeral.Id{},
+	return rounds.Round{}, []ephemeral.Id{},
 		errors.New("failed to send the message, unknown error")
 }
diff --git a/cmix/utils_test.go b/cmix/utils_test.go
index 54f05de98e8251b98f23934f00df9e21337c62a9..0a0cb6d2264412e767de7840fc73f07230e0b2c6 100644
--- a/cmix/utils_test.go
+++ b/cmix/utils_test.go
@@ -8,6 +8,7 @@
 package cmix
 
 import (
+	"gitlab.com/elixxir/client/cmix/rounds"
 	"time"
 
 	"github.com/pkg/errors"
@@ -192,14 +193,14 @@ func (mrr *mockRoundEventRegistrar) AddRoundEventChan(rid id.Round, eventChan ch
 
 // mockCriticalSender
 func mockCriticalSender(msg format.Message, recipient *id.ID,
-	params CMIXParams) (id.Round, ephemeral.Id, error) {
-	return id.Round(1), ephemeral.Id{}, nil
+	params CMIXParams) (rounds.Round, ephemeral.Id, error) {
+	return rounds.Round{ID: 1}, ephemeral.Id{}, nil
 }
 
 // mockFailCriticalSender
 func mockFailCriticalSender(msg format.Message, recipient *id.ID,
-	params CMIXParams) (id.Round, ephemeral.Id, error) {
-	return id.Round(1), ephemeral.Id{}, errors.New("Test error")
+	params CMIXParams) (rounds.Round, ephemeral.Id, error) {
+	return rounds.Round{ID: 1}, ephemeral.Id{}, errors.New("Test error")
 }
 
 // func newTestClient(t *testing.T) (*client, error) {
diff --git a/connect/authenticated.go b/connect/authenticated.go
index 45f8dac4a97c204d9c25e87d07bbe86558933328..c5493deb0da13d227017e6695789f9317da2c830 100644
--- a/connect/authenticated.go
+++ b/connect/authenticated.go
@@ -138,12 +138,8 @@ func connectWithAuthentication(conn Connection, timeStart time.Time,
 
 	// Track the result of the round(s) we sent the
 	// identity authentication message on
-	err = net.GetRoundResults(remainingTime,
+	net.GetRoundResults(remainingTime,
 		roundCb, sendReport.RoundList...)
-	if err != nil {
-		return nil, errors.Errorf("could not track rounds for successful " +
-			"identity confirmation message delivery")
-	}
 	// Block waiting for confirmation of the round(s) success (or timeout
 	jww.DEBUG.Printf("AuthenticatedConnection waiting for authenticated "+
 		"connection with %s to be established...", recipient.ID.String())
diff --git a/connect/utils_test.go b/connect/utils_test.go
index fbf1d560f5182dc13bba249d09c44bb75b5a7155..86f190808b95d44e80cff854d87be643339f2f0c 100644
--- a/connect/utils_test.go
+++ b/connect/utils_test.go
@@ -160,11 +160,17 @@ func (m *mockCmix) Follow(cmix.ClientErrorReport) (stoppable.Stoppable, error) {
 func (m *mockCmix) GetMaxMessageLength() int { return 4096 }
 
 func (m *mockCmix) Send(*id.ID, format.Fingerprint, message.Service, []byte,
-	[]byte, cmix.CMIXParams) (id.Round, ephemeral.Id, error) {
-	return 0, ephemeral.Id{}, nil
+	[]byte, cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
+	return rounds.Round{}, ephemeral.Id{}, nil
 }
-func (m *mockCmix) SendMany([]cmix.TargetedCmixMessage, cmix.CMIXParams) (id.Round, []ephemeral.Id, error) {
-	return 0, []ephemeral.Id{}, nil
+
+func (m *mockCmix) SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler,
+	cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
+	return rounds.Round{}, ephemeral.Id{}, nil
+}
+
+func (m *mockCmix) SendMany([]cmix.TargetedCmixMessage, cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) {
+	return rounds.Round{}, []ephemeral.Id{}, nil
 }
 func (m *mockCmix) AddIdentity(*id.ID, time.Time, bool) {}
 func (m *mockCmix) RemoveIdentity(*id.ID)               {}
@@ -189,9 +195,8 @@ func (m *mockCmix) HasNode(*id.ID) bool
 func (m *mockCmix) NumRegisteredNodes() int                                            { return 24 }
 func (m *mockCmix) TriggerNodeRegistration(*id.ID)                                     {}
 
-func (m *mockCmix) GetRoundResults(_ time.Duration, roundCallback cmix.RoundEventCallback, _ ...id.Round) error {
+func (m *mockCmix) GetRoundResults(_ time.Duration, roundCallback cmix.RoundEventCallback, _ ...id.Round) {
 	roundCallback(true, false, nil)
-	return nil
 }
 
 func (m *mockCmix) LookupHistoricalRound(id.Round, rounds.RoundResultCallback) error { return nil }
diff --git a/dummy/mockCmix_test.go b/dummy/mockCmix_test.go
index b60b5fe4ea43e0536f212fb0db291db8d95fc3b5..2aca679d0702225c69d01e7449c028ddd57cd7ee 100644
--- a/dummy/mockCmix_test.go
+++ b/dummy/mockCmix_test.go
@@ -38,12 +38,26 @@ func newMockCmix(payloadSize int) cmix.Client {
 	}
 }
 
-func (m *mockCmix) Send(recipient *id.ID, fingerprint format.Fingerprint, service message.Service, payload, mac []byte, cmixParams cmix.CMIXParams) (id.Round, ephemeral.Id, error) {
+func (m *mockCmix) Send(recipient *id.ID, fingerprint format.Fingerprint, service message.Service, payload, mac []byte, cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
 	m.Lock()
 	defer m.Unlock()
 	m.messages[*recipient] = generateMessage(m.payloadSize, fingerprint, service, payload, mac)
 
-	return 0, ephemeral.Id{}, nil
+	return rounds.Round{}, ephemeral.Id{}, nil
+}
+
+func (m *mockCmix) SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler,
+	cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
+	m.Lock()
+	defer m.Unlock()
+
+	fingerprint, service, payload, mac, err := assembler(42)
+	if err != nil {
+		return rounds.Round{}, ephemeral.Id{}, err
+	}
+	m.messages[*recipient] = generateMessage(m.payloadSize, fingerprint, service, payload, mac)
+
+	return rounds.Round{}, ephemeral.Id{}, nil
 }
 
 func (m *mockCmix) GetMsgListLen() int {
@@ -64,11 +78,10 @@ func (m mockCmix) Follow(report cmix.ClientErrorReport) (stoppable.Stoppable, er
 }
 
 func (m mockCmix) GetMaxMessageLength() int {
-	//TODO implement me
-	panic("implement me")
+	return 100
 }
 
-func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage, p cmix.CMIXParams) (id.Round, []ephemeral.Id, error) {
+func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage, p cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) {
 	//TODO implement me
 	panic("implement me")
 }
@@ -163,7 +176,7 @@ func (m mockCmix) TriggerNodeRegistration(nid *id.ID) {
 	panic("implement me")
 }
 
-func (m mockCmix) GetRoundResults(timeout time.Duration, roundCallback cmix.RoundEventCallback, roundList ...id.Round) error {
+func (m mockCmix) GetRoundResults(timeout time.Duration, roundCallback cmix.RoundEventCallback, roundList ...id.Round) {
 	//TODO implement me
 	panic("implement me")
 }
diff --git a/dummy/random.go b/dummy/random.go
index 2a41d8243decc43ae56ae93068c5106a8f9371e7..2f1abc73ec290ead86c142f527e85e8db176a136 100644
--- a/dummy/random.go
+++ b/dummy/random.go
@@ -49,7 +49,7 @@ func (m *Manager) newRandomCmixMessage(rng csprng.Source) (
 	}
 
 	// Generate random message payload
-	payloadSize := m.store.GetCmixGroup().GetP().ByteLen()
+	payloadSize := m.net.GetMaxMessageLength()
 	payload, err = newRandomPayload(payloadSize, rng)
 	if err != nil {
 		return nil, format.Fingerprint{}, message.Service{}, nil, nil,
@@ -79,13 +79,8 @@ func (m *Manager) newRandomCmixMessage(rng csprng.Source) (
 // newRandomPayload generates a random payload of a random length
 // within the maxPayloadSize.
 func newRandomPayload(maxPayloadSize int, rng csprng.Source) ([]byte, error) {
-	// Generate random payload size
-	randomPayloadSize, err := randomInt(maxPayloadSize, rng)
-	if err != nil {
-		return nil, errors.Errorf(payloadSizeRngErr, err)
-	}
 
-	randomMsg, err := csprng.Generate(randomPayloadSize, rng)
+	randomMsg, err := csprng.Generate(maxPayloadSize, rng)
 	if err != nil {
 		return nil, err
 	}
diff --git a/dummy/random_test.go b/dummy/random_test.go
index fab84c22fc0e552931026ec0f27cc0c07e511b32..a4336983df79a8a33e3f95e56763d85728962d6c 100644
--- a/dummy/random_test.go
+++ b/dummy/random_test.go
@@ -75,16 +75,16 @@ func Test_durationRng_Consistency(t *testing.T) {
 // 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==",
+		"U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVLf15tNdkKbYXoMn58NO6VbDMDWFEyIhTWEGsvgcJsHWA==",
+		"CD9h03W8ArQd9PkZKeGP2p5vguVOdI6B555LvW/jTNy6hD7o1j6MT/4c6+pUbY+sE90arATOLqKHfFV5z6LHjg==",
+		"GwuvrogbgqdREIpC7TyQPKpDRlp4YgYWl4rtDOPGxPOue8PgSVtXEv79vgwQKIfhANrNLYhfaSy2B9oAoRwccA==",
+		"ceeWotwtwlpbdLLhKXBeJz8FySMmgo4rBW44F2WOEGFJiUf980RBDtTBFgI/qONXa2/tJ/+JdLrAyv2a0FaSsQ==",
+		"NhnnOJZN/ceejVNDc2Yc/WbXT+weG4lJGrcjbkt1IWKQzyvrQsPKJzKFYPGqwGfOpui/RtSrK0aAQCxfsoIOiA==",
+		"XTJg8d6XgoPUoJo2+WwglBdG4+1NpkaprotPp7T8OiC6+hp17TJ6hriww5rxz9KztRIZ6nlTOr9EjSxHnTJgdQ==",
+		"M5BZFMjMHPCdo54Okp0CSry8sWk5e7c05+8KbgHxhU3rX+Qk/vesIQiR9ZdeKSqiuKoEfGHNszNz6+csJ6CYwA==",
+		"IZfa5rcyw1HfZo+HTiyfHOCcqGAX5+IXSDA/9BwbI+EcSO0XU51oX3byp5i8ZN4OXbKGSyrTwmzmOCNCdloT1g==",
+		"luUt92D2w0ZeKaDcpGrDoNVwEzvCFXH19UpkMQVRP9hCmxlK4bqfKoOGrnKzZh/oLCrGTb9GFRgk4jBTEmN8mQ==",
+		"wrh9bfDdXvKDZxkHLWcvYfqgvob0V5Iew3wORgzw1wPQfcX1ZhpFATNAmnEramar17plIkyiaXjZpc5i/rEagw==",
 	}
 
 	prng := NewPrng(42)
diff --git a/dummy/send.go b/dummy/send.go
index ac6b39796add64bbfed8be9ed8c3e3f8687904f8..77b0f4e7de569e36b985c60521c6083b40006ee3 100644
--- a/dummy/send.go
+++ b/dummy/send.go
@@ -125,6 +125,7 @@ func (m *Manager) sendMessage(index, totalMessages int, rng csprng.Source) error
 
 	// Send message
 	p := cmix.GetDefaultCMIXParams()
+	p.Probe = true
 	_, _, err = m.net.Send(recipient, fp, service, payload, mac, p)
 	if err != nil {
 		return errors.Errorf("Failed to send message: %+v", err)
diff --git a/e2e/fpGenerator_test.go b/e2e/fpGenerator_test.go
index f3bd2b1dc45c77a5fab006d6f0df9be3a20d9cb8..249dc5bba79fc07fb38439a64b4266a658215ee0 100644
--- a/e2e/fpGenerator_test.go
+++ b/e2e/fpGenerator_test.go
@@ -118,11 +118,15 @@ func newMockFpgCmix() *mockFpgCmix {
 
 func (m *mockFpgCmix) Follow(cmix.ClientErrorReport) (stoppable.Stoppable, error) { return nil, nil }
 func (m *mockFpgCmix) GetMaxMessageLength() int                                   { return 0 }
-func (m *mockFpgCmix) Send(*id.ID, format.Fingerprint, message.Service, []byte, []byte, cmix.CMIXParams) (id.Round, ephemeral.Id, error) {
-	return 0, ephemeral.Id{}, nil
+func (m *mockFpgCmix) Send(*id.ID, format.Fingerprint, message.Service, []byte, []byte, cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
+	return rounds.Round{}, ephemeral.Id{}, nil
 }
-func (m *mockFpgCmix) SendMany([]cmix.TargetedCmixMessage, cmix.CMIXParams) (id.Round, []ephemeral.Id, error) {
-	return 0, nil, nil
+func (m *mockFpgCmix) SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler,
+	cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
+	return rounds.Round{}, ephemeral.Id{}, nil
+}
+func (m *mockFpgCmix) SendMany([]cmix.TargetedCmixMessage, cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) {
+	return rounds.Round{}, nil, nil
 }
 func (m *mockFpgCmix) AddIdentity(*id.ID, time.Time, bool) {}
 func (m *mockFpgCmix) RemoveIdentity(*id.ID)               {}
@@ -166,8 +170,7 @@ func (m *mockFpgCmix) RemoveHealthCallback(uint64)
 func (m *mockFpgCmix) HasNode(*id.ID) bool                                      { return false }
 func (m *mockFpgCmix) NumRegisteredNodes() int                                  { return 0 }
 func (m *mockFpgCmix) TriggerNodeRegistration(*id.ID)                           {}
-func (m *mockFpgCmix) GetRoundResults(time.Duration, cmix.RoundEventCallback, ...id.Round) error {
-	return nil
+func (m *mockFpgCmix) GetRoundResults(time.Duration, cmix.RoundEventCallback, ...id.Round) {
 }
 func (m *mockFpgCmix) LookupHistoricalRound(id.Round, rounds.RoundResultCallback) error { return nil }
 func (m *mockFpgCmix) SendToAny(func(host *connect.Host) (interface{}, error), *stoppable.Single) (interface{}, error) {
diff --git a/e2e/parse/partition/store.go b/e2e/parse/partition/store.go
index 5ca9007a85a78f28b4566b2ec0be710f4305b0bc..713fc291a9bb530994c3598b26e595eea52a6b03 100644
--- a/e2e/parse/partition/store.go
+++ b/e2e/parse/partition/store.go
@@ -171,7 +171,7 @@ func (s *Store) loadActivePartitions() {
 	defer s.mux.Unlock()
 	obj, err := s.kv.Get(activePartitions, activePartitionVersion)
 	if err != nil {
-		jww.DEBUG.Printf("Could not load active partitions: %+v", err)
+		jww.DEBUG.Printf("Could not load active partitions: %s", err.Error())
 		return
 	}
 
diff --git a/e2e/rekey/utils_test.go b/e2e/rekey/utils_test.go
index 34b7de358b4fafc072a95da66fd274aad0d53365..0b9fd388668ff77f9cbfed8bc7dc6dc6572d3eeb 100644
--- a/e2e/rekey/utils_test.go
+++ b/e2e/rekey/utils_test.go
@@ -236,13 +236,18 @@ func (m *mockNetManager) GetMaxMessageLength() int {
 
 func (m *mockNetManager) Send(recipient *id.ID, fingerprint format.Fingerprint,
 	service message.Service, payload, mac []byte, cmixParams cmix.CMIXParams) (
-	id.Round, ephemeral.Id, error) {
-	return id.Round(0), ephemeral.Id{}, nil
+	rounds.Round, ephemeral.Id, error) {
+	return rounds.Round{}, ephemeral.Id{}, nil
+}
+
+func (m *mockNetManager) SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler,
+	cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
+	return rounds.Round{}, ephemeral.Id{}, nil
 }
 
 func (m *mockNetManager) SendMany(messages []cmix.TargetedCmixMessage, p cmix.CMIXParams) (
-	id.Round, []ephemeral.Id, error) {
-	return id.Round(0), nil, nil
+	rounds.Round, []ephemeral.Id, error) {
+	return rounds.Round{}, nil, nil
 }
 
 func (m *mockNetManager) AddIdentity(id *id.ID, validUntil time.Time, persistent bool) {}
@@ -297,8 +302,7 @@ func (m *mockNetManager) NumRegisteredNodes() int {
 func (m *mockNetManager) TriggerNodeRegistration(nid *id.ID) {}
 
 func (m *mockNetManager) GetRoundResults(timeout time.Duration, roundCallback cmix.RoundEventCallback,
-	roundList ...id.Round) error {
-	return nil
+	roundList ...id.Round) {
 }
 
 func (m *mockNetManager) LookupHistoricalRound(
diff --git a/e2e/sendE2E.go b/e2e/sendE2E.go
index 6b3d275e7bd196379a752b5a83cdd5994698473b..c05700842e7264a04358be6803e8ac4ed038afb9 100644
--- a/e2e/sendE2E.go
+++ b/e2e/sendE2E.go
@@ -170,8 +170,7 @@ func (m *manager) prepareSendE2E(mt catalog.MessageType, recipient *id.ID,
 		thisSendFunc := func() {
 			wg.Add(1)
 			go func(i int) {
-				var err error
-				roundIds[i], _, err = m.net.Send(recipient,
+				r, _, err := m.net.Send(recipient,
 					key.Fingerprint(), s, contentsEnc, mac,
 					params.CMIXParams)
 				if err != nil {
@@ -179,6 +178,7 @@ func (m *manager) prepareSendE2E(mt catalog.MessageType, recipient *id.ID,
 						"Send: %+v", err)
 					errCh <- err
 				}
+				roundIds[i] = r.ID
 				wg.Done()
 			}(localI)
 		}
diff --git a/e2e/sendUnsafe.go b/e2e/sendUnsafe.go
index 1eb2cee40684bcedeff7029ce08c8510ad38aeb0..b59bd3243bfb6a9208c302761580cf6f90417983 100644
--- a/e2e/sendUnsafe.go
+++ b/e2e/sendUnsafe.go
@@ -72,13 +72,13 @@ func (m *manager) sendUnsafe(mt catalog.MessageType, recipient *id.ID,
 			jww.TRACE.Printf("sendUnsafe contents: %v, fp: %v, mac: %v",
 				payload, fp, unencryptedMAC)
 
-			var err error
-			roundIds[i], _, err = m.net.Send(recipient, fp,
+			r, _, err := m.net.Send(recipient, fp,
 				srvc, payload, unencryptedMAC,
 				params.CMIXParams)
 			if err != nil {
 				errCh <- err
 			}
+			roundIds[i] = r.ID
 			wg.Done()
 		}(i, p)
 	}
diff --git a/e2e/utils_test.go b/e2e/utils_test.go
index d3dcda2491dbb91232101557d9f804472753211a..6d8835b8c51edaf47ca2821521e9e55de95f5db3 100644
--- a/e2e/utils_test.go
+++ b/e2e/utils_test.go
@@ -181,7 +181,7 @@ func (m *mockCmix) GetMaxMessageLength() int {
 }
 
 func (m *mockCmix) Send(_ *id.ID, fp format.Fingerprint, srv message.Service,
-	payload, mac []byte, _ cmix.CMIXParams) (id.Round, ephemeral.Id, error) {
+	payload, mac []byte, _ cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
 	m.handler.Lock()
 	defer m.handler.Unlock()
 
@@ -193,21 +193,26 @@ func (m *mockCmix) Send(_ *id.ID, fp format.Fingerprint, srv message.Service,
 	if m.handler.processorMap[fp] != nil {
 		m.handler.processorMap[fp].Process(
 			msg, receptionID.EphemeralIdentity{}, rounds.Round{})
-		return 0, ephemeral.Id{}, nil
+		return rounds.Round{}, ephemeral.Id{}, nil
 	} else if m.handler.serviceMap[srv.Tag] != nil {
 		m.handler.serviceMap[srv.Tag].Process(
 			msg, receptionID.EphemeralIdentity{}, rounds.Round{})
-		return 0, ephemeral.Id{}, nil
+		return rounds.Round{}, ephemeral.Id{}, nil
 	}
 
 	m.t.Errorf("No processor found for fingerprint %s", fp)
-	return 0, ephemeral.Id{},
+	return rounds.Round{}, ephemeral.Id{},
 		errors.Errorf("No processor found for fingerprint %s", fp)
 
 }
 
-func (m *mockCmix) SendMany([]cmix.TargetedCmixMessage, cmix.CMIXParams) (id.Round, []ephemeral.Id, error) {
-	return 0, nil, nil
+func (m *mockCmix) SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler,
+	cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
+	panic("implement me")
+}
+
+func (m *mockCmix) SendMany([]cmix.TargetedCmixMessage, cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) {
+	return rounds.Round{}, nil, nil
 }
 func (m *mockCmix) AddIdentity(*id.ID, time.Time, bool)            {}
 func (m *mockCmix) RemoveIdentity(*id.ID)                          {}
@@ -245,8 +250,7 @@ func (m *mockCmix) RemoveHealthCallback(uint64)                              {}
 func (m *mockCmix) HasNode(*id.ID) bool                                      { return true }
 func (m *mockCmix) NumRegisteredNodes() int                                  { return 0 }
 func (m *mockCmix) TriggerNodeRegistration(*id.ID)                           {}
-func (m *mockCmix) GetRoundResults(time.Duration, cmix.RoundEventCallback, ...id.Round) error {
-	return nil
+func (m *mockCmix) GetRoundResults(time.Duration, cmix.RoundEventCallback, ...id.Round) {
 }
 func (m *mockCmix) LookupHistoricalRound(id.Round, rounds.RoundResultCallback) error { return nil }
 func (m *mockCmix) SendToAny(func(host *connect.Host) (interface{}, error), *stoppable.Single) (interface{}, error) {
diff --git a/fileTransfer/connect/utils_test.go b/fileTransfer/connect/utils_test.go
index b79fd3f6bae97ca390b9c140560b1c924db3bdca..6c9de504f42e812ec9bfc7c2c9925667241daa71 100644
--- a/fileTransfer/connect/utils_test.go
+++ b/fileTransfer/connect/utils_test.go
@@ -115,12 +115,12 @@ func (m *mockCmix) GetMaxMessageLength() int {
 }
 
 func (m *mockCmix) Send(*id.ID, format.Fingerprint, message.Service, []byte,
-	[]byte, cmix.CMIXParams) (id.Round, ephemeral.Id, error) {
+	[]byte, cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
 	panic("implement me")
 }
 
 func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage,
-	_ cmix.CMIXParams) (id.Round, []ephemeral.Id, error) {
+	_ cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) {
 	m.handler.Lock()
 	for _, targetedMsg := range messages {
 		msg := format.NewMessage(m.numPrimeBytes)
@@ -132,7 +132,12 @@ func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage,
 			rounds.Round{ID: 42})
 	}
 	m.handler.Unlock()
-	return 42, []ephemeral.Id{}, nil
+	return rounds.Round{ID: 42}, []ephemeral.Id{}, nil
+}
+
+func (m *mockCmix) SendWithAssembler(*id.ID, cmix.MessageAssembler,
+	cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
+	panic("implement me")
 }
 
 func (m *mockCmix) AddIdentity(*id.ID, time.Time, bool)            { panic("implement me") }
@@ -184,18 +189,19 @@ func (m *mockCmix) NumRegisteredNodes() int        { panic("implement me") }
 func (m *mockCmix) TriggerNodeRegistration(*id.ID) { panic("implement me") }
 
 func (m *mockCmix) GetRoundResults(_ time.Duration,
-	roundCallback cmix.RoundEventCallback, _ ...id.Round) error {
+	roundCallback cmix.RoundEventCallback, _ ...id.Round) {
 	go roundCallback(true, false, map[id.Round]cmix.RoundResult{42: {}})
-	return nil
 }
 
 func (m *mockCmix) LookupHistoricalRound(id.Round, rounds.RoundResultCallback) error {
 	panic("implement me")
 }
-func (m *mockCmix) SendToAny(func(host *connect.Host) (interface{}, error), *stoppable.Single) (interface{}, error) {
+func (m *mockCmix) SendToAny(func(host *connect.Host) (interface{}, error),
+	*stoppable.Single) (interface{}, error) {
 	panic("implement me")
 }
-func (m *mockCmix) SendToPreferred([]*id.ID, gateway.SendToPreferredFunc, *stoppable.Single, time.Duration) (interface{}, error) {
+func (m *mockCmix) SendToPreferred([]*id.ID, gateway.SendToPreferredFunc,
+	*stoppable.Single, time.Duration) (interface{}, error) {
 	panic("implement me")
 }
 func (m *mockCmix) SetGatewayFilter(gateway.Filter)   { panic("implement me") }
diff --git a/fileTransfer/e2e/utils_test.go b/fileTransfer/e2e/utils_test.go
index f212e42d0dcb33ffe44f46a402701d146ce163ed..7de4bba2c25ddb23103da9f04c4a6eac8898984c 100644
--- a/fileTransfer/e2e/utils_test.go
+++ b/fileTransfer/e2e/utils_test.go
@@ -117,12 +117,12 @@ func (m *mockCmix) GetMaxMessageLength() int {
 }
 
 func (m *mockCmix) Send(*id.ID, format.Fingerprint, message.Service, []byte,
-	[]byte, cmix.CMIXParams) (id.Round, ephemeral.Id, error) {
+	[]byte, cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
 	panic("implement me")
 }
 
 func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage,
-	_ cmix.CMIXParams) (id.Round, []ephemeral.Id, error) {
+	_ cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) {
 	m.handler.Lock()
 	for _, targetedMsg := range messages {
 		msg := format.NewMessage(m.numPrimeBytes)
@@ -134,7 +134,12 @@ func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage,
 			rounds.Round{ID: 42})
 	}
 	m.handler.Unlock()
-	return 42, []ephemeral.Id{}, nil
+	return rounds.Round{ID: 42}, []ephemeral.Id{}, nil
+}
+
+func (m *mockCmix) SendWithAssembler(*id.ID, cmix.MessageAssembler,
+	cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
+	panic("implement me")
 }
 
 func (m *mockCmix) AddIdentity(*id.ID, time.Time, bool)            { panic("implement me") }
@@ -186,18 +191,19 @@ func (m *mockCmix) NumRegisteredNodes() int        { panic("implement me") }
 func (m *mockCmix) TriggerNodeRegistration(*id.ID) { panic("implement me") }
 
 func (m *mockCmix) GetRoundResults(_ time.Duration,
-	roundCallback cmix.RoundEventCallback, _ ...id.Round) error {
+	roundCallback cmix.RoundEventCallback, _ ...id.Round) {
 	go roundCallback(true, false, map[id.Round]cmix.RoundResult{42: {}})
-	return nil
 }
 
 func (m *mockCmix) LookupHistoricalRound(id.Round, rounds.RoundResultCallback) error {
 	panic("implement me")
 }
-func (m *mockCmix) SendToAny(func(host *connect.Host) (interface{}, error), *stoppable.Single) (interface{}, error) {
+func (m *mockCmix) SendToAny(func(host *connect.Host) (interface{}, error),
+	*stoppable.Single) (interface{}, error) {
 	panic("implement me")
 }
-func (m *mockCmix) SendToPreferred([]*id.ID, gateway.SendToPreferredFunc, *stoppable.Single, time.Duration) (interface{}, error) {
+func (m *mockCmix) SendToPreferred([]*id.ID, gateway.SendToPreferredFunc,
+	*stoppable.Single, time.Duration) (interface{}, error) {
 	panic("implement me")
 }
 func (m *mockCmix) SetGatewayFilter(gateway.Filter)   { panic("implement me") }
diff --git a/fileTransfer/groupChat/utils_test.go b/fileTransfer/groupChat/utils_test.go
index e9f865511a40a99bb436cfbd448257744289b27f..61dd18006d7d80b35712cc938267a2f5bd2ae4f7 100644
--- a/fileTransfer/groupChat/utils_test.go
+++ b/fileTransfer/groupChat/utils_test.go
@@ -112,12 +112,12 @@ func (m *mockCmix) GetMaxMessageLength() int {
 }
 
 func (m *mockCmix) Send(*id.ID, format.Fingerprint, message.Service, []byte,
-	[]byte, cmix.CMIXParams) (id.Round, ephemeral.Id, error) {
+	[]byte, cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
 	panic("implement me")
 }
 
 func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage,
-	_ cmix.CMIXParams) (id.Round, []ephemeral.Id, error) {
+	_ cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) {
 	m.handler.Lock()
 	for _, targetedMsg := range messages {
 		msg := format.NewMessage(m.numPrimeBytes)
@@ -129,7 +129,12 @@ func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage,
 			rounds.Round{ID: 42})
 	}
 	m.handler.Unlock()
-	return 42, []ephemeral.Id{}, nil
+	return rounds.Round{ID: 42}, []ephemeral.Id{}, nil
+}
+
+func (m *mockCmix) SendWithAssembler(*id.ID, cmix.MessageAssembler,
+	cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
+	panic("implement me")
 }
 
 func (m *mockCmix) AddIdentity(*id.ID, time.Time, bool)            { panic("implement me") }
@@ -181,18 +186,19 @@ func (m *mockCmix) NumRegisteredNodes() int        { panic("implement me") }
 func (m *mockCmix) TriggerNodeRegistration(*id.ID) { panic("implement me") }
 
 func (m *mockCmix) GetRoundResults(_ time.Duration,
-	roundCallback cmix.RoundEventCallback, _ ...id.Round) error {
+	roundCallback cmix.RoundEventCallback, _ ...id.Round) {
 	go roundCallback(true, false, map[id.Round]cmix.RoundResult{42: {}})
-	return nil
 }
 
 func (m *mockCmix) LookupHistoricalRound(id.Round, rounds.RoundResultCallback) error {
 	panic("implement me")
 }
-func (m *mockCmix) SendToAny(func(host *connect.Host) (interface{}, error), *stoppable.Single) (interface{}, error) {
+func (m *mockCmix) SendToAny(func(host *connect.Host) (interface{}, error),
+	*stoppable.Single) (interface{}, error) {
 	panic("implement me")
 }
-func (m *mockCmix) SendToPreferred([]*id.ID, gateway.SendToPreferredFunc, *stoppable.Single, time.Duration) (interface{}, error) {
+func (m *mockCmix) SendToPreferred([]*id.ID, gateway.SendToPreferredFunc,
+	*stoppable.Single, time.Duration) (interface{}, error) {
 	panic("implement me")
 }
 func (m *mockCmix) SetGatewayFilter(gateway.Filter)   { panic("implement me") }
@@ -230,14 +236,14 @@ func newMockGC(handler *mockGcHandler) *mockGC {
 }
 
 func (m *mockGC) Send(groupID *id.ID, tag string, message []byte) (
-	id.Round, time.Time, group.MessageID, error) {
+	rounds.Round, time.Time, group.MessageID, error) {
 	m.handler.Lock()
 	defer m.handler.Unlock()
 	m.handler.services[tag].Process(groupChat.MessageReceive{
 		GroupID: groupID,
 		Payload: message,
 	}, format.Message{}, receptionID.EphemeralIdentity{}, rounds.Round{})
-	return 0, time.Time{}, group.MessageID{}, nil
+	return rounds.Round{}, time.Time{}, group.MessageID{}, nil
 }
 
 func (m *mockGC) AddService(tag string, p groupChat.Processor) error {
diff --git a/fileTransfer/groupChat/wrapper.go b/fileTransfer/groupChat/wrapper.go
index 8566d645026bb5c3335cae6680cfdb4666576005..73bbf2cb0e26677c19046be47e31c444a7a33718 100644
--- a/fileTransfer/groupChat/wrapper.go
+++ b/fileTransfer/groupChat/wrapper.go
@@ -9,6 +9,7 @@ package groupChat
 
 import (
 	"github.com/pkg/errors"
+	"gitlab.com/elixxir/client/cmix/rounds"
 	ft "gitlab.com/elixxir/client/fileTransfer"
 	"gitlab.com/elixxir/client/groupChat"
 	ftCrypto "gitlab.com/elixxir/crypto/fileTransfer"
@@ -44,7 +45,7 @@ type Wrapper struct {
 // by the Wrapper for easier testing.
 type gcManager interface {
 	Send(groupID *id.ID, tag string, message []byte) (
-		id.Round, time.Time, group.MessageID, error)
+		rounds.Round, time.Time, group.MessageID, error)
 	AddService(tag string, p groupChat.Processor) error
 }
 
diff --git a/fileTransfer/manager.go b/fileTransfer/manager.go
index 3963a3ca9e7e0c5a2a61c529c2c855d6f148b192..f4ef20744369953459f0f49c1fc24c5b57e18726 100644
--- a/fileTransfer/manager.go
+++ b/fileTransfer/manager.go
@@ -13,6 +13,7 @@ import (
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/cmix"
 	"gitlab.com/elixxir/client/cmix/message"
+	"gitlab.com/elixxir/client/cmix/rounds"
 	"gitlab.com/elixxir/client/e2e"
 	"gitlab.com/elixxir/client/fileTransfer/callbackTracker"
 	"gitlab.com/elixxir/client/fileTransfer/store"
@@ -145,7 +146,7 @@ type FtE2e interface {
 // transfer manager for easier testing.
 type Cmix interface {
 	GetMaxMessageLength() int
-	SendMany(messages []cmix.TargetedCmixMessage, p cmix.CMIXParams) (id.Round,
+	SendMany(messages []cmix.TargetedCmixMessage, p cmix.CMIXParams) (rounds.Round,
 		[]ephemeral.Id, error)
 	AddFingerprint(identity *id.ID, fingerprint format.Fingerprint,
 		mp message.Processor) error
@@ -155,7 +156,7 @@ type Cmix interface {
 	AddHealthCallback(f func(bool)) uint64
 	RemoveHealthCallback(uint64)
 	GetRoundResults(timeout time.Duration,
-		roundCallback cmix.RoundEventCallback, roundList ...id.Round) error
+		roundCallback cmix.RoundEventCallback, roundList ...id.Round)
 }
 
 // Storage interface matches a subset of the storage.Session methods used by the
diff --git a/fileTransfer/send.go b/fileTransfer/send.go
index fd736280c8f55e64b927002c5aeec42120784cd5..e4a107f7b3e588318056706dc2b8f6dba9bf3d02 100644
--- a/fileTransfer/send.go
+++ b/fileTransfer/send.go
@@ -163,13 +163,14 @@ func (m *manager) sendCmix(packet []store.Part) {
 		}
 	}
 
-	err = m.cmix.GetRoundResults(
-		roundResultsTimeout, m.roundResultsCallback(validParts), rid)
+	m.cmix.GetRoundResults(
+		roundResultsTimeout, m.roundResultsCallback(validParts), rid.ID)
 }
 
 // roundResultsCallback generates a network.RoundEventCallback that handles
 // all parts in the packet once the round succeeds or fails.
-func (m *manager) roundResultsCallback(packet []store.Part) cmix.RoundEventCallback {
+func (m *manager) roundResultsCallback(
+	packet []store.Part) cmix.RoundEventCallback {
 	// Group file parts by transfer
 	grouped := map[ftCrypto.TransferID][]store.Part{}
 	for _, p := range packet {
diff --git a/fileTransfer/store/received.go b/fileTransfer/store/received.go
index 189025a711b89537022a99ec72d19eaf98e63c91..6ec51f2e0188bc4d1ab05b7ec2c2ee044588951c 100644
--- a/fileTransfer/store/received.go
+++ b/fileTransfer/store/received.go
@@ -100,7 +100,7 @@ func (r *Received) AddTransfer(key *ftCrypto.TransferKey,
 
 	_, exists := r.transfers[*tid]
 	if exists {
-		return nil, errors.Errorf(errAddExistingReceivedTransfer, tid)
+		return nil, errors.Errorf(errAddExistingReceivedTransfer, *tid)
 	}
 
 	rt, err := newReceivedTransfer(
diff --git a/fileTransfer/store/received_test.go b/fileTransfer/store/received_test.go
index 4839c6b70c5b0d1403837303b2f876fa7b13f0bf..3bcba24b6e32847945ce6d10961517aa14dded3a 100644
--- a/fileTransfer/store/received_test.go
+++ b/fileTransfer/store/received_test.go
@@ -204,8 +204,8 @@ func TestReceived_save(t *testing.T) {
 	kv := versioned.NewKV(ekv.MakeMemstore())
 	r, _, _ := NewOrLoadReceived(kv)
 	r.transfers = map[ftCrypto.TransferID]*ReceivedTransfer{
-		ftCrypto.TransferID{0}: nil, ftCrypto.TransferID{1}: nil,
-		ftCrypto.TransferID{2}: nil, ftCrypto.TransferID{3}: nil,
+		{0}: nil, {1}: nil,
+		{2}: nil, {3}: nil,
 	}
 
 	err := r.save()
diff --git a/fileTransfer/store/sent.go b/fileTransfer/store/sent.go
index a7f89a598dd79ec14b421b0101d0dae5ab7e05cf..d5e22ad8e976bb7d21c7e5e0c100496c19b363b3 100644
--- a/fileTransfer/store/sent.go
+++ b/fileTransfer/store/sent.go
@@ -108,7 +108,7 @@ func (s *Sent) AddTransfer(recipient *id.ID, key *ftCrypto.TransferKey,
 
 	_, exists := s.transfers[*tid]
 	if exists {
-		return nil, errors.Errorf(errAddExistingSentTransfer, tid)
+		return nil, errors.Errorf(errAddExistingSentTransfer, *tid)
 	}
 
 	st, err := newSentTransfer(
diff --git a/fileTransfer/store/sent_test.go b/fileTransfer/store/sent_test.go
index c343685bf5c4456def286fcb33360b694839f980..f3f24e43e4670b3d317d43d3e88a6c21c2ae4ace 100644
--- a/fileTransfer/store/sent_test.go
+++ b/fileTransfer/store/sent_test.go
@@ -225,8 +225,8 @@ func TestSent_save(t *testing.T) {
 	kv := versioned.NewKV(ekv.MakeMemstore())
 	s, _, _ := NewOrLoadSent(kv)
 	s.transfers = map[ftCrypto.TransferID]*SentTransfer{
-		ftCrypto.TransferID{0}: nil, ftCrypto.TransferID{1}: nil,
-		ftCrypto.TransferID{2}: nil, ftCrypto.TransferID{3}: nil,
+		{0}: nil, {1}: nil,
+		{2}: nil, {3}: nil,
 	}
 
 	err := s.save()
diff --git a/fileTransfer/utils_test.go b/fileTransfer/utils_test.go
index 32c34ab230cb63a8d74aee6c2e50931b85ce6592..ff7750fcbce0c8d99f260ffdd2c48bb6d64d14a7 100644
--- a/fileTransfer/utils_test.go
+++ b/fileTransfer/utils_test.go
@@ -166,12 +166,12 @@ func (m *mockCmix) GetMaxMessageLength() int {
 }
 
 func (m *mockCmix) Send(*id.ID, format.Fingerprint, message.Service, []byte,
-	[]byte, cmix.CMIXParams) (id.Round, ephemeral.Id, error) {
+	[]byte, cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
 	panic("implement me")
 }
 
 func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage,
-	_ cmix.CMIXParams) (id.Round, []ephemeral.Id, error) {
+	_ cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) {
 	m.handler.Lock()
 	defer m.handler.Unlock()
 	round := m.round
@@ -185,7 +185,12 @@ func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage,
 			receptionID.EphemeralIdentity{Source: targetedMsg.Recipient},
 			rounds.Round{ID: round})
 	}
-	return round, []ephemeral.Id{}, nil
+	return rounds.Round{ID: round}, []ephemeral.Id{}, nil
+}
+
+func (m *mockCmix) SendWithAssembler(*id.ID, cmix.MessageAssembler,
+	cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
+	panic("implement me")
 }
 
 func (m *mockCmix) AddIdentity(*id.ID, time.Time, bool)            { panic("implement me") }
@@ -237,18 +242,19 @@ func (m *mockCmix) NumRegisteredNodes() int        { panic("implement me") }
 func (m *mockCmix) TriggerNodeRegistration(*id.ID) { panic("implement me") }
 
 func (m *mockCmix) GetRoundResults(_ time.Duration,
-	roundCallback cmix.RoundEventCallback, rids ...id.Round) error {
+	roundCallback cmix.RoundEventCallback, rids ...id.Round) {
 	go roundCallback(true, false, map[id.Round]cmix.RoundResult{rids[0]: {}})
-	return nil
 }
 
 func (m *mockCmix) LookupHistoricalRound(id.Round, rounds.RoundResultCallback) error {
 	panic("implement me")
 }
-func (m *mockCmix) SendToAny(func(host *connect.Host) (interface{}, error), *stoppable.Single) (interface{}, error) {
+func (m *mockCmix) SendToAny(func(host *connect.Host) (interface{}, error),
+	*stoppable.Single) (interface{}, error) {
 	panic("implement me")
 }
-func (m *mockCmix) SendToPreferred([]*id.ID, gateway.SendToPreferredFunc, *stoppable.Single, time.Duration) (interface{}, error) {
+func (m *mockCmix) SendToPreferred([]*id.ID, gateway.SendToPreferredFunc,
+	*stoppable.Single, time.Duration) (interface{}, error) {
 	panic("implement me")
 }
 func (m *mockCmix) SetGatewayFilter(gateway.Filter)   { panic("implement me") }
diff --git a/go.mod b/go.mod
index dbcf2c6ea9aeed6f58d7159cf847bd27eda87ae7..13f55b334e9e8a2f31d1bcc842f14bc4140a548f 100644
--- a/go.mod
+++ b/go.mod
@@ -12,35 +12,47 @@ require (
 	github.com/spf13/cobra v1.5.0
 	github.com/spf13/jwalterweatherman v1.1.0
 	github.com/spf13/viper v1.12.0
+	github.com/stretchr/testify v1.8.0
 	gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f
-	gitlab.com/elixxir/comms v0.0.4-0.20220916183942-257688b39f0c
-	gitlab.com/elixxir/crypto v0.0.7-0.20220901215826-1ceaeb59081f
+	gitlab.com/elixxir/comms v0.0.4-0.20221024232930-61a6369c1f68
+	gitlab.com/elixxir/crypto v0.0.7-0.20221024215625-33315c3de43e
 	gitlab.com/elixxir/ekv v0.2.1
-	gitlab.com/elixxir/primitives v0.0.3-0.20220901220638-1acc75fabdc6
-	gitlab.com/xx_network/comms v0.0.4-0.20220916183430-cc3077fedf9f
-	gitlab.com/xx_network/crypto v0.0.5-0.20220902182733-69aad094b487
-	gitlab.com/xx_network/primitives v0.0.4-0.20220902183448-319596e2fec8
+	gitlab.com/elixxir/primitives v0.0.3-0.20221017172918-6176818d1aba
+	gitlab.com/xx_network/comms v0.0.4-0.20221017172508-09e33697dc15
+	gitlab.com/xx_network/crypto v0.0.5-0.20221017172404-b384a8d8b171
+	gitlab.com/xx_network/primitives v0.0.4-0.20221017171439-42169a3e5c0d
 	go.uber.org/ratelimit v0.2.0
 	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
-	golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b
-	google.golang.org/grpc v1.48.0
+	golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c
+	google.golang.org/grpc v1.49.0
 	google.golang.org/protobuf v1.28.1
 )
 
 require (
+	git.xx.network/elixxir/grpc-web-go-client v0.0.0-20220908170150-ef04339ffe65 // indirect
 	github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
 	github.com/badoux/checkmail v1.2.1 // indirect
+	github.com/cenkalti/backoff/v4 v4.1.3 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
 	github.com/elliotchance/orderedmap v1.4.0 // indirect
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
+	github.com/gorilla/websocket v1.5.0 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
+	github.com/improbable-eng/grpc-web v0.15.0 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
+	github.com/klauspost/compress v1.11.7 // indirect
 	github.com/klauspost/cpuid/v2 v2.1.0 // indirect
 	github.com/magiconair/properties v1.8.6 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/pelletier/go-toml v1.9.5 // indirect
 	github.com/pelletier/go-toml/v2 v2.0.2 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/rs/cors v1.8.2 // indirect
+	github.com/sethvargo/go-diceware v0.3.0 // indirect
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
+	github.com/soheilhy/cmux v0.1.5 // indirect
 	github.com/spf13/afero v1.9.2 // indirect
 	github.com/spf13/cast v1.5.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
@@ -50,10 +62,12 @@ require (
 	github.com/tyler-smith/go-bip39 v1.1.0 // indirect
 	github.com/zeebo/blake3 v0.2.3 // indirect
 	gitlab.com/xx_network/ring v0.0.3-0.20220222211904-da613960ad93 // indirect
+	go.uber.org/atomic v1.10.0 // indirect
 	golang.org/x/sys v0.0.0-20220731174439-a90be440212d // indirect
 	golang.org/x/text v0.3.7 // indirect
-	google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78 // indirect
+	google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect
 	gopkg.in/ini.v1 v1.66.6 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
+	nhooyr.io/websocket v1.8.7 // indirect
 )
diff --git a/go.sum b/go.sum
index 16d580d24dacae4af5cdd67a6a41c22af883a5a5..31dcefda3e6f7209aa7a43976c0c2c65890427bd 100644
--- a/go.sum
+++ b/go.sum
@@ -54,10 +54,17 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
 cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+git.xx.network/elixxir/grpc-web-go-client v0.0.0-20220908170150-ef04339ffe65 h1:ksB3ZiMeFplqlaCjMDqKegbGzDZdS5pU0Z5GgFeBdZ0=
+git.xx.network/elixxir/grpc-web-go-client v0.0.0-20220908170150-ef04339ffe65/go.mod h1:uFKw2wmgtlYMdiIm08dM0Vj4XvX9ZKVCj71c8O7SAPo=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
+github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
+github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
+github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -66,11 +73,17 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5
 github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI=
 github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
+github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
+github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
 github.com/badoux/checkmail v1.2.1 h1:TzwYx5pnsV6anJweMx2auXdekBwGr/yt1GgalIx9nBQ=
 github.com/badoux/checkmail v1.2.1/go.mod h1:XroCOBU5zzZJcLvgwU15I+2xXyCdTWXyR9MGfRhBYy0=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -79,6 +92,12 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
 github.com/bwesterb/go-ristretto v1.2.1/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
+github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
+github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
+github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
+github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
+github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
+github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -87,6 +106,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
 github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
+github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
 github.com/cloudflare/circl v1.2.0 h1:NheeISPSUcYftKlfrLuOo4T62FkmD4t4jviLfFFYaec=
@@ -100,16 +120,32 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH
 github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
+github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I=
+github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
+github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
+github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
+github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
 github.com/elliotchance/orderedmap v1.4.0 h1:wZtfeEONCbx6in1CZyE6bELEt/vFayMvsxqI5SgsR+A=
 github.com/elliotchance/orderedmap v1.4.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys=
+github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -123,28 +159,54 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
+github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
+github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
 github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
 github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
+github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
 github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
+github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
+github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
+github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
+github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
+github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
 github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -176,6 +238,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
 github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
 github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -215,6 +278,7 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe
 github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
@@ -224,9 +288,22 @@ github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/Oth
 github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
 github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
 github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
+github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
 github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
+github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
 github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
@@ -240,37 +317,61 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP
 github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
 github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
 github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
+github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
 github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
 github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
 github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
 github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
+github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
 github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
+github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
 github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
+github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
 github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
 github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/improbable-eng/grpc-web v0.12.0/go.mod h1:6hRR09jOEG81ADP5wCQju1z71g6OL4eEvELdran/3cs=
+github.com/improbable-eng/grpc-web v0.14.1/go.mod h1:zEjGHa8DAlkoOXmswrNvhUGEYQA9UI7DhrGeHR1DMGU=
+github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ=
+github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.11.7 h1:0hzRabrMN4tSTvMfnL3SCv1ZGeAP23ynzodBgaHeMeg=
+github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
@@ -289,8 +390,21 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/ktr0731/dept v0.1.3/go.mod h1:b1EtCEjbjGShAfhZue+BrFKTG7sQmK7aSD7Q6VcGvO0=
+github.com/ktr0731/go-multierror v0.0.0-20171204182908-b7773ae21874/go.mod h1:ZWayuE/hCzOD96CJizvcYnqrbmTC7RAG332yNtlKj6w=
+github.com/ktr0731/grpc-test v0.1.4/go.mod h1:v47616grayBYXQveGWxO3OwjLB3nEEnHsZuMTc73FM0=
+github.com/ktr0731/grpc-test v0.1.12 h1:Yha+zH2hB48huOfbsEMfyG7FeHCrVWq4fYmHfr3iH3U=
+github.com/ktr0731/grpc-test v0.1.12/go.mod h1:AP4+ZrqSzdDaUNhAsp2fye06MXO2fdYY6YQJifb588M=
+github.com/ktr0731/grpc-web-go-client v0.2.8 h1:nUf9p+YWirmFwmH0mwtAWhuXvzovc+/3C/eAY2Fshnk=
+github.com/ktr0731/grpc-web-go-client v0.2.8/go.mod h1:1Iac8gFJvC/DRfZoGnFZsfEbEq/wQFK+2Ve1o3pHkCQ=
+github.com/ktr0731/modfile v1.11.2/go.mod h1:LzNwnHJWHbuDh3BO17lIqzqDldXqGu1HCydWH3SinE0=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
+github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
 github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea h1:uyJ13zfy6l79CM3HnVhDalIyZ4RJAyVfDrbnfFeJoC4=
 github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea/go.mod h1:w4pGU9PkiX2hAWyF0yuHEHmYTQFAd6WHzp6+IY7JVjE=
+github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
 github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
 github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -299,48 +413,90 @@ github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope
 github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
 github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
 github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
 github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
 github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
+github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
+github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
 github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
 github.com/multiformats/go-multihash v0.2.1/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc=
 github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mwitkow/grpc-proxy v0.0.0-20181017164139-0f1106ef9c76/go.mod h1:x5OoJHDHqxHS801UIuhqGl6QdSAEJvtausosHSdazIo=
+github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
+github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
+github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
+github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
+github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
+github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
+github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
+github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
+github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
+github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
+github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
+github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
+github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
+github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
+github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
+github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
 github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
 github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
 github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw=
 github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI=
+github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
+github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
+github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
 github.com/pkg/profile v1.6.0 h1:hUDfIISABYI59DyeB3OTay/HxSRwTQ8rB/H83k6r5dM=
 github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
 github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
@@ -349,36 +505,63 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
 github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
 github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
 github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
 github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
+github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
 github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
 github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
 github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs=
+github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
+github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U=
+github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8=
+github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/sethvargo/go-diceware v0.3.0 h1:UVVEfmN/uF50JfWAN7nbY6CiAlp5xeSx+5U0lWKkMCQ=
+github.com/sethvargo/go-diceware v0.3.0/go.mod h1:lH5Q/oSPMivseNdhMERAC7Ti5oOPqsaVddU1BcN1CY0=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
+github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
+github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
@@ -386,16 +569,22 @@ github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
 github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
 github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
 github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
 github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
 github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
 github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
+github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
+github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
+github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -403,11 +592,13 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
 github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs=
 github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs=
 github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0XS0qTf5FznsMOzTjGqavBGuCbo0=
 github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2/go.mod h1:4kyMkleCiLkgY6z8gK5BkI01ChBtxR0ro3I1ZDcGM3w=
 github.com/ttacon/libphonenumber v1.2.1 h1:fzOfY5zUADkCkbIafAed11gL1sW+bJ26p6zWLBMElR4=
@@ -415,6 +606,13 @@ github.com/ttacon/libphonenumber v1.2.1/go.mod h1:E0TpmdVMq5dyVlQ7oenAkhsLu86OkU
 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
 github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
 github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
+github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -433,47 +631,59 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
 github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
 gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f h1:yXGvNBqzZwAhDYlSnxPRbgor6JWoOt1Z7s3z1O9JR40=
 gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f/go.mod h1:H6jztdm0k+wEV2QGK/KYA+MY9nj9Zzatux/qIvDDv3k=
-gitlab.com/elixxir/comms v0.0.4-0.20220916183942-257688b39f0c h1:N++gLUKVOfwgk/YnVIkIXpXznkpGb34nAqIcePkXoL8=
-gitlab.com/elixxir/comms v0.0.4-0.20220916183942-257688b39f0c/go.mod h1:Yy5EAFq/ua0rZ2g1c3F86c3ry8bnsCACSVjRmwdGuIM=
+gitlab.com/elixxir/comms v0.0.4-0.20221017173926-4eaa6061dfaa h1:/FEpu0N0rAyq74FkvO3uY8BcQoWLSbVPhj/s5QfscZw=
+gitlab.com/elixxir/comms v0.0.4-0.20221017173926-4eaa6061dfaa/go.mod h1:rW7xdbHntP2MoF3q+2+f+IR8OHol94MRyviotfR5rXg=
+gitlab.com/elixxir/comms v0.0.4-0.20221021234520-a4f94f752e3e h1:Go3Ec+LOm8t6j8wVgI4GqTfuy+PQkyblZCYQU7yGB2E=
+gitlab.com/elixxir/comms v0.0.4-0.20221021234520-a4f94f752e3e/go.mod h1:rW7xdbHntP2MoF3q+2+f+IR8OHol94MRyviotfR5rXg=
+gitlab.com/elixxir/comms v0.0.4-0.20221023173239-c75420d94293 h1:QIiZYjdtwjBCYaO7dZI6K8cEaA73wtd/YKIWD1oeDWw=
+gitlab.com/elixxir/comms v0.0.4-0.20221023173239-c75420d94293/go.mod h1:rW7xdbHntP2MoF3q+2+f+IR8OHol94MRyviotfR5rXg=
+gitlab.com/elixxir/comms v0.0.4-0.20221023190124-3441c3fdc3de h1:1YnKkJn3a7xiftFBRqLK5os7C6uF4okMtDXZyLrzIuY=
+gitlab.com/elixxir/comms v0.0.4-0.20221023190124-3441c3fdc3de/go.mod h1:rW7xdbHntP2MoF3q+2+f+IR8OHol94MRyviotfR5rXg=
+gitlab.com/elixxir/comms v0.0.4-0.20221024012811-e6754f7740db h1:LQUde8pjIfQpVdg7trANu6o5uzZv9ADK13S8bkMgkBw=
+gitlab.com/elixxir/comms v0.0.4-0.20221024012811-e6754f7740db/go.mod h1:NevrBdsi5wJvitUeMsid3xI1FrzzuzfxKy4Bapnhzao=
+gitlab.com/elixxir/comms v0.0.4-0.20221024050701-bced94c1b026 h1:CdqvzyM91wN6u4MmGj0n+gKO/0tJabWPN3EQ4SFsZsg=
+gitlab.com/elixxir/comms v0.0.4-0.20221024050701-bced94c1b026/go.mod h1:NevrBdsi5wJvitUeMsid3xI1FrzzuzfxKy4Bapnhzao=
+gitlab.com/elixxir/comms v0.0.4-0.20221024232930-61a6369c1f68 h1:DKsCIo15pFed5QGVIw/sJ+5bwoR4Tyaq//hBiJUOWfs=
+gitlab.com/elixxir/comms v0.0.4-0.20221024232930-61a6369c1f68/go.mod h1:NevrBdsi5wJvitUeMsid3xI1FrzzuzfxKy4Bapnhzao=
 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.20220606201132-c370d5039cea/go.mod h1:Oy+VWQ2Sa0Ybata3oTV+Yc46hkaDwAsuIMW0wJ01z2M=
-gitlab.com/elixxir/crypto v0.0.7-0.20220901215826-1ceaeb59081f h1:G5I01Ob+ff0YjGyluQ2h3+I97qWtOyNRrJGIJkJWaW8=
-gitlab.com/elixxir/crypto v0.0.7-0.20220901215826-1ceaeb59081f/go.mod h1:IYRYQwpS3zhTVtrir+kGGTqRCMI3It/i6mGW/mxezrM=
+gitlab.com/elixxir/crypto v0.0.7-0.20221017173452-565da4101a3b/go.mod h1:1rftbwSVdy49LkBIkPr+w+P2mDOerYeBKoZuB3r0yqI=
+gitlab.com/elixxir/crypto v0.0.7-0.20221022003355-d8a6158b32a7 h1:+8DHBxZxJcmJSmcUFK4ZjjXgwV3wSo9O4+4NCaLdO4c=
+gitlab.com/elixxir/crypto v0.0.7-0.20221022003355-d8a6158b32a7/go.mod h1:P/S3pEPYl7fuHQ1m4mL2pIaCxAjYIXrJml/pnfofI+U=
+gitlab.com/elixxir/crypto v0.0.7-0.20221024012326-cf941c375c1f h1:ku5gWZnvgs8TPHfGIOfKO5QPnRJl5fsSAic5H1Y/QRg=
+gitlab.com/elixxir/crypto v0.0.7-0.20221024012326-cf941c375c1f/go.mod h1:P/S3pEPYl7fuHQ1m4mL2pIaCxAjYIXrJml/pnfofI+U=
+gitlab.com/elixxir/crypto v0.0.7-0.20221024215625-33315c3de43e h1:CIb5XdBTSUf9U/MDWv9DrjRicSYPDLudETMUkxzNVjQ=
+gitlab.com/elixxir/crypto v0.0.7-0.20221024215625-33315c3de43e/go.mod h1:P/S3pEPYl7fuHQ1m4mL2pIaCxAjYIXrJml/pnfofI+U=
 gitlab.com/elixxir/ekv v0.2.1 h1:dtwbt6KmAXG2Tik5d60iDz2fLhoFBgWwST03p7T+9Is=
 gitlab.com/elixxir/ekv v0.2.1/go.mod h1:USLD7xeDnuZEavygdrgzNEwZXeLQJK/w1a+htpN+JEU=
 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.20220606195757-40f7a589347f/go.mod h1:9Bb2+u+CDSwsEU5Droo6saDAXuBDvLRjexpBhPAYxhA=
-gitlab.com/elixxir/primitives v0.0.3-0.20220810173935-592f34a88326/go.mod h1:9Bb2+u+CDSwsEU5Droo6saDAXuBDvLRjexpBhPAYxhA=
-gitlab.com/elixxir/primitives v0.0.3-0.20220901220638-1acc75fabdc6 h1:/cxxZBP5jTPDpC3zgOx9vV1ojmJyG8pYtkl3IbcewNQ=
-gitlab.com/elixxir/primitives v0.0.3-0.20220901220638-1acc75fabdc6/go.mod h1:9Bb2+u+CDSwsEU5Droo6saDAXuBDvLRjexpBhPAYxhA=
+gitlab.com/elixxir/primitives v0.0.3-0.20221017172918-6176818d1aba h1:skhrlmHdEQfuT/4Ip2IVcI4ToNzPlmAxTYlTk034Il0=
+gitlab.com/elixxir/primitives v0.0.3-0.20221017172918-6176818d1aba/go.mod h1:Rqbl8ToZxPn86RyqrpjIE9FJbFHoJgQfg68iSOTPcz8=
 gitlab.com/xx_network/comms v0.0.0-20200805174823-841427dd5023/go.mod h1:owEcxTRl7gsoM8c3RQ5KAm5GstxrJp5tn+6JfQ4z5Hw=
-gitlab.com/xx_network/comms v0.0.4-0.20220916183430-cc3077fedf9f h1:oomZumvJSgKvzOX7XGIkixHitxqUCCeB6YCNQxju6Dc=
-gitlab.com/xx_network/comms v0.0.4-0.20220916183430-cc3077fedf9f/go.mod h1:TraR4sW+YxK/2CV+IQUNaAV61+ElrBV0kXd5HLEjM7M=
+gitlab.com/xx_network/comms v0.0.4-0.20221017172508-09e33697dc15 h1:H2OptkCpcGAgplz5PYMqUiA4aH/LUcjc9nLbGM+vUus=
+gitlab.com/xx_network/comms v0.0.4-0.20221017172508-09e33697dc15/go.mod h1:yBDaXAWiPGGv2zXQgP35Ks/ek800Uaah4roVFyvMkWU=
 gitlab.com/xx_network/crypto v0.0.3/go.mod h1:DF2HYvvCw9wkBybXcXAgQMzX+MiGbFPjwt3t17VRqRE=
 gitlab.com/xx_network/crypto v0.0.4/go.mod h1:+lcQEy+Th4eswFgQDwT0EXKp4AXrlubxalwQFH5O0Mk=
-gitlab.com/xx_network/crypto v0.0.5-0.20220606200528-3f886fe49e81/go.mod h1:/SJf+R75E+QepdTLh0H1/udsovxx2Q5ru34q1v0umKk=
-gitlab.com/xx_network/crypto v0.0.5-0.20220729193517-1e5e96f39f6e/go.mod h1:/SJf+R75E+QepdTLh0H1/udsovxx2Q5ru34q1v0umKk=
-gitlab.com/xx_network/crypto v0.0.5-0.20220902182733-69aad094b487 h1:CsnSSVw2o4PXFKo4OFbivjoLA0RE/Ktx9hxvtQRGNjw=
-gitlab.com/xx_network/crypto v0.0.5-0.20220902182733-69aad094b487/go.mod h1:/SJf+R75E+QepdTLh0H1/udsovxx2Q5ru34q1v0umKk=
+gitlab.com/xx_network/crypto v0.0.5-0.20221017172404-b384a8d8b171 h1:zGrUU8U6pMl5J4yzKzJ3QOAoo6g0Nna7Z+vVxxijpLg=
+gitlab.com/xx_network/crypto v0.0.5-0.20221017172404-b384a8d8b171/go.mod h1:sZ5bMKbb6v+Qx3zwXsWsbhVikOte/1ibflLpbCVuboQ=
 gitlab.com/xx_network/primitives v0.0.0-20200803231956-9b192c57ea7c/go.mod h1:wtdCMr7DPePz9qwctNoAUzZtbOSHSedcK++3Df3psjA=
 gitlab.com/xx_network/primitives v0.0.0-20200804183002-f99f7a7284da/go.mod h1:OK9xevzWCaPO7b1wiluVJGk7R5ZsuC7pHY5hteZFQug=
 gitlab.com/xx_network/primitives v0.0.2/go.mod h1:cs0QlFpdMDI6lAo61lDRH2JZz+3aVkHy+QogOB6F/qc=
-gitlab.com/xx_network/primitives v0.0.4-0.20220222211843-901fa4a2d72b/go.mod h1:9imZHvYwNFobxueSvVtHneZLk9wTK7HQTzxPm+zhFhE=
-gitlab.com/xx_network/primitives v0.0.4-0.20220324193139-b292d1ae6e7e/go.mod h1:AXVVFt7dDAeIUpOGPiStCcUIKsBXLWbmV/BgZ4T+tOo=
-gitlab.com/xx_network/primitives v0.0.4-0.20220630163313-7890038258c6/go.mod h1:AXVVFt7dDAeIUpOGPiStCcUIKsBXLWbmV/BgZ4T+tOo=
-gitlab.com/xx_network/primitives v0.0.4-0.20220712193914-aebd8544396e/go.mod h1:AXVVFt7dDAeIUpOGPiStCcUIKsBXLWbmV/BgZ4T+tOo=
-gitlab.com/xx_network/primitives v0.0.4-0.20220902183448-319596e2fec8 h1:esjWkrzTD+bpTgT5NphhdpX/fbSpZRXLpPaXb4HqkXg=
-gitlab.com/xx_network/primitives v0.0.4-0.20220902183448-319596e2fec8/go.mod h1:AXVVFt7dDAeIUpOGPiStCcUIKsBXLWbmV/BgZ4T+tOo=
+gitlab.com/xx_network/primitives v0.0.4-0.20221017171439-42169a3e5c0d h1:rOcGGx5SrhBkbD1P8JqbtqBBeuGhOSZxETytNGJcf/U=
+gitlab.com/xx_network/primitives v0.0.4-0.20221017171439-42169a3e5c0d/go.mod h1:AXVVFt7dDAeIUpOGPiStCcUIKsBXLWbmV/BgZ4T+tOo=
 gitlab.com/xx_network/ring v0.0.3-0.20220222211904-da613960ad93 h1:eJZrXqHsMmmejEPWw8gNAt0I8CGAMNO/7C339Zco3TM=
 gitlab.com/xx_network/ring v0.0.3-0.20220222211904-da613960ad93/go.mod h1:aLzpP2TiZTQut/PVHR40EJAomzugDdHXetbieRClXIM=
+go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
 go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
 go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
 go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU=
 go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
+go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
+go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -482,16 +692,30 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
-go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
+go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
+go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
 go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA=
 go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
 go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -502,7 +726,6 @@ golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWP
 golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
@@ -519,6 +742,7 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -546,8 +770,13 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -558,6 +787,7 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -566,6 +796,7 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
@@ -576,6 +807,7 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -584,7 +816,7 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
 golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
@@ -592,8 +824,8 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su
 golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b h1:3ogNYyK4oIQdIKzTu68hQrr4iuVxF3AxKl9Aj/eDrw0=
-golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
+golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c h1:JVAXQ10yGGVbSyoer5VILysz6YKjdNT2bsvlayjqhes=
+golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -629,7 +861,11 @@ golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -640,6 +876,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -647,6 +884,7 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -659,6 +897,7 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -694,6 +933,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -726,15 +966,19 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@@ -745,6 +989,9 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -752,6 +999,7 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@@ -790,6 +1038,7 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -830,6 +1079,7 @@ google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69
 google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
 google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
@@ -841,6 +1091,7 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn
 google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
 google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
@@ -851,12 +1102,14 @@ google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvx
 google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200204235621-fb4a7afc5178/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
 google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
@@ -872,6 +1125,7 @@ google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
@@ -914,12 +1168,17 @@ google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX
 google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
 google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
 google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
-google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78 h1:QntLWYqZeuBtJkth3m/6DLznnI0AHJr+AgJXvVh/izw=
-google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=
+google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc h1:Nf+EdcTLHR8qDNN/KfkQL0u0ssxt9OhbaWCl5C0ucEI=
+google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
+google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
@@ -929,6 +1188,7 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
 google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
 google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
@@ -946,8 +1206,9 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5
 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
 google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
 google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
-google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w=
 google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw=
+google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -972,10 +1233,17 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
 gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
 gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -991,6 +1259,7 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C
 gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -999,7 +1268,12 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
+nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
+nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
+nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
 sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
+sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
diff --git a/groupChat/interface.go b/groupChat/interface.go
index 983d709d87db528c144d5583f1aa18419edd8b21..e9b14c911abe56c1072e571d552dca6047d35cbd 100644
--- a/groupChat/interface.go
+++ b/groupChat/interface.go
@@ -24,6 +24,7 @@ import (
 	"gitlab.com/elixxir/client/catalog"
 	"gitlab.com/elixxir/client/cmix"
 	"gitlab.com/elixxir/client/cmix/message"
+	"gitlab.com/elixxir/client/cmix/rounds"
 	"gitlab.com/elixxir/client/e2e"
 	"gitlab.com/elixxir/client/e2e/ratchet/partner"
 	sessionImport "gitlab.com/elixxir/client/e2e/ratchet/partner/session"
@@ -68,7 +69,7 @@ type GroupChat interface {
 	// 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, tag string, message []byte) (
-		id.Round, time.Time, group.MessageID, error)
+		rounds.Round, time.Time, group.MessageID, error)
 
 	// GetGroups returns a list of all registered GroupChat IDs.
 	GetGroups() []*id.ID
@@ -114,7 +115,7 @@ type groupE2e interface {
 // methods needed by GroupChat
 type groupCmix interface {
 	SendMany(messages []cmix.TargetedCmixMessage, p cmix.CMIXParams) (
-		id.Round, []ephemeral.Id, error)
+		rounds.Round, []ephemeral.Id, error)
 	AddService(
 		clientID *id.ID, newService message.Service, response message.Processor)
 	DeleteService(
diff --git a/groupChat/networkManager_test.go b/groupChat/networkManager_test.go
index 9c6535d1c73f7114c50012557f993b405f1a261f..e87b3f655bca1e2d5ccbf3d6547a25c1b1ac471d 100644
--- a/groupChat/networkManager_test.go
+++ b/groupChat/networkManager_test.go
@@ -44,9 +44,9 @@ func newTestNetworkManager(sendErr int) cmix.Client {
 	}
 }
 
-func (tnm *testNetworkManager) SendMany(messages []cmix.TargetedCmixMessage, _ cmix.CMIXParams) (id.Round, []ephemeral.Id, error) {
+func (tnm *testNetworkManager) SendMany(messages []cmix.TargetedCmixMessage, _ cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) {
 	if tnm.sendErr == 1 {
-		return 0, nil, errors.New("SendManyCMIX error")
+		return rounds.Round{}, nil, errors.New("SendManyCMIX error")
 	}
 
 	tnm.Lock()
@@ -63,7 +63,7 @@ func (tnm *testNetworkManager) SendMany(messages []cmix.TargetedCmixMessage, _ c
 		receiveMessages = append(receiveMessages, receiveMsg)
 	}
 	tnm.receptionMessages = append(tnm.receptionMessages, receiveMessages)
-	return 0, nil, nil
+	return rounds.Round{}, nil, nil
 }
 
 func (*testNetworkManager) AddService(*id.ID, message.Service, message.Processor)    {}
@@ -78,7 +78,14 @@ func (tnm *testNetworkManager) Follow(report cmix.ClientErrorReport) (stoppable.
 	panic("implement me")
 }
 
-func (tnm *testNetworkManager) Send(recipient *id.ID, fingerprint format.Fingerprint, service message.Service, payload, mac []byte, cmixParams cmix.CMIXParams) (id.Round, ephemeral.Id, error) {
+func (tnm *testNetworkManager) SendWithAssembler(recipient *id.ID,
+	assembler cmix.MessageAssembler, cmixParams cmix.CMIXParams) (rounds.Round,
+	ephemeral.Id, error) {
+	//TODO implement me
+	panic("implement me")
+}
+
+func (tnm *testNetworkManager) Send(recipient *id.ID, fingerprint format.Fingerprint, service message.Service, payload, mac []byte, cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
 	//TODO implement me
 	panic("implement me")
 }
@@ -163,7 +170,7 @@ func (tnm *testNetworkManager) TriggerNodeRegistration(nid *id.ID) {
 	panic("implement me")
 }
 
-func (tnm *testNetworkManager) GetRoundResults(timeout time.Duration, roundCallback cmix.RoundEventCallback, roundList ...id.Round) error {
+func (tnm *testNetworkManager) GetRoundResults(timeout time.Duration, roundCallback cmix.RoundEventCallback, roundList ...id.Round) {
 	//TODO implement me
 	panic("implement me")
 }
diff --git a/groupChat/send.go b/groupChat/send.go
index b75f6e209fde626c64f25686928ebbf0c8f01418..9486885e4feeefc17d7d618bfe768927079642c6 100644
--- a/groupChat/send.go
+++ b/groupChat/send.go
@@ -12,6 +12,7 @@ import (
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/cmix"
 	"gitlab.com/elixxir/client/cmix/message"
+	"gitlab.com/elixxir/client/cmix/rounds"
 	gs "gitlab.com/elixxir/client/groupChat/groupStore"
 	"gitlab.com/elixxir/crypto/group"
 	"gitlab.com/xx_network/primitives/id"
@@ -44,7 +45,7 @@ const (
 // Send sends a message to all group members using Cmix.SendMany.
 // The send fails if the message is too long.
 func (m *manager) Send(groupID *id.ID, tag string, message []byte) (
-	id.Round, time.Time, group.MessageID, error) {
+	rounds.Round, time.Time, group.MessageID, error) {
 
 	if tag == "" {
 		tag = defaultServiceTag
@@ -53,7 +54,7 @@ func (m *manager) Send(groupID *id.ID, tag string, message []byte) (
 	// Get the relevant group
 	g, exists := m.GetGroup(groupID)
 	if !exists {
-		return 0, time.Time{}, group.MessageID{},
+		return rounds.Round{}, time.Time{}, group.MessageID{},
 			errors.Errorf(newNoGroupErr, groupID)
 	}
 
@@ -63,7 +64,7 @@ func (m *manager) Send(groupID *id.ID, tag string, message []byte) (
 	// Create a cMix message for each group member
 	groupMessages, msgId, err := m.newMessages(g, tag, message, timeNow)
 	if err != nil {
-		return 0, time.Time{}, group.MessageID{},
+		return rounds.Round{}, time.Time{}, group.MessageID{},
 			errors.Errorf(newCmixMsgErr, g.Name, g.ID, err)
 	}
 
@@ -72,7 +73,7 @@ func (m *manager) Send(groupID *id.ID, tag string, message []byte) (
 	param.DebugTag = "group.Message"
 	rid, _, err := m.getCMix().SendMany(groupMessages, param)
 	if err != nil {
-		return 0, time.Time{}, group.MessageID{},
+		return rounds.Round{}, time.Time{}, group.MessageID{},
 			errors.Errorf(sendManyCmixErr, m.getReceptionIdentity().ID, g.Name, g.ID, err)
 	}
 
diff --git a/groupChat/send_test.go b/groupChat/send_test.go
index 2b19472859973171c40409527c48412ddda4f306..c0d4f599f63e7e1ab853af0257c80cf5cb662a6e 100644
--- a/groupChat/send_test.go
+++ b/groupChat/send_test.go
@@ -56,7 +56,7 @@ func Test_manager_Send(t *testing.T) {
 		reception.Process(msg, receptionID.EphemeralIdentity{
 			EphId: ephemeral.Id{1, 2, 3}, Source: &id.ID{4, 5, 6},
 		},
-			rounds.Round{ID: roundId, Timestamps: timestamps})
+			rounds.Round{ID: roundId.ID, Timestamps: timestamps})
 		select {
 		case result := <-msgChan:
 			if !result.SenderID.Cmp(m.getReceptionIdentity().ID) {
diff --git a/groupChat/wrapper.go b/groupChat/wrapper.go
index 4e9d73b3e46d4e94ea985ab572baf32fa57029eb..462811c8d20f606353c419f61bd079e2a8480875 100644
--- a/groupChat/wrapper.go
+++ b/groupChat/wrapper.go
@@ -8,6 +8,7 @@
 package groupChat
 
 import (
+	"gitlab.com/elixxir/client/cmix/rounds"
 	gs "gitlab.com/elixxir/client/groupChat/groupStore"
 	"gitlab.com/elixxir/crypto/group"
 	"gitlab.com/xx_network/primitives/id"
@@ -53,7 +54,7 @@ func (w *Wrapper) LeaveGroup(groupID *id.ID) error {
 
 // Send calls GroupChat.Send.
 func (w *Wrapper) Send(groupID *id.ID, message []byte, tag string) (
-	id.Round, time.Time, group.MessageID, error) {
+	rounds.Round, time.Time, group.MessageID, error) {
 	return w.gc.Send(groupID, tag, message)
 }
 
diff --git a/single/interfaces.go b/single/interfaces.go
index d8a28d9545d824a4dd3ac1f583ea454f45b2b896..8a6f965e8728b05b26a09c9f99fe8c248c19cc52 100644
--- a/single/interfaces.go
+++ b/single/interfaces.go
@@ -44,7 +44,7 @@ type RequestCmix interface {
 	GetMaxMessageLength() int
 	Send(recipient *id.ID, fingerprint format.Fingerprint,
 		service cMixMsg.Service, payload, mac []byte,
-		cmixParams cmix.CMIXParams) (id.Round, ephemeral.Id, error)
+		cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error)
 	GetInstance() *network.Instance
 }
 
@@ -72,7 +72,7 @@ type Cmix interface {
 	AddIdentity(id *id.ID, validUntil time.Time, persistent bool)
 	Send(recipient *id.ID, fingerprint format.Fingerprint,
 		service message.Service, payload, mac []byte, cmixParams cmix.CMIXParams) (
-		id.Round, ephemeral.Id, error)
+		rounds.Round, ephemeral.Id, error)
 	AddService(clientID *id.ID, newService message.Service,
 		response message.Processor)
 	DeleteService(clientID *id.ID, toDelete message.Service,
diff --git a/single/listener_test.go b/single/listener_test.go
index c0c0ae24907b940278ba41c2e9928fb1116e2ea9..43006ae979f8a9801921566ebff95ac27ca73fec 100644
--- a/single/listener_test.go
+++ b/single/listener_test.go
@@ -255,7 +255,7 @@ func (m mockListenCmix) GetMaxMessageLength() int {
 
 func (m mockListenCmix) Send(recipient *id.ID, fingerprint format.Fingerprint,
 	service cMixMsg.Service, payload, mac []byte, _ cmix.CMIXParams) (
-	id.Round, ephemeral.Id, error) {
+	rounds.Round, ephemeral.Id, error) {
 	msg := format.NewMessage(m.numPrimeBytes)
 	msg.SetContents(payload)
 	msg.SetMac(mac)
@@ -270,7 +270,7 @@ func (m mockListenCmix) Send(recipient *id.ID, fingerprint format.Fingerprint,
 		p.Process(msg, receptionID.EphemeralIdentity{}, rounds.Round{})
 	}
 
-	return 0, ephemeral.Id{}, nil
+	return rounds.Round{}, ephemeral.Id{}, nil
 }
 
 func (m mockListenCmix) GetInstance() *network.Instance {
diff --git a/single/receivedRequest.go b/single/receivedRequest.go
index 70ff0dc6f1fe673f69f13d4bbd826aa55cfdcc8a..cc05610088cca70c26539b5df5a0a090616b67cc 100644
--- a/single/receivedRequest.go
+++ b/single/receivedRequest.go
@@ -104,11 +104,11 @@ func (r *Request) Respond(payload []byte, cMixParams cmix.CMIXParams,
 
 			jww.DEBUG.Printf("[SU] Sent single-use response cMix message part "+
 				"%d of %d on round %d to %s (eph ID %d) (%s).",
-				i, len(parts), round, r.sender, ephID.Int64(), r.tag)
-			rounds[i] = round
+				i, len(parts), round.ID, r.sender, ephID.Int64(), r.tag)
+			rounds[i] = round.ID
 
 			r.net.GetInstance().GetRoundEvents().AddRoundEventChan(
-				round, sendResults, timeout, states.COMPLETED, states.FAILED)
+				round.ID, sendResults, timeout, states.COMPLETED, states.FAILED)
 		}(i, parts[i].Marshal())
 	}
 
diff --git a/single/receivedRequest_test.go b/single/receivedRequest_test.go
index d58b9a224fa147a34d96bbc0be4ca127dcdbc6f8..4d5762828038be1ba058cc7e59a4bc1dd8490fca 100644
--- a/single/receivedRequest_test.go
+++ b/single/receivedRequest_test.go
@@ -11,6 +11,7 @@ import (
 	"bytes"
 	"gitlab.com/elixxir/client/cmix"
 	cmixMsg "gitlab.com/elixxir/client/cmix/message"
+	"gitlab.com/elixxir/client/cmix/rounds"
 	"gitlab.com/elixxir/client/single/message"
 	"gitlab.com/elixxir/comms/network"
 	"gitlab.com/elixxir/crypto/cyclic"
@@ -186,14 +187,14 @@ func (m *mockRequestCmix) GetMaxMessageLength() int {
 
 func (m *mockRequestCmix) Send(_ *id.ID, fp format.Fingerprint,
 	_ cmixMsg.Service, payload, mac []byte, _ cmix.CMIXParams) (
-	id.Round, ephemeral.Id, error) {
+	rounds.Round, ephemeral.Id, error) {
 	msg := format.NewMessage(m.numPrimeBytes)
 	msg.SetMac(mac)
 	msg.SetKeyFP(fp)
 	msg.SetContents(payload)
 	m.sendPayload <- msg
 
-	return 0, ephemeral.Id{}, nil
+	return rounds.Round{}, ephemeral.Id{}, nil
 }
 
 func (m *mockRequestCmix) GetInstance() *network.Instance {
diff --git a/single/request.go b/single/request.go
index 01424229f1a6e275abcd5b13bc7df9d964315b0d..16429e835021a6eb5b570ba7d60362ae5ba0dca6 100644
--- a/single/request.go
+++ b/single/request.go
@@ -211,14 +211,14 @@ func TransmitRequest(recipient contact.Contact, tag string, payload []byte,
 
 	jww.DEBUG.Printf("[SU] Sent single-use request cMix message part "+
 		"%d of %d on round %d to %s (eph ID %d) (%s).",
-		0, len(parts)+1, rid, recipient.ID, ephID.Int64(), tag)
+		0, len(parts)+1, rid.ID, recipient.ID, ephID.Int64(), tag)
 
 	var wg sync.WaitGroup
 	wg.Add(len(parts))
 	failed := uint32(0)
 
 	roundIDs := make([]id.Round, len(parts)+1)
-	roundIDs[0] = rid
+	roundIDs[0] = rid.ID
 	for i, part := range parts {
 		go func(i int, part []byte) {
 			defer wg.Done()
@@ -231,9 +231,7 @@ func TransmitRequest(recipient contact.Contact, tag string, payload []byte,
 			encryptedPayload := auth.Crypt(key, fp[:24], requestPart.Marshal())
 			mac := singleUse.MakeMAC(key, encryptedPayload)
 
-			var ephID ephemeral.Id
-			var err error
-			roundIDs[i], ephID, err = net.Send(recipient.ID, fp,
+			r, ephID, err := net.Send(recipient.ID, fp,
 				cmixMsg.Service{}, encryptedPayload, mac, params.CmixParams)
 			if err != nil {
 				atomic.AddUint32(&failed, 1)
@@ -242,6 +240,7 @@ func TransmitRequest(recipient contact.Contact, tag string, payload []byte,
 					i, len(part)+1, recipient.ID, tag, err)
 				return
 			}
+			roundIDs[i] = r.ID
 
 			jww.DEBUG.Printf("[SU] Sent single-use request cMix message part "+
 				"%d of %d on round %d to %s (eph ID %d) (%s).", i,
@@ -263,7 +262,7 @@ func TransmitRequest(recipient contact.Contact, tag string, payload []byte,
 	remainingTimeout := params.Timeout - netTime.Since(timeStart)
 	go waitForTimeout(timeoutKillChan, wrapper, remainingTimeout)
 
-	return []id.Round{rid}, sendingID, nil
+	return []id.Round{rid.ID}, sendingID, nil
 }
 
 // generateDhKeys generates a new public key and DH key.
diff --git a/single/utils_test.go b/single/utils_test.go
index e2657840d3be6112d414178fe5195bdce71a2bf8..003d68e54cf6ca822918d07ca77985323ea8e70a 100644
--- a/single/utils_test.go
+++ b/single/utils_test.go
@@ -118,7 +118,7 @@ func (m *mockCmix) AddIdentity(*id.ID, time.Time, bool) {}
 
 func (m *mockCmix) Send(recipient *id.ID, fp format.Fingerprint,
 	ms message.Service, payload, mac []byte, _ cmix.CMIXParams) (
-	id.Round, ephemeral.Id, error) {
+	rounds.Round, ephemeral.Id, error) {
 
 	msg := format.NewMessage(m.numPrimeBytes)
 	msg.SetMac(mac)
@@ -155,7 +155,7 @@ func (m *mockCmix) Send(recipient *id.ID, fp format.Fingerprint,
 		})
 	}
 
-	return 0, ephemeral.Id{}, nil
+	return rounds.Round{}, ephemeral.Id{}, nil
 }
 
 func serviceKey(ms message.Service) string {
diff --git a/storage/utility/encryptionSalt.go b/storage/utility/encryptionSalt.go
new file mode 100644
index 0000000000000000000000000000000000000000..18b60c15d4d9f3d39aba0d34fb7efb4877fd92e0
--- /dev/null
+++ b/storage/utility/encryptionSalt.go
@@ -0,0 +1,69 @@
+package utility
+
+import (
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/storage/versioned"
+	"gitlab.com/xx_network/primitives/netTime"
+	"io"
+)
+
+// Storage constats
+const (
+	saltKey     = "encryptionSalt"
+	saltVersion = 0
+	saltPrefix  = "encryptionSaltPrefix"
+)
+
+// saltSize is the defined size in bytes of the salt generated in
+// newSalt.
+const saltSize = 32
+
+// NewOrLoadSalt will attempt to find a stored salt if one exists.
+// If one does not exist in storage, a new one will be generated. The newly
+// generated salt will be stored.
+func NewOrLoadSalt(kv *versioned.KV, stream io.Reader) ([]byte, error) {
+	kv = kv.Prefix(saltPrefix)
+	salt, err := loadSalt(kv)
+	if err != nil {
+		jww.WARN.Printf("Failed to load salt, generating new one...")
+		salt, err = newSalt(kv, stream)
+	}
+
+	return salt, err
+}
+
+// loadSalt is a helper function which attempts to load a stored salt from
+// memory.
+func loadSalt(kv *versioned.KV) ([]byte, error) {
+	obj, err := kv.Get(saltKey, saltVersion)
+	if err != nil {
+		return nil, err
+	}
+
+	return obj.Data, nil
+}
+
+// newSalt generates a new random salt. This salt is stored and returned
+// to the caller.
+func newSalt(kv *versioned.KV, stream io.Reader) ([]byte, error) {
+	// Generate a new salt
+	salt := make([]byte, saltSize)
+	_, err := stream.Read(salt)
+	if err != nil {
+		return nil, err
+	}
+
+	// Store salt in storage
+	obj := &versioned.Object{
+		Version:   saltVersion,
+		Timestamp: netTime.Now(),
+		Data:      salt,
+	}
+
+	err = kv.Set(saltKey, obj)
+	if err != nil {
+		return nil, err
+	}
+
+	return salt, nil
+}
\ No newline at end of file
diff --git a/storage/utility/encryptionSalt_test.go b/storage/utility/encryptionSalt_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..f041b9ded2673277022ae55ce3c356e9744ce712
--- /dev/null
+++ b/storage/utility/encryptionSalt_test.go
@@ -0,0 +1,49 @@
+package utility
+
+import (
+	"bytes"
+	"gitlab.com/elixxir/client/storage/versioned"
+	"gitlab.com/elixxir/ekv"
+	"gitlab.com/xx_network/crypto/csprng"
+	"testing"
+)
+
+// Smoke test
+func TestNewOrLoadSalt(t *testing.T) {
+	kv := ekv.MakeMemstore()
+	vkv := versioned.NewKV(kv)
+
+	rng := csprng.NewSystemRNG()
+
+	_, err := NewOrLoadSalt(vkv, rng)
+	if err != nil {
+		t.Fatalf("NewOrLoadSalt error: %+v", err)
+	}
+
+}
+
+// Test that calling NewOrLoadSalt twice returns the same
+// salt that exists in storage.
+func TestLoadSalt(t *testing.T) {
+
+	kv := ekv.MakeMemstore()
+	vkv := versioned.NewKV(kv)
+
+	rng := csprng.NewSystemRNG()
+
+	original, err := NewOrLoadSalt(vkv, rng)
+	if err != nil {
+		t.Fatalf("NewOrLoadSalt error: %+v", err)
+	}
+
+	loaded, err := NewOrLoadSalt(vkv, rng)
+	if err != nil {
+		t.Fatalf("NewOrLoadSalt error: %+v", err)
+	}
+
+	// Test that loaded matches the original (ie a new one was not generated)
+	if !bytes.Equal(original, loaded) {
+		t.Fatalf("Failed to load salt.")
+	}
+
+}
diff --git a/ud/channelIDTracking.go b/ud/channelIDTracking.go
new file mode 100644
index 0000000000000000000000000000000000000000..e40f4a1e33cbfb5519f7f0bc4964df04c9cd01b8
--- /dev/null
+++ b/ud/channelIDTracking.go
@@ -0,0 +1,380 @@
+package ud
+
+import (
+	"crypto/ed25519"
+	"encoding/json"
+	"errors"
+	"gitlab.com/xx_network/primitives/netTime"
+	"sync"
+	"time"
+
+	jww "github.com/spf13/jwalterweatherman"
+
+	"gitlab.com/elixxir/client/channels"
+	"gitlab.com/elixxir/client/stoppable"
+	"gitlab.com/elixxir/client/storage/versioned"
+	"gitlab.com/elixxir/client/xxdk"
+	"gitlab.com/elixxir/comms/mixmessages"
+	"gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	"gitlab.com/xx_network/comms/connect"
+)
+
+const (
+	registrationDiskKey     = "registrationDiskKey"
+	registrationDiskVersion = 0
+	graceDuration           = time.Hour
+)
+
+var ErrChannelLeaseSignature = errors.New("failure to validate lease signature")
+
+// loadRegistrationDisk loads a registrationDisk from the kv
+// and returns the registrationDisk.
+func loadRegistrationDisk(kv *versioned.KV) (registrationDisk, error) {
+	obj, err := kv.Get(registrationDiskKey, registrationDiskVersion)
+	if err != nil {
+		return registrationDisk{}, err
+	}
+	return UnmarshallRegistrationDisk(obj.Data)
+}
+
+// saveRegistrationDisk saves the given saveRegistrationDisk to
+// the given kv.
+func saveRegistrationDisk(kv *versioned.KV, reg registrationDisk) error {
+	regBytes, err := reg.Marshall()
+	if err != nil {
+		return err
+	}
+	obj := versioned.Object{
+		Version:   registrationDiskVersion,
+		Timestamp: netTime.Now(),
+		Data:      regBytes,
+	}
+	return kv.Set(registrationDiskKey, &obj)
+}
+
+// registrationDisk is used to encapsulate the channel user's key pair,
+// lease and lease signature.
+type registrationDisk struct {
+	rwmutex sync.RWMutex
+
+	Registered bool
+	PublicKey  ed25519.PublicKey
+	PrivateKey ed25519.PrivateKey
+	Lease      int64
+	Signature  []byte
+}
+
+// newRegistrationDisk creates a new newRegistrationDisk.
+func newRegistrationDisk(publicKey ed25519.PublicKey, privateKey ed25519.PrivateKey,
+	lease time.Time, signature []byte) registrationDisk {
+	return registrationDisk{
+		Lease:      lease.UnixNano(),
+		PublicKey:  publicKey,
+		PrivateKey: privateKey,
+		Signature:  signature,
+	}
+}
+
+func (r registrationDisk) IsRegistered() bool {
+	r.rwmutex.RLock()
+	defer r.rwmutex.RUnlock()
+
+	return r.Registered
+}
+
+// Update updates the registrationDisk that is currently
+// stored on the kv with a new lease and lease signature.
+func (r registrationDisk) Update(lease int64, signature []byte) {
+	r.rwmutex.Lock()
+	defer r.rwmutex.Unlock()
+
+	r.Registered = true
+	r.Lease = lease
+	r.Signature = signature
+}
+
+// Marshall marshalls the registrationDisk.
+func (r registrationDisk) Marshall() ([]byte, error) {
+	r.rwmutex.RLock()
+	defer r.rwmutex.RUnlock()
+
+	return json.Marshal(&r)
+}
+
+// UnmarshallRegistrationDisk unmarshalls a registrationDisk
+func UnmarshallRegistrationDisk(data []byte) (registrationDisk, error) {
+	var r registrationDisk
+	err := json.Unmarshal(data, &r)
+	if err != nil {
+		return registrationDisk{}, err
+	}
+	return r, nil
+}
+
+// GetLease returns the current registrationDisk lease.
+func (r registrationDisk) GetLease() time.Time {
+	r.rwmutex.RLock()
+	defer r.rwmutex.RUnlock()
+
+	return time.Unix(0, r.Lease)
+}
+
+// GetPublicKey returns the current public key.
+func (r registrationDisk) GetPublicKey() ed25519.PublicKey {
+	r.rwmutex.RLock()
+	defer r.rwmutex.RUnlock()
+
+	pubkey := make([]byte, ed25519.PublicKeySize)
+	copy(pubkey, r.PublicKey)
+	return pubkey
+}
+
+// GetPrivateKey returns the current private key.
+func (r registrationDisk) getPrivateKey() ed25519.PrivateKey {
+	r.rwmutex.RLock()
+	defer r.rwmutex.RUnlock()
+
+	return r.PrivateKey
+}
+
+// GetLeaseSignature returns the currentl signature and lease time.
+func (r registrationDisk) GetLeaseSignature() ([]byte, time.Time) {
+	r.rwmutex.RLock()
+	defer r.rwmutex.RUnlock()
+
+	return r.Signature, time.Unix(0, r.Lease)
+}
+
+// clientIDTracker encapsulates the client channel lease and the
+// repetitive scheduling of new lease registrations when the
+// current lease expires.
+type clientIDTracker struct {
+	kv *versioned.KV
+
+	username string
+
+	registrationDisk  *registrationDisk
+	receptionIdentity *xxdk.ReceptionIdentity
+
+	rngSource *fastRNG.StreamGenerator
+
+	host     *connect.Host
+	comms    channelLeaseComms
+	udPubKey ed25519.PublicKey
+}
+
+// clientIDTracker implements the NameService interface.
+var _ channels.NameService = (*clientIDTracker)(nil)
+
+// newclientIDTracker creates a new clientIDTracker.
+func newclientIDTracker(comms channelLeaseComms, host *connect.Host, username string, kv *versioned.KV,
+	receptionIdentity xxdk.ReceptionIdentity, udPubKey ed25519.PublicKey, rngSource *fastRNG.StreamGenerator) *clientIDTracker {
+
+	reg, err := loadRegistrationDisk(kv)
+	if !kv.Exists(err) {
+		rng := rngSource.GetStream()
+		defer rng.Close()
+
+		publicKey, privateKey, err := ed25519.GenerateKey(rng)
+		if err != nil {
+			jww.FATAL.Panic(err)
+		}
+
+		reg = registrationDisk{
+			PublicKey:  publicKey,
+			PrivateKey: privateKey,
+			Lease:      0,
+		}
+		err = saveRegistrationDisk(kv, reg)
+		if err != nil {
+			jww.FATAL.Panic(err)
+		}
+	} else if err != nil {
+		jww.FATAL.Panic(err)
+	}
+
+	c := &clientIDTracker{
+		kv:                kv,
+		rngSource:         rngSource,
+		registrationDisk:  &reg,
+		receptionIdentity: &receptionIdentity,
+		username:          username,
+		comms:             comms,
+		host:              host,
+		udPubKey:          udPubKey,
+	}
+
+	if !reg.IsRegistered() {
+		err = c.register()
+		if err != nil {
+			jww.FATAL.Panic(err)
+		}
+	}
+
+	return c
+}
+
+// Start starts the registration worker.
+func (c *clientIDTracker) Start() (stoppable.Stoppable, error) {
+	stopper := stoppable.NewSingle("ud.ClientIDTracker")
+	go c.registrationWorker(stopper)
+	return stopper, nil
+}
+
+func pow(base, exponent int) int {
+	if exponent == 0 {
+		return 1
+	}
+	result := base
+	for i := 2; i <= exponent; i++ {
+		result *= base
+	}
+	return result
+}
+
+// registrationWorker is meant to run in it's own goroutine
+// periodically registering, getting a new lease.
+func (c *clientIDTracker) registrationWorker(stopper *stoppable.Single) {
+	// start backoff at 32 seconds
+	base := 2
+	exponent := 5
+	waitTime := time.Second
+	maxBackoff := 300
+	for {
+		if netTime.Now().After(c.registrationDisk.GetLease().Add(-graceDuration)) {
+			err := c.register()
+			if err != nil {
+				backoffSeconds := pow(base, exponent)
+				if backoffSeconds > maxBackoff {
+					backoffSeconds = maxBackoff
+				} else {
+					exponent += 1
+				}
+				waitTime = time.Second * time.Duration(backoffSeconds)
+			} else {
+				waitTime = time.Second
+			}
+		}
+
+		select {
+		case <-stopper.Quit():
+			return
+		case <-time.After(c.registrationDisk.GetLease().Add(-graceDuration).Sub(netTime.Now())):
+		}
+
+		// Avoid spamming the server in the event that it's service is down.
+		select {
+		case <-stopper.Quit():
+			return
+		case <-time.After(waitTime):
+		}
+	}
+}
+
+// GetUsername returns the username.
+func (c *clientIDTracker) GetUsername() string {
+	return c.username
+}
+
+// GetChannelValidationSignature returns the validation
+// signature and the time it was signed.
+func (c *clientIDTracker) GetChannelValidationSignature() ([]byte, time.Time) {
+	return c.registrationDisk.GetLeaseSignature()
+}
+
+// GetChannelPubkey returns the user's public key.
+func (c *clientIDTracker) GetChannelPubkey() ed25519.PublicKey {
+	return c.registrationDisk.GetPublicKey()
+}
+
+// SignChannelMessage returns the signature of the given
+// message. The ed25519 private key stored in the registrationDisk on the
+// kv is used for signing.
+func (c *clientIDTracker) SignChannelMessage(message []byte) ([]byte, error) {
+	privateKey := c.registrationDisk.getPrivateKey()
+	return ed25519.Sign(privateKey, message), nil
+}
+
+// ValidateoChannelMessage
+func (c *clientIDTracker) ValidateChannelMessage(username string, lease time.Time, pubKey ed25519.PublicKey, authorIDSignature []byte) bool {
+	return channel.VerifyChannelLease(authorIDSignature, pubKey, username, lease, c.udPubKey)
+}
+
+// register causes a request for a new channel lease to be sent to
+// the user discovery server. If successful in procuration of a new lease
+// then it is written to the registrationDisk on the kv.
+func (c *clientIDTracker) register() error {
+	lease, signature, err := c.requestChannelLease()
+	if err != nil {
+		return err
+	}
+
+	c.registrationDisk.Update(lease, signature)
+
+	return nil
+}
+
+// requestChannelLease requests a new channel lease
+// from the user discovery server.
+func (c *clientIDTracker) requestChannelLease() (int64, []byte, error) {
+	ts := netTime.Now().UnixNano()
+	privKey, err := c.receptionIdentity.GetRSAPrivateKey()
+	if err != nil {
+		return 0, nil, err
+	}
+
+	rng := c.rngSource.GetStream()
+	userPubKey := c.registrationDisk.GetPublicKey()
+	fSig, err := channel.SignChannelIdentityRequest(userPubKey, time.Unix(0, ts), privKey, rng)
+	if err != nil {
+		return 0, nil, err
+	}
+	rng.Close()
+
+	msg := &mixmessages.ChannelLeaseRequest{
+		UserID:                 c.receptionIdentity.ID.Marshal(),
+		UserEd25519PubKey:      userPubKey,
+		Timestamp:              ts,
+		UserPubKeyRSASignature: fSig,
+	}
+
+	resp, err := c.comms.SendChannelLeaseRequest(c.host, msg)
+	if err != nil {
+		return 0, nil, err
+	}
+
+	ok := channel.VerifyChannelLease(resp.UDLeaseEd25519Signature,
+		userPubKey, c.username, time.Unix(0, resp.Lease), c.udPubKey)
+	if !ok {
+		return 0, nil, ErrChannelLeaseSignature
+	}
+
+	return resp.Lease, resp.UDLeaseEd25519Signature, err
+}
+
+// StartChannelNameService creates a new clientIDTracker
+// and returns a reference to it's type as the NameService interface.
+// However, it's scheduler thread isn't started until it's Start
+// method is called.
+func (m *Manager) StartChannelNameService() (channels.NameService, error) {
+
+	if m.nameService == nil {
+		udPubKeyBytes := m.user.GetCmix().GetInstance().GetPartialNdf().Get().UDB.DhPubKey
+		username, err := m.store.GetUsername()
+		if err != nil {
+			return nil, err
+		}
+		m.nameService = newclientIDTracker(
+			m.comms,
+			m.ud.host,
+			username,
+			m.getKv(),
+			m.user.GetReceptionIdentity(),
+			udPubKeyBytes,
+			m.getRng())
+
+	}
+
+	return m.nameService, nil
+}
diff --git a/ud/channelIDTracking_test.go b/ud/channelIDTracking_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..52c4c9826949856a363f04c315a2fd5dc7449637
--- /dev/null
+++ b/ud/channelIDTracking_test.go
@@ -0,0 +1,137 @@
+package ud
+
+import (
+	"crypto/ed25519"
+	"crypto/rand"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+
+	"gitlab.com/xx_network/comms/connect"
+	"gitlab.com/xx_network/crypto/csprng"
+	"gitlab.com/xx_network/crypto/signature/rsa"
+	"gitlab.com/xx_network/primitives/id"
+
+	"gitlab.com/elixxir/client/event"
+	"gitlab.com/elixxir/client/storage/versioned"
+	store "gitlab.com/elixxir/client/ud/store"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	"gitlab.com/elixxir/ekv"
+)
+
+func TestSignChannelMessage(t *testing.T) {
+	publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
+	require.NoError(t, err)
+
+	reg := registrationDisk{
+		PublicKey:  publicKey,
+		PrivateKey: privateKey,
+		Lease:      0,
+	}
+	c := &clientIDTracker{
+		registrationDisk: &reg,
+	}
+
+	message := []byte("hello world")
+	sig, err := c.SignChannelMessage(message)
+	require.NoError(t, err)
+
+	require.True(t, ed25519.Verify(publicKey, message, sig))
+}
+
+func TestNewRegistrationDisk(t *testing.T) {
+	publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
+	require.NoError(t, err)
+	lease := time.Now().UnixNano()
+
+	signature := make([]byte, 64)
+	reg := newRegistrationDisk(publicKey, privateKey, time.Unix(0, lease), signature)
+	require.Equal(t, reg.PublicKey, publicKey)
+	require.Equal(t, reg.PrivateKey, privateKey)
+	require.Equal(t, reg.Signature, signature)
+	require.Equal(t, reg.Lease, lease)
+}
+
+func TestLoadSaveRegistration(t *testing.T) {
+	publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
+	require.NoError(t, err)
+	lease := time.Now()
+	signature := make([]byte, 64)
+	reg := newRegistrationDisk(publicKey, privateKey, lease, signature)
+
+	kv := versioned.NewKV(ekv.MakeMemstore())
+
+	registrationDisk, err := loadRegistrationDisk(kv)
+	require.Error(t, err)
+	require.False(t, kv.Exists(err))
+
+	err = saveRegistrationDisk(kv, reg)
+	require.NoError(t, err)
+
+	registrationDisk, err = loadRegistrationDisk(kv)
+	require.NoError(t, err)
+	require.Equal(t, registrationDisk, reg)
+}
+
+func TestChannelIDTracking(t *testing.T) {
+	rngGen := fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG)
+
+	// comms AddHost
+	stream := rngGen.GetStream()
+	privKey, err := rsa.GenerateKey(stream, 1024)
+	require.NoError(t, err)
+
+	tnm := newTestNetworkManager(t)
+	managerkv := versioned.NewKV(ekv.MakeMemstore())
+	udStore, err := store.NewOrLoadStore(managerkv)
+	m := &Manager{
+		user: mockE2e{
+			grp:     getGroup(),
+			events:  event.NewEventManager(),
+			rng:     rngGen,
+			kv:      managerkv,
+			network: tnm,
+			t:       t,
+			key:     privKey,
+		},
+		store: udStore,
+		comms: &mockComms{},
+	}
+
+	netDef := m.getCmix().GetInstance().GetPartialNdf().Get()
+	udID, err := id.Unmarshal(netDef.UDB.ID)
+	require.NoError(t, err)
+
+	params := connect.GetDefaultHostParams()
+	params.AuthEnabled = false
+	params.SendTimeout = 20 * time.Second
+
+	host, err := m.comms.AddHost(udID, netDef.UDB.Address,
+		[]byte(netDef.UDB.Cert), params)
+	require.NoError(t, err)
+
+	// register
+
+	kv := versioned.NewKV(ekv.MakeMemstore())
+	comms := new(mockComms)
+	username := "Alice"
+
+	udPubKey, udPrivKey, err := ed25519.GenerateKey(rand.Reader)
+	require.NoError(t, err)
+
+	rsaPrivKey, err := m.user.GetReceptionIdentity().GetRSAPrivateKey()
+	require.NoError(t, err)
+
+	comms.SetUserRSAPubKey(rsaPrivKey.GetPublic())
+	comms.SetUDEd25519PrivateKey(&udPrivKey)
+	comms.SetUsername(username)
+
+	myTestClientIDTracker := newclientIDTracker(
+		comms, host, username,
+		kv, m.user.GetReceptionIdentity(),
+		udPubKey, rngGen)
+
+	err = myTestClientIDTracker.register()
+	require.NoError(t, err)
+}
diff --git a/ud/comms.go b/ud/comms.go
index 160f7529aa9296235c63d7a32ad0de226be90112..df993f608cc1ccc08a5f3e670a8322e4d97ff819 100644
--- a/ud/comms.go
+++ b/ud/comms.go
@@ -44,6 +44,8 @@ type Comms interface {
 	// object. This will be used to send to the UD service on the above
 	// gRPC send functions.
 	GetHost(hostId *id.ID) (*connect.Host, bool)
+
+	channelLeaseComms
 }
 
 // removeFactComms is a sub-interface of the Comms interface for the
@@ -75,3 +77,7 @@ type registerUserComms interface {
 type addFactComms interface {
 	SendRegisterFact(host *connect.Host, message *pb.FactRegisterRequest) (*pb.FactRegisterResponse, error)
 }
+
+type channelLeaseComms interface {
+	SendChannelLeaseRequest(host *connect.Host, message *pb.ChannelLeaseRequest) (*pb.ChannelLeaseResponse, error)
+}
diff --git a/ud/lookup_test.go b/ud/lookup_test.go
index 9542cfc4015981be3c342d50444e3c5eb234a7d9..4b4d61d65e4154cb94b831fe5c4bee44d1a01d11 100644
--- a/ud/lookup_test.go
+++ b/ud/lookup_test.go
@@ -50,7 +50,7 @@ func TestManager_Lookup(t *testing.T) {
 		DhPubKey: publicKey,
 	}
 
-	contacts := []*Contact{&Contact{
+	contacts := []*Contact{{
 		UserID: expectedContact.ID.Bytes(),
 		PubKey: expectedContact.DhPubKey.Bytes(),
 	}}
diff --git a/ud/manager.go b/ud/manager.go
index 75604bd3ddee8848a23179c645742ea735ff3f16..9ea87cf84bef02982f8830ba8afa68b8149938dc 100644
--- a/ud/manager.go
+++ b/ud/manager.go
@@ -44,6 +44,10 @@ type Manager struct {
 	// ud is the tracker for the contact information of the specified UD server.
 	// This information is specified in Manager's constructors (NewOrLoad and NewManagerFromBackup).
 	ud *userDiscovery
+
+	// nameService adheres to the channels.NameService interface. This is
+	// implemented using the clientIDTracker.
+	nameService *clientIDTracker
 }
 
 // NewOrLoad loads an existing Manager from storage or creates a
diff --git a/ud/mockComms_test.go b/ud/mockComms_test.go
index 1a50717f55bf749b4681009ceba7325c3b749a45..b77efc11a502e72036b343708e993ede522f46ba 100644
--- a/ud/mockComms_test.go
+++ b/ud/mockComms_test.go
@@ -8,14 +8,24 @@
 package ud
 
 import (
-	pb "gitlab.com/elixxir/comms/mixmessages"
+	"crypto/ed25519"
+	"time"
+
 	"gitlab.com/xx_network/comms/connect"
 	"gitlab.com/xx_network/comms/messages"
+	"gitlab.com/xx_network/crypto/signature/rsa"
 	"gitlab.com/xx_network/primitives/id"
+
+	pb "gitlab.com/elixxir/comms/mixmessages"
+	"gitlab.com/elixxir/crypto/channel"
 )
 
 type mockComms struct {
-	udHost *connect.Host
+	udHost            *connect.Host
+	userRsaPub        *rsa.PublicKey
+	userEd25519PubKey []byte
+	udPrivKey         *ed25519.PrivateKey
+	username          string
 }
 
 func (m mockComms) SendRegisterUser(host *connect.Host, message *pb.UDBUserRegistration) (*messages.Ack, error) {
@@ -51,3 +61,44 @@ func (m *mockComms) AddHost(hid *id.ID, address string, cert []byte, params conn
 func (m mockComms) GetHost(hostId *id.ID) (*connect.Host, bool) {
 	return m.udHost, true
 }
+
+func (m *mockComms) SetUDEd25519PrivateKey(key *ed25519.PrivateKey) {
+	m.udPrivKey = key
+}
+
+func (m *mockComms) SetUserRSAPubKey(userRsaPub *rsa.PublicKey) {
+	m.userRsaPub = userRsaPub
+}
+
+func (m *mockComms) SetUsername(u string) {
+	m.username = u
+}
+
+func (m mockComms) SendChannelLeaseRequest(host *connect.Host, message *pb.ChannelLeaseRequest) (*pb.ChannelLeaseResponse, error) {
+
+	err := channel.VerifyChannelIdentityRequest(message.UserPubKeyRSASignature,
+		message.UserEd25519PubKey,
+		time.Now(),
+		time.Unix(0, message.Timestamp),
+		m.userRsaPub)
+	if err != nil {
+		panic(err)
+	}
+
+	d, _ := time.ParseDuration("4h30m")
+	lease := time.Now().Add(d).UnixNano()
+	signature := channel.SignChannelLease(message.UserEd25519PubKey, m.username,
+		time.Unix(0, lease), *m.udPrivKey)
+
+	if err != nil {
+		panic(err)
+	}
+
+	response := &pb.ChannelLeaseResponse{
+		Lease:                   lease,
+		UserEd25519PubKey:       m.userEd25519PubKey,
+		UDLeaseEd25519Signature: signature,
+	}
+
+	return response, nil
+}
diff --git a/ud/networkManager_test.go b/ud/networkManager_test.go
index b10cd8f2361568dcc6b7a6b3ea7c3b780a911982..bd3630ebf9016e37535dbba6ea332c5676da4a1a 100644
--- a/ud/networkManager_test.go
+++ b/ud/networkManager_test.go
@@ -34,9 +34,44 @@ type testNetworkManager struct {
 	responseProcessor message.Processor
 }
 
+func (tnm *testNetworkManager) SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler,
+	cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
+
+	msg := format.NewMessage(tnm.instance.GetE2EGroup().GetP().ByteLen())
+
+	var rid id.Round = 123
+	ephemeralId := new(ephemeral.Id)
+
+	fingerprint, service, payload, mac, err := assembler(rid)
+	if err != nil {
+		return rounds.Round{ID: rid}, *ephemeralId, err
+	}
+
+	// Build message. Will panic if inputs are not correct.
+	msg.SetKeyFP(fingerprint)
+	msg.SetContents(payload)
+	msg.SetMac(mac)
+	msg.SetSIH(service.Hash(msg.GetContents()))
+	// If the recipient for a call to Send is UD, then this
+	// is the request pathway. Call the UD processor to simulate
+	// the UD picking up the request
+	if bytes.Equal(tnm.instance.GetFullNdf().
+		Get().UDB.ID,
+		recipient.Bytes()) {
+		tnm.responseProcessor.Process(msg, receptionID.EphemeralIdentity{}, rounds.Round{})
+
+	} else {
+		// This should happen when the mock UD service Sends back a response.
+		// Calling process mocks up the requester picking up the response.
+		tnm.requestProcess.Process(msg, receptionID.EphemeralIdentity{}, rounds.Round{})
+	}
+
+	return rounds.Round{}, ephemeral.Id{}, nil
+}
+
 func (tnm *testNetworkManager) Send(recipient *id.ID, fingerprint format.Fingerprint,
 	service message.Service,
-	payload, mac []byte, cmixParams cmix.CMIXParams) (id.Round, ephemeral.Id, error) {
+	payload, mac []byte, cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
 	msg := format.NewMessage(tnm.instance.GetE2EGroup().GetP().ByteLen())
 	// Build message. Will panic if inputs are not correct.
 	msg.SetKeyFP(fingerprint)
@@ -57,7 +92,7 @@ func (tnm *testNetworkManager) Send(recipient *id.ID, fingerprint format.Fingerp
 		tnm.requestProcess.Process(msg, receptionID.EphemeralIdentity{}, rounds.Round{})
 	}
 
-	return 0, ephemeral.Id{}, nil
+	return rounds.Round{}, ephemeral.Id{}, nil
 }
 
 func (tnm *testNetworkManager) AddFingerprint(identity *id.ID,
@@ -136,7 +171,7 @@ func (tnm *testNetworkManager) SendToAny(sendFunc func(host *connect.Host) (inte
 	panic("implement me")
 }
 
-func (tnm *testNetworkManager) SendMany(messages []cmix.TargetedCmixMessage, p cmix.CMIXParams) (id.Round, []ephemeral.Id, error) {
+func (tnm *testNetworkManager) SendMany(messages []cmix.TargetedCmixMessage, p cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) {
 	//TODO implement me
 	panic("implement me")
 }
@@ -196,7 +231,7 @@ func (tnm *testNetworkManager) TriggerNodeRegistration(nid *id.ID) {
 	panic("implement me")
 }
 
-func (tnm *testNetworkManager) GetRoundResults(timeout time.Duration, roundCallback cmix.RoundEventCallback, roundList ...id.Round) error {
+func (tnm *testNetworkManager) GetRoundResults(timeout time.Duration, roundCallback cmix.RoundEventCallback, roundList ...id.Round) {
 	//TODO implement me
 	panic("implement me")
 }
diff --git a/xxdk/ndf.go b/xxdk/ndf.go
index de1056622c25efd98bb252c5eee46f15efaaece0..1872276f9c0b9ff8a0eee745b05db2018e27fad2 100644
--- a/xxdk/ndf.go
+++ b/xxdk/ndf.go
@@ -9,6 +9,7 @@ package xxdk
 
 import (
 	"encoding/base64"
+	"github.com/golang/protobuf/proto"
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/comms/client"
@@ -18,7 +19,6 @@ import (
 	"gitlab.com/xx_network/crypto/tls"
 	"gitlab.com/xx_network/primitives/id"
 	"gitlab.com/xx_network/primitives/id/ephemeral"
-	"google.golang.org/protobuf/proto"
 	"io/ioutil"
 	"net/http"
 )
@@ -59,7 +59,7 @@ func DownloadNdfFromGateway(address string, cert []byte) (
 	}
 
 	// Send poll request and receive response containing NDF
-	resp, err := comms.SendPoll(host, pollMsg)
+	resp, _, _, err := comms.SendPoll(host, pollMsg)
 	if err != nil {
 		return nil, err
 	}
diff --git a/xxdk/utilsInterfaces_test.go b/xxdk/utilsInterfaces_test.go
index b1b93977c64fbf1fcdd8ce1635ceeb15bc3b0655..4fbf3852387c7c095934c2fbf65f388cfa046e2f 100644
--- a/xxdk/utilsInterfaces_test.go
+++ b/xxdk/utilsInterfaces_test.go
@@ -109,13 +109,20 @@ func (t *testNetworkManagerGeneric) AddFingerprint(identity *id.ID, fingerprint
 }
 
 func (t *testNetworkManagerGeneric) Send(*id.ID, format.Fingerprint,
-	message.Service, []byte, []byte, cmix.CMIXParams) (id.Round,
+	message.Service, []byte, []byte, cmix.CMIXParams) (rounds.Round,
 	ephemeral.Id, error) {
-	return id.Round(0), ephemeral.Id{}, nil
+	return rounds.Round{}, ephemeral.Id{}, nil
 }
+
+func (t *testNetworkManagerGeneric) SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler,
+	cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) {
+
+	return rounds.Round{}, ephemeral.Id{}, nil
+}
+
 func (t *testNetworkManagerGeneric) SendMany(messages []cmix.TargetedCmixMessage,
-	p cmix.CMIXParams) (id.Round, []ephemeral.Id, error) {
-	return 0, []ephemeral.Id{}, nil
+	p cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) {
+	return rounds.Round{}, []ephemeral.Id{}, nil
 }
 func (t *testNetworkManagerGeneric) GetInstance() *network.Instance {
 	return t.instance
@@ -176,8 +183,7 @@ func (t *testNetworkManagerGeneric) GetIdentity(get *id.ID) (
 	return identity.TrackedID{}, nil
 }
 func (t *testNetworkManagerGeneric) GetRoundResults(timeout time.Duration,
-	roundCallback cmix.RoundEventCallback, roundList ...id.Round) error {
-	return nil
+	roundCallback cmix.RoundEventCallback, roundList ...id.Round) {
 }
 func (t *testNetworkManagerGeneric) HasNode(nid *id.ID) bool { return false }
 func (t *testNetworkManagerGeneric) IsHealthy() bool         { return true }
diff --git a/xxdk/version_vars.go b/xxdk/version_vars.go
index 61bd87da81f93ce196ac84a628a11107357ecb24..76a41f587dcf4dfd5fa2e3ba8b7ebea7510c4d58 100644
--- a/xxdk/version_vars.go
+++ b/xxdk/version_vars.go
@@ -1,11 +1,11 @@
 // Code generated by go generate; DO NOT EDIT.
 // This file was generated by robots at
-// 2022-09-15 10:52:12.1953796 -0700 PDT m=+0.451573601
+// 2022-10-17 12:52:49.822998 -0500 CDT m=+0.037589931
 
 package xxdk
 
-const GITVERSION = `55b66f6b Update proto files`
-const SEMVER = "4.2.0"
+const GITVERSION = `a401d138 update deps`
+const SEMVER = "4.3.0"
 const DEPENDENCIES = `module gitlab.com/elixxir/client
 
 go 1.17
@@ -20,35 +20,47 @@ require (
 	github.com/spf13/cobra v1.5.0
 	github.com/spf13/jwalterweatherman v1.1.0
 	github.com/spf13/viper v1.12.0
+	github.com/stretchr/testify v1.8.0
 	gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f
-	gitlab.com/elixxir/comms v0.0.4-0.20220914220142-601071b77d78
-	gitlab.com/elixxir/crypto v0.0.7-0.20220901215826-1ceaeb59081f
+	gitlab.com/elixxir/comms v0.0.4-0.20221017173926-4eaa6061dfaa
+	gitlab.com/elixxir/crypto v0.0.7-0.20221017173452-565da4101a3b
 	gitlab.com/elixxir/ekv v0.2.1
-	gitlab.com/elixxir/primitives v0.0.3-0.20220901220638-1acc75fabdc6
-	gitlab.com/xx_network/comms v0.0.4-0.20220914220351-2e461edbfe48
-	gitlab.com/xx_network/crypto v0.0.5-0.20220902182733-69aad094b487
-	gitlab.com/xx_network/primitives v0.0.4-0.20220902183448-319596e2fec8
+	gitlab.com/elixxir/primitives v0.0.3-0.20221017172918-6176818d1aba
+	gitlab.com/xx_network/comms v0.0.4-0.20221017172508-09e33697dc15
+	gitlab.com/xx_network/crypto v0.0.5-0.20221017172404-b384a8d8b171
+	gitlab.com/xx_network/primitives v0.0.4-0.20221017171439-42169a3e5c0d
 	go.uber.org/ratelimit v0.2.0
 	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
-	golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b
-	google.golang.org/grpc v1.48.0
+	golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c
+	google.golang.org/grpc v1.49.0
 	google.golang.org/protobuf v1.28.1
 )
 
 require (
+	git.xx.network/elixxir/grpc-web-go-client v0.0.0-20220908170150-ef04339ffe65 // indirect
 	github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
 	github.com/badoux/checkmail v1.2.1 // indirect
+	github.com/cenkalti/backoff/v4 v4.1.3 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
 	github.com/elliotchance/orderedmap v1.4.0 // indirect
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
+	github.com/gorilla/websocket v1.5.0 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
+	github.com/improbable-eng/grpc-web v0.15.0 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
+	github.com/klauspost/compress v1.11.7 // indirect
 	github.com/klauspost/cpuid/v2 v2.1.0 // indirect
 	github.com/magiconair/properties v1.8.6 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/pelletier/go-toml v1.9.5 // indirect
 	github.com/pelletier/go-toml/v2 v2.0.2 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/rs/cors v1.8.2 // indirect
+	github.com/sethvargo/go-diceware v0.3.0 // indirect
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
+	github.com/soheilhy/cmux v0.1.5 // indirect
 	github.com/spf13/afero v1.9.2 // indirect
 	github.com/spf13/cast v1.5.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
@@ -58,11 +70,13 @@ require (
 	github.com/tyler-smith/go-bip39 v1.1.0 // indirect
 	github.com/zeebo/blake3 v0.2.3 // indirect
 	gitlab.com/xx_network/ring v0.0.3-0.20220222211904-da613960ad93 // indirect
+	go.uber.org/atomic v1.10.0 // indirect
 	golang.org/x/sys v0.0.0-20220731174439-a90be440212d // indirect
 	golang.org/x/text v0.3.7 // indirect
-	google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78 // indirect
+	google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect
 	gopkg.in/ini.v1 v1.66.6 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
+	nhooyr.io/websocket v1.8.7 // indirect
 )
 `