From b19e510f30f52b24f16a3fdf7f34d8a2450f9d62 Mon Sep 17 00:00:00 2001
From: josh <josh@elixxir.io>
Date: Wed, 13 Apr 2022 16:05:56 -0700
Subject: [PATCH] WIP: Restructure user discovery

---
 bindings/ud.go                         |   2 +-
 cmd/ud.go                              |   2 +-
 ud/addFact.go                          |  39 ++--
 ud/comms.go                            |  50 +++++
 ud/confirmFact.go                      |  27 +--
 ud/lookup.go                           |  61 +++--
 ud/manager.go                          | 283 +++++++++++++++--------
 ud/register.go                         |  70 ++----
 ud/registered.go                       |  31 +--
 ud/remove.go                           |  88 ++++----
 ud/search.go                           |  34 ++-
 {storage/ud => ud/store}/facts.go      |   0
 {storage/ud => ud/store}/facts_test.go |   0
 {storage/ud => ud/store}/store.go      |   0
 {storage/ud => ud/store}/store_test.go |   0
 ud/store/ud/facts.go                   | 190 ++++++++++++++++
 ud/store/ud/facts_test.go              | 296 +++++++++++++++++++++++++
 ud/store/ud/store.go                   | 271 ++++++++++++++++++++++
 ud/store/ud/store_test.go              | 129 +++++++++++
 19 files changed, 1277 insertions(+), 296 deletions(-)
 create mode 100644 ud/comms.go
 rename {storage/ud => ud/store}/facts.go (100%)
 rename {storage/ud => ud/store}/facts_test.go (100%)
 rename {storage/ud => ud/store}/store.go (100%)
 rename {storage/ud => ud/store}/store_test.go (100%)
 create mode 100644 ud/store/ud/facts.go
 create mode 100644 ud/store/ud/facts_test.go
 create mode 100644 ud/store/ud/store.go
 create mode 100644 ud/store/ud/store_test.go

diff --git a/bindings/ud.go b/bindings/ud.go
index df34f572d..ac89b1e08 100644
--- a/bindings/ud.go
+++ b/bindings/ud.go
@@ -76,7 +76,7 @@ func (ud *UserDiscovery) AddFact(fStr string) (string, error) {
 // ConfirmFact confirms a fact first registered via AddFact. The confirmation ID comes from
 // AddFact while the code will come over the associated communications system
 func (ud *UserDiscovery) ConfirmFact(confirmationID, code string) error {
-	return ud.ud.SendConfirmFact(confirmationID, code)
+	return ud.ud.ConfirmFact(confirmationID, code)
 }
 
 // RemoveFact removes a previously confirmed fact.  Will fail if the passed fact string is
diff --git a/cmd/ud.go b/cmd/ud.go
index 10271f53b..eb95afab0 100644
--- a/cmd/ud.go
+++ b/cmd/ud.go
@@ -130,7 +130,7 @@ var udCmd = &cobra.Command{
 
 		confirmID := viper.GetString("confirm")
 		if confirmID != "" {
-			err = userDiscoveryMgr.SendConfirmFact(confirmID, confirmID)
+			err = userDiscoveryMgr.ConfirmFact(confirmID, confirmID)
 			if err != nil {
 				fmt.Printf("Couldn't confirm fact: %s\n",
 					err.Error())
diff --git a/ud/addFact.go b/ud/addFact.go
index 1985970ff..d67fe0961 100644
--- a/ud/addFact.go
+++ b/ud/addFact.go
@@ -8,15 +8,10 @@ import (
 	"gitlab.com/elixxir/crypto/factID"
 	"gitlab.com/elixxir/crypto/hash"
 	"gitlab.com/elixxir/primitives/fact"
-	"gitlab.com/xx_network/comms/connect"
 	"gitlab.com/xx_network/crypto/signature/rsa"
 	"gitlab.com/xx_network/primitives/id"
 )
 
-type addFactComms interface {
-	SendRegisterFact(host *connect.Host, message *pb.FactRegisterRequest) (*pb.FactRegisterResponse, error)
-}
-
 // SendRegisterFact adds a fact for the user to user discovery. Will only
 // succeed if the user is already registered and the system does not have the
 // fact currently registered for any user.
@@ -24,16 +19,17 @@ type addFactComms interface {
 // confirmation id instead. Over the communications system the fact is
 // associated with, a code will be sent. This confirmation ID needs to be
 // called along with the code to finalize the fact.
-func (m *Manager) SendRegisterFact(fact fact.Fact) (string, error) {
-	jww.INFO.Printf("ud.SendRegisterFact(%s)", fact.Stringify())
-	return m.addFact(fact, m.myID, m.comms)
+func (m *Manager) SendRegisterFact(f fact.Fact) (string, error) {
+	jww.INFO.Printf("ud.SendRegisterFact(%s)", f.Stringify())
+	return m.addFact(f, m.myID, m.comms)
 }
 
-func (m *Manager) addFact(inFact fact.Fact, uid *id.ID, aFC addFactComms) (string, error) {
+func (m *Manager) addFact(inFact fact.Fact, myId *id.ID, aFC addFactComms) (string, error) {
 
-	if !m.IsRegistered() {
-		return "", errors.New("Failed to add fact: " +
-			"client is not registered")
+	// get UD host
+	udHost, err := m.getOrAddUdHost()
+	if err != nil {
+		return "", err
 	}
 
 	// Create a primitives Fact so we can hash it
@@ -53,31 +49,26 @@ func (m *Manager) addFact(inFact fact.Fact, uid *id.ID, aFC addFactComms) (strin
 
 	// Create our Fact Removal Request message data
 	remFactMsg := pb.FactRegisterRequest{
-		UID: uid.Marshal(),
+		UID: myId.Marshal(),
 		Fact: &pb.Fact{
-			Fact:     inFact.Fact,
-			FactType: uint32(inFact.T),
+			Fact:     f.Fact,
+			FactType: uint32(f.T),
 		},
 		FactSig: fSig,
 	}
 
-	// get UD host
-	host, err := m.getHost()
-	if err != nil {
-		return "", err
-	}
-
 	// Send the message
-	response, err := aFC.SendRegisterFact(host, &remFactMsg)
+	response, err := aFC.SendRegisterFact(udHost, &remFactMsg)
 
 	confirmationID := ""
 	if response != nil {
 		confirmationID = response.ConfirmationID
 	}
 
-	err = m.storage.GetUd().StoreUnconfirmedFact(confirmationID, f)
+	err = m.store.StoreUnconfirmedFact(confirmationID, f)
 	if err != nil {
-		return "", errors.WithMessagef(err, "Failed to store unconfirmed fact %v", f.Fact)
+		return "", errors.WithMessagef(err,
+			"Failed to store unconfirmed fact %v", f.Fact)
 	}
 	// Return the error
 	return confirmationID, err
diff --git a/ud/comms.go b/ud/comms.go
new file mode 100644
index 000000000..4d4ffd07a
--- /dev/null
+++ b/ud/comms.go
@@ -0,0 +1,50 @@
+package ud
+
+import (
+	pb "gitlab.com/elixxir/comms/mixmessages"
+	"gitlab.com/xx_network/comms/connect"
+	"gitlab.com/xx_network/comms/messages"
+	"gitlab.com/xx_network/primitives/id"
+)
+
+// Comms is a sub-interface of the client.Comms interface. This contains
+// RPCs relevant to
+// todo: docsting on what it is, why it's needed. This is half finished as is
+type Comms interface {
+	// todo: docsting on what it is, why it's needed
+	SendRegisterUser(host *connect.Host, message *pb.UDBUserRegistration) (*messages.Ack, error)
+	// todo: docsting on what it is, why it's needed
+	SendRegisterFact(host *connect.Host, message *pb.FactRegisterRequest) (*pb.FactRegisterResponse, error)
+	// todo: docsting on what it is, why it's needed
+	SendConfirmFact(host *connect.Host, message *pb.FactConfirmRequest) (*messages.Ack, error)
+	// todo: docsting on what it is, why it's needed
+	SendRemoveFact(host *connect.Host, message *pb.FactRemovalRequest) (*messages.Ack, error)
+	// todo: docsting on what it is, why it's needed
+	SendRemoveUser(host *connect.Host, message *pb.FactRemovalRequest) (*messages.Ack, error)
+	// todo: docsting on what it is, why it's needed
+	AddHost(hid *id.ID, address string,
+		cert []byte, params connect.HostParams) (host *connect.Host, err error)
+	// todo: docsting on what it is, why it's needed
+	GetHost(hostId *id.ID) (*connect.Host, bool)
+}
+
+// todo: docsting on what it is, why it's needed
+type removeFactComms interface {
+	SendRemoveFact(host *connect.Host, message *pb.FactRemovalRequest) (*messages.Ack, error)
+}
+
+type removeUserComms interface {
+	SendRemoveUser(host *connect.Host, message *pb.FactRemovalRequest) (*messages.Ack, error)
+}
+
+type confirmFactComm interface {
+	SendConfirmFact(host *connect.Host, message *pb.FactConfirmRequest) (*messages.Ack, error)
+}
+
+type registerUserComms interface {
+	SendRegisterUser(*connect.Host, *pb.UDBUserRegistration) (*messages.Ack, error)
+}
+
+type addFactComms interface {
+	SendRegisterFact(host *connect.Host, message *pb.FactRegisterRequest) (*pb.FactRegisterResponse, error)
+}
diff --git a/ud/confirmFact.go b/ud/confirmFact.go
index 7432911b2..77d1f9abe 100644
--- a/ud/confirmFact.go
+++ b/ud/confirmFact.go
@@ -4,19 +4,13 @@ import (
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
 	pb "gitlab.com/elixxir/comms/mixmessages"
-	"gitlab.com/xx_network/comms/connect"
-	"gitlab.com/xx_network/comms/messages"
 )
 
-type confirmFactComm interface {
-	SendConfirmFact(host *connect.Host, message *pb.FactConfirmRequest) (*messages.Ack, error)
-}
-
-// SendConfirmFact confirms a fact first registered via AddFact. The
+// ConfirmFact confirms a fact first registered via AddFact. The
 // confirmation ID comes from AddFact while the code will come over the
 // associated communications system.
-func (m *Manager) SendConfirmFact(confirmationID, code string) error {
-	jww.INFO.Printf("ud.SendConfirmFact(%s, %s)", confirmationID, code)
+func (m *Manager) ConfirmFact(confirmationID, code string) error {
+	jww.INFO.Printf("ud.ConfirmFact(%s, %s)", confirmationID, code)
 	if err := m.confirmFact(confirmationID, code, m.comms); err != nil {
 		return errors.WithMessage(err, "Failed to confirm fact")
 	}
@@ -24,13 +18,8 @@ func (m *Manager) SendConfirmFact(confirmationID, code string) error {
 }
 
 func (m *Manager) confirmFact(confirmationID, code string, comm confirmFactComm) error {
-	if !m.IsRegistered() {
-		return errors.New("Failed to confirm fact: " +
-			"client is not registered")
-	}
-
 	// get UD host
-	host, err := m.getHost()
+	udHost, err := m.getOrAddUdHost()
 	if err != nil {
 		return err
 	}
@@ -39,14 +28,16 @@ func (m *Manager) confirmFact(confirmationID, code string, comm confirmFactComm)
 		ConfirmationID: confirmationID,
 		Code:           code,
 	}
-	_, err = comm.SendConfirmFact(host, msg)
+	_, err = comm.SendConfirmFact(udHost, msg)
 	if err != nil {
 		return err
 	}
 
-	err = m.storage.GetUd().ConfirmFact(confirmationID)
+	err = m.store.ConfirmFact(confirmationID)
 	if err != nil {
-		return errors.WithMessagef(err, "Failed to confirm fact in storage with confirmation ID: %q", confirmationID)
+		return errors.WithMessagef(err,
+			"Failed to confirm fact in storage with confirmation ID: %q",
+			confirmationID)
 	}
 
 	return nil
diff --git a/ud/lookup.go b/ud/lookup.go
index 2ba652234..5ff0899f9 100644
--- a/ud/lookup.go
+++ b/ud/lookup.go
@@ -4,7 +4,13 @@ import (
 	"github.com/golang/protobuf/proto"
 	"github.com/pkg/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/single"
 	"gitlab.com/elixxir/crypto/contact"
+	"gitlab.com/elixxir/crypto/cyclic"
+	"gitlab.com/elixxir/crypto/fastRNG"
 	"gitlab.com/elixxir/primitives/fact"
 	"gitlab.com/xx_network/primitives/id"
 	"time"
@@ -17,24 +23,32 @@ const LookupTag = "xxNetwork_UdLookup"
 // TODO: reconsider where this comes from
 const maxLookupMessages = 20
 
-type lookupCallback func(contact.Contact, error)
-
 // Lookup returns the public key of the passed ID as known by the user discovery
 // system or returns by the timeout.
-func (m *Manager) Lookup(uid *id.ID, callback lookupCallback, timeout time.Duration) error {
+func Lookup(udContact contact.Contact,
+	services cmix.Client,
+	callback single.Response,
+	rng *fastRNG.StreamGenerator,
+	uid *id.ID, grp *cyclic.Group,
+	timeout time.Duration) error {
+
 	jww.INFO.Printf("ud.Lookup(%s, %s)", uid, timeout)
-	return m.lookup(uid, callback, timeout)
+	return lookup(services, callback, rng, uid, grp, timeout, udContact)
 }
 
 // BatchLookup performs a Lookup operation on a list of user IDs.
 // The lookup performs a callback on each lookup on the returned contact object
 // constructed from the response.
-func (m *Manager) BatchLookup(uids []*id.ID, callback lookupCallback, timeout time.Duration) {
+func BatchLookup(udContact contact.Contact,
+	services cmix.Client, callback single.Response,
+	rng *fastRNG.StreamGenerator,
+	uids []*id.ID, grp *cyclic.Group,
+	timeout time.Duration) {
 	jww.INFO.Printf("ud.BatchLookup(%s, %s)", uids, timeout)
 
 	for _, uid := range uids {
 		go func(localUid *id.ID) {
-			err := m.lookup(localUid, callback, timeout)
+			err := lookup(services, callback, rng, localUid, grp, timeout, udContact)
 			if err != nil {
 				jww.WARN.Printf("Failed batch lookup on user %s: %v", localUid, err)
 			}
@@ -47,26 +61,37 @@ func (m *Manager) BatchLookup(uids []*id.ID, callback lookupCallback, timeout ti
 // lookup is a helper function which sends a lookup request to the user discovery
 // service. It will construct a contact object off of the returned public key.
 // The callback will be called on that contact object.
-func (m *Manager) lookup(uid *id.ID, callback lookupCallback, timeout time.Duration) error {
+func lookup(services cmix.Client, callback single.Response,
+	rng *fastRNG.StreamGenerator,
+	uid *id.ID, grp *cyclic.Group,
+	timeout time.Duration, udContact contact.Contact) error {
 	// Build the request and marshal it
 	request := &LookupSend{UserID: uid.Marshal()}
 	requestMarshaled, err := proto.Marshal(request)
 	if err != nil {
-		return errors.WithMessage(err, "Failed to form outgoing lookup request.")
+		return errors.WithMessage(err,
+			"Failed to form outgoing lookup request.")
 	}
 
-	f := func(payload []byte, err error) {
-		m.lookupResponseProcess(uid, callback, payload, err)
+	// todo: figure out callback structure, maybe you do not pass
+	//  in a single.Response but a manager callback?
+	f := func(payload []byte, receptionID receptionID.EphemeralIdentity,
+		round rounds.Round, err error) {
+		m.lookupResponseProcess(payload, receptionID, round, err)
 	}
 
-	// get UD contact
-	c, err := m.getContact()
-	if err != nil {
-		return err
+	p := single.RequestParams{
+		Timeout:     timeout,
+		MaxMessages: maxLookupMessages,
+		CmixParam:   cmix.GetDefaultCMIXParams(),
 	}
 
-	err = m.single.TransmitSingleUse(c, requestMarshaled, LookupTag,
-		maxLookupMessages, f, timeout)
+	stream := rng.GetStream()
+	defer stream.Close()
+
+	rndId, ephId, err := single.TransmitRequest(udContact, LookupTag, requestMarshaled,
+		callback, p, services, stream,
+		grp)
 	if err != nil {
 		return errors.WithMessage(err, "Failed to transmit lookup request.")
 	}
@@ -77,10 +102,10 @@ func (m *Manager) lookup(uid *id.ID, callback lookupCallback, timeout time.Durat
 // lookupResponseProcess processes the lookup response. The returned public key
 // and the user ID will be constructed into a contact object. The contact object
 // will be passed into the callback.
-func (m *Manager) lookupResponseProcess(uid *id.ID, callback lookupCallback,
+func (m *Manager) lookupResponseProcess(uid *id.ID, cb single.Response,
 	payload []byte, err error) {
 	if err != nil {
-		go callback(contact.Contact{}, errors.WithMessage(err, "Failed to lookup."))
+		go cb.Callback(contact.Contact{}, errors.WithMessage(err, "Failed to lookup."))
 		return
 	}
 
diff --git a/ud/manager.go b/ud/manager.go
index 6bd6b7b1a..9aa0027d2 100644
--- a/ud/manager.go
+++ b/ud/manager.go
@@ -1,53 +1,81 @@
 package ud
 
 import (
+	"fmt"
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/api"
-	"gitlab.com/elixxir/client/interfaces"
+	"gitlab.com/elixxir/client/cmix"
+	"gitlab.com/elixxir/client/cmix/identity/receptionID"
+	"gitlab.com/elixxir/client/e2e"
+	"gitlab.com/elixxir/client/event"
+	"gitlab.com/elixxir/client/interfaces/user"
 	"gitlab.com/elixxir/client/single"
-	"gitlab.com/elixxir/client/single/old"
 	"gitlab.com/elixxir/client/stoppable"
-	"gitlab.com/elixxir/client/storage"
+	"gitlab.com/elixxir/client/storage/versioned"
 	"gitlab.com/elixxir/comms/client"
 	"gitlab.com/elixxir/crypto/contact"
 	"gitlab.com/elixxir/crypto/cyclic"
 	"gitlab.com/elixxir/crypto/fastRNG"
 	"gitlab.com/elixxir/primitives/fact"
 	"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"
-	"math"
 	"time"
 )
 
 type SingleInterface interface {
-	TransmitSingleUse(contact.Contact, []byte, string, uint8, single.ReplyCallback,
-		time.Duration) error
+	TransmitRequest(recipient contact.Contact, tag string, payload []byte,
+		callback single.Response, param single.RequestParams, net cmix.Client, rng csprng.Source,
+		e2eGrp *cyclic.Group) (id.Round, receptionID.EphemeralIdentity, error)
 	StartProcesses() (stoppable.Stoppable, error)
 }
 
+type Userinfo interface {
+	PortableUserInfo() user.Info
+	GetUsername() (string, error)
+	GetReceptionRegistrationValidationSignature() []byte
+}
+
+const (
+// todo: populate with err messages
+)
+
+// todo: newuserDiscRegistratration, loadUserDiscRegistration
+//  neworLoad?
+// fixme: search/lookup off ud object
+//  shouldn't be, pass stuff into
+//
+
+// ud takes an interface to backup to store dep loop
+
 type Manager struct {
-	// External
-	client  *api.Client
-	comms   *client.Comms
-	rng     *fastRNG.StreamGenerator
-	sw      interfaces.Switchboard
-	storage *storage.Session
-	net     interfaces.NetworkManager
+	// refactored
+	// todo: docsting on what it is, why it's needed. For all things
+	//  in this object and the object itself
+	services cmix.Client
+	e2e      e2e.Handler
+	events   event.Manager
+	store    *store.Store
+
+	// todo: find a way to remove this, maybe just pass user into object (?)
+	user Userinfo
+
+	comms Comms
+	rng   *fastRNG.StreamGenerator
+
+	kv *versioned.KV
 
 	// Loaded from external access
 	privKey *rsa.PrivateKey
 	grp     *cyclic.Group
 
 	// internal structures
-	single SingleInterface
-	myID   *id.ID
+	myID *id.ID
 
 	// alternate User discovery service to circumvent production
 	alternativeUd *alternateUd
-
-	registered *uint32
 }
 
 // alternateUd is an alternative user discovery service.
@@ -60,58 +88,112 @@ type alternateUd struct {
 
 // NewManager builds a new user discovery manager. It requires that an updated
 // NDF is available and will error if one is not.
-func NewManager(client *api.Client, single *old.Manager) (*Manager, error) {
+// todo: docstring, organize the order of arguments in a meaningful way
+func NewManager(services cmix.Client, e2e e2e.Handler, events event.Manager,
+	comms Comms, userStore Userinfo, rng *fastRNG.StreamGenerator,
+	privKey *rsa.PrivateKey, username string,
+	kv *versioned.KV) (*Manager, error) {
 	jww.INFO.Println("ud.NewManager()")
+
+	// fixme: figuring out a way to avoid importing api would be nice
 	if client.NetworkFollowerStatus() != api.Running {
 		return nil, errors.New(
 			"cannot start UD Manager when network follower is not running.")
 	}
 
+	udStore, err := store.NewOrLoadStore(kv)
+	if err != nil {
+		return nil, errors.Errorf("Failed to initialize store: %v", err)
+	}
+
 	m := &Manager{
-		client:  client,
-		comms:   client.GetComms(),
-		rng:     client.GetRng(),
-		sw:      client.GetSwitchboard(),
-		storage: client.GetStorage(),
-		net:     client.GetNetworkInterface(),
-		single:  single,
+		services: services,
+		e2e:      e2e,
+		events:   events,
+		comms:    comms,
+		rng:      rng,
+		store:    udStore,
+		myID:     e2e.GetReceptionID(),
+		grp:      e2e.GetGroup(),
+		privKey:  privKey,
+		user:     userStore,
+		kv:       kv,
 	}
 
 	// check that user discovery is available in the NDF
-	def := m.net.GetInstance().GetPartialNdf().Get()
+	def := m.services.GetInstance().GetPartialNdf().Get()
 
 	if def.UDB.Cert == "" {
-		return nil, errors.New("NDF does not have User Discovery information, " +
-			"is there network access?: Cert not present.")
+		return nil, errors.New("NDF does not have User Discovery " +
+			"information, is there network access?: Cert not present.")
 	}
 
-	// Create the user discovery host object
-	hp := connect.GetDefaultHostParams()
-	// Client will not send KeepAlive packets
-	hp.KaClientOpts.Time = time.Duration(math.MaxInt64)
-	hp.MaxRetries = 3
-	hp.SendTimeout = 3 * time.Second
-	hp.AuthEnabled = false
-
-	m.myID = m.storage.User().GetCryptographicIdentity().GetReceptionID()
+	// Pull user discovery ID from NDF
+	udID, err := id.Unmarshal(def.UDB.ID)
+	if err != nil {
+		return nil, errors.Errorf("failed to unmarshal UD ID "+
+			"from NDF: %+v", err)
+	}
 
-	// get the commonly used data from storage
-	m.privKey = m.storage.GetUser().ReceptionRSA
+	udHost, err := m.getOrAddUdHost()
+	if err != nil {
+		return nil, errors.WithMessage(err, "User Discovery host object could "+
+			"not be constructed.")
+	}
 
-	// Load if the client is registered
-	m.loadRegistered()
+	// Register with user discovery
+	err = m.register(username, comms, udHost)
+	if err != nil {
+		return nil, errors.Errorf("Failed to register: %v", err)
+	}
 
-	// Store the pointer to the group locally for easy access
-	m.grp = m.storage.E2e().GetGroup()
+	// Set storage to registered
+	// todo: maybe we don't need this?
+	if err = m.setRegistered(); err != nil && m.events != nil {
+		m.events.Report(1, "UserDiscovery", "Registration",
+			fmt.Sprintf("User Registered with UD: %+v",
+				username))
+	}
 
 	return m, nil
 }
 
+func LoadManager(services cmix.Client, e2e e2e.Handler, events event.Manager,
+	comms Comms, userStore Userinfo, rng *fastRNG.StreamGenerator,
+	privKey *rsa.PrivateKey, kv *versioned.KV) (*Manager, error) {
+
+	m := &Manager{
+		services: services,
+		e2e:      e2e,
+		events:   events,
+		comms:    comms,
+		user:     userStore,
+		rng:      rng,
+		privKey:  privKey,
+		kv:       kv,
+	}
+
+	if !m.isRegistered() {
+		return nil, errors.Errorf("LoadManager could not detect that " +
+			"the user has been registered. Has a manager been initiated before?")
+	}
+
+	udStore, err := store.NewOrLoadStore(kv)
+	if err != nil {
+		return nil, errors.Errorf("Failed to initialize store: %v", err)
+	}
+
+	m.store = udStore
+
+	return m, err
+}
+
 // SetAlternativeUserDiscovery sets the alternativeUd object within manager.
 // Once set, any user discovery operation will go through the alternative
 // user discovery service.
 // To undo this operation, use UnsetAlternativeUserDiscovery.
-func (m *Manager) SetAlternativeUserDiscovery(altCert, altAddress, contactFile []byte) error {
+func (m *Manager) SetAlternativeUserDiscovery(altCert, altAddress,
+	contactFile []byte) error {
 	params := connect.GetDefaultHostParams()
 	params.AuthEnabled = false
 
@@ -152,74 +234,41 @@ func (m *Manager) UnsetAlternativeUserDiscovery() error {
 	return nil
 }
 
-// BackUpMissingFacts adds a registered fact to the Store object. It can take in both an
-// email and a phone number. One or the other may be nil, however both is considered
-// an error. It checks for the proper fact type for the associated fact.
-// Any other fact.FactType is not accepted and returns an error and nothing is backed up.
-// If you attempt to back up a fact type that has already been backed up,
-// an error will be returned and nothing will be backed up.
-// Otherwise, it adds the fact and returns whether the Store saved successfully.
+// BackUpMissingFacts adds a registered fact to the Store object.
+// It can take in both an email and a phone number. One or the other may be nil,
+// however both is considered an error. It checks for the proper fact type for
+// the associated fact. Any other fact.FactType is not accepted and returns an
+// error and nothing is backed up. If you attempt to back up a fact type that h
+// as already been backed up, an error will be returned and nothing will be
+// backed up. Otherwise, it adds the fact and returns whether the Store saved
+// successfully.
 func (m *Manager) BackUpMissingFacts(email, phone fact.Fact) error {
-	return m.storage.GetUd().BackUpMissingFacts(email, phone)
+	return m.store.BackUpMissingFacts(email, phone)
 }
 
 // GetFacts returns a list of fact.Fact objects that exist within the
 // Store's registeredFacts map.
 func (m *Manager) GetFacts() []fact.Fact {
-	return m.storage.GetUd().GetFacts()
+	return m.store.GetFacts()
 }
 
 // GetStringifiedFacts returns a list of stringified facts from the Store's
 // registeredFacts map.
 func (m *Manager) GetStringifiedFacts() []string {
-	return m.storage.GetUd().GetStringifiedFacts()
-}
-
-// getHost returns the current UD host for the UD ID found in the NDF. If the
-// host does not exist, then it is added and returned
-func (m *Manager) getHost() (*connect.Host, error) {
-	// Return alternative User discovery service if it has been set
-	if m.alternativeUd != nil {
-		return m.alternativeUd.host, nil
-	}
-
-	netDef := m.net.GetInstance().GetPartialNdf().Get()
-	// Unmarshal UD ID from the NDF
-	udID, err := id.Unmarshal(netDef.UDB.ID)
-	if err != nil {
-		return nil, errors.Errorf("failed to unmarshal UD ID from NDF: %+v", err)
-	}
-
-	// Return the host, if it exists
-	host, exists := m.comms.GetHost(udID)
-	if exists {
-		return host, nil
-	}
-
-	params := connect.GetDefaultHostParams()
-	params.AuthEnabled = false
-	params.SendTimeout = 20 * time.Second
-
-	// Add a new host and return it if it does not already exist
-	host, err = m.comms.AddHost(udID, netDef.UDB.Address,
-		[]byte(netDef.UDB.Cert), params)
-	if err != nil {
-		return nil, errors.WithMessage(err, "User Discovery host object could "+
-			"not be constructed.")
-	}
-
-	return host, nil
+	return m.store.GetStringifiedFacts()
 }
 
-// getContact returns the contact for UD as retrieved from the NDF.
-func (m *Manager) getContact() (contact.Contact, error) {
+// GetContact returns the contact for UD as retrieved from the NDF.
+func (m *Manager) GetContact() (contact.Contact, error) {
 	// Return alternative User discovery contact if set
 	if m.alternativeUd != nil {
 		// Unmarshal UD DH public key
-		alternativeDhPubKey := m.storage.E2e().GetGroup().NewInt(1)
-		if err := alternativeDhPubKey.UnmarshalJSON(m.alternativeUd.dhPubKey); err != nil {
+		alternativeDhPubKey := m.grp.NewInt(1)
+		if err := alternativeDhPubKey.
+			UnmarshalJSON(m.alternativeUd.dhPubKey); err != nil {
 			return contact.Contact{},
-				errors.WithMessage(err, "Failed to unmarshal UD DH public key.")
+				errors.WithMessage(err, "Failed to unmarshal UD "+
+					"DH public key.")
 		}
 
 		return contact.Contact{
@@ -230,7 +279,7 @@ func (m *Manager) getContact() (contact.Contact, error) {
 		}, nil
 	}
 
-	netDef := m.net.GetInstance().GetPartialNdf().Get()
+	netDef := m.services.GetInstance().GetPartialNdf().Get()
 
 	// Unmarshal UD ID from the NDF
 	udID, err := id.Unmarshal(netDef.UDB.ID)
@@ -240,10 +289,11 @@ func (m *Manager) getContact() (contact.Contact, error) {
 	}
 
 	// Unmarshal UD DH public key
-	dhPubKey := m.storage.E2e().GetGroup().NewInt(1)
+	dhPubKey := m.grp.NewInt(1)
 	if err = dhPubKey.UnmarshalJSON(netDef.UDB.DhPubKey); err != nil {
 		return contact.Contact{},
-			errors.WithMessage(err, "Failed to unmarshal UD DH public key.")
+			errors.WithMessage(err, "Failed to unmarshal UD DH "+
+				"public key.")
 	}
 
 	return contact.Contact{
@@ -253,3 +303,40 @@ func (m *Manager) getContact() (contact.Contact, error) {
 		Facts:          nil,
 	}, nil
 }
+
+// getOrAddUdHost returns the current UD host for the UD ID found in the NDF.
+// If the host does not exist, then it is added and returned.
+func (m *Manager) getOrAddUdHost() (*connect.Host, error) {
+	// Return alternative User discovery service if it has been set
+	if m.alternativeUd != nil {
+		return m.alternativeUd.host, nil
+	}
+
+	netDef := m.services.GetInstance().GetPartialNdf().Get()
+	// Unmarshal UD ID from the NDF
+	udID, err := id.Unmarshal(netDef.UDB.ID)
+	if err != nil {
+		return nil, errors.Errorf("failed to "+
+			"unmarshal UD ID from NDF: %+v", err)
+	}
+
+	// Return the host, if it exists
+	host, exists := m.comms.GetHost(udID)
+	if exists {
+		return host, nil
+	}
+
+	params := connect.GetDefaultHostParams()
+	params.AuthEnabled = false
+	params.SendTimeout = 20 * time.Second
+
+	// Add a new host and return it if it does not already exist
+	host, err = m.comms.AddHost(udID, netDef.UDB.Address,
+		[]byte(netDef.UDB.Cert), params)
+	if err != nil {
+		return nil, errors.WithMessage(err, "User Discovery host "+
+			"object could not be constructed.")
+	}
+
+	return host, nil
+}
diff --git a/ud/register.go b/ud/register.go
index 55e62c1f8..a7b7928f4 100644
--- a/ud/register.go
+++ b/ud/register.go
@@ -1,61 +1,41 @@
 package ud
 
 import (
-	"fmt"
 	"github.com/pkg/errors"
-	jww "github.com/spf13/jwalterweatherman"
 	pb "gitlab.com/elixxir/comms/mixmessages"
 	"gitlab.com/elixxir/crypto/factID"
 	"gitlab.com/elixxir/crypto/hash"
 	"gitlab.com/elixxir/primitives/fact"
 	"gitlab.com/xx_network/comms/connect"
-	"gitlab.com/xx_network/comms/messages"
 	"gitlab.com/xx_network/crypto/signature/rsa"
 )
 
-type registerUserComms interface {
-	SendRegisterUser(*connect.Host, *pb.UDBUserRegistration) (*messages.Ack, error)
-}
-
-// Register registers a user with user discovery. Will return an error if the
-// network signatures are malformed or if the username is taken. Usernames cannot
-// be changed after registration at this time. Will fail if the user is already
-// registered.
-// Identity does not go over cmix, it occurs over normal communications
-func (m *Manager) Register(username string) error {
-	jww.INFO.Printf("ud.Register(%s)", username)
-	return m.register(username, m.comms)
-}
-
-// register registers a user with user discovery with a specified comm for
-// easier testing.
-func (m *Manager) register(username string, comm registerUserComms) error {
-	if m.IsRegistered() {
-		return errors.New("cannot register client with User Discovery: " +
-			"client is already registered")
-	}
+// register initiates registration with user discovery given a specified
+// username. Provided a comms sub-interface to facilitate testing.
+func (m *Manager) register(username string,
+	comm registerUserComms, udHost *connect.Host) error {
 
 	var err error
-	user := m.storage.User()
-	cryptoUser := m.storage.User().GetCryptographicIdentity()
-	rng := m.rng.GetStream()
+	cryptoUser := m.user.PortableUserInfo()
+	stream := m.rng.GetStream()
+	defer stream.Close()
 
 	// Construct the user registration message
 	msg := &pb.UDBUserRegistration{
-		PermissioningSignature: user.GetReceptionRegistrationValidationSignature(),
-		RSAPublicPem:           string(rsa.CreatePublicKeyPem(cryptoUser.GetReceptionRSA().GetPublic())),
+		PermissioningSignature: m.user.GetReceptionRegistrationValidationSignature(),
+		RSAPublicPem:           string(rsa.CreatePublicKeyPem(cryptoUser.ReceptionRSA.GetPublic())),
 		IdentityRegistration: &pb.Identity{
 			Username: username,
-			DhPubKey: m.storage.E2e().GetDHPublicKey().Bytes(),
-			Salt:     cryptoUser.GetReceptionSalt(),
+			DhPubKey: cryptoUser.E2eDhPublicKey.Bytes(),
+			Salt:     cryptoUser.ReceptionSalt,
 		},
-		UID:       cryptoUser.GetReceptionID().Marshal(),
-		Timestamp: user.GetRegistrationTimestamp().UnixNano(),
+		UID:       cryptoUser.ReceptionID.Marshal(),
+		Timestamp: cryptoUser.RegistrationTimestamp,
 	}
 
 	// Sign the identity data and add to user registration message
 	identityDigest := msg.IdentityRegistration.Digest()
-	msg.IdentitySignature, err = rsa.Sign(rng, cryptoUser.GetReceptionRSA(),
+	msg.IdentitySignature, err = rsa.Sign(stream, cryptoUser.ReceptionRSA,
 		hash.CMixHash, identityDigest, nil)
 	if err != nil {
 		return errors.Errorf("Failed to sign user's IdentityRegistration: %+v", err)
@@ -69,11 +49,11 @@ func (m *Manager) register(username string, comm registerUserComms) error {
 
 	// Hash and sign fact
 	hashedFact := factID.Fingerprint(usernameFact)
-	signedFact, err := rsa.Sign(rng, cryptoUser.GetReceptionRSA(), hash.CMixHash, hashedFact, nil)
+	signedFact, err := rsa.Sign(stream, cryptoUser.ReceptionRSA, hash.CMixHash, hashedFact, nil)
 
 	// Add username fact register request to the user registration message
 	msg.Frs = &pb.FactRegisterRequest{
-		UID: cryptoUser.GetReceptionID().Marshal(),
+		UID: cryptoUser.ReceptionID.Marshal(),
 		Fact: &pb.Fact{
 			Fact:     username,
 			FactType: 0,
@@ -81,23 +61,7 @@ func (m *Manager) register(username string, comm registerUserComms) error {
 		FactSig: signedFact,
 	}
 
-	// get UD host
-	host, err := m.getHost()
-	if err != nil {
-		return err
-	}
-
 	// Register user with user discovery
-	_, err = comm.SendRegisterUser(host, msg)
-
-	if err == nil {
-		err = m.setRegistered()
-		if m.client != nil {
-			m.client.ReportEvent(1, "UserDiscovery", "Registration",
-				fmt.Sprintf("User Registered with UD: %+v",
-					user))
-		}
-	}
-
+	_, err = comm.SendRegisterUser(udHost, msg)
 	return err
 }
diff --git a/ud/registered.go b/ud/registered.go
index d81e61589..ad1854498 100644
--- a/ud/registered.go
+++ b/ud/registered.go
@@ -2,53 +2,36 @@ package ud
 
 import (
 	"encoding/binary"
-	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/storage/versioned"
 	"gitlab.com/xx_network/primitives/netTime"
-	"sync/atomic"
 )
 
 const isRegisteredKey = "isRegisteredKey"
 const isRegisteredVersion = 0
 
-// loadRegistered loads from storage if the client is registered with user
+// isRegistered loads from storage if the client is registered with user
 // discovery.
-func (m *Manager) loadRegistered() {
-	var isReg = uint32(0)
-	obj, err := m.storage.Get(isRegisteredKey)
+func (m *Manager) isRegistered() bool {
+	obj, err := m.kv.Get(isRegisteredKey, isRegisteredVersion)
 	if err != nil {
-		jww.INFO.Printf("Failed to load is registered, "+
-			"assuming un-registered: %s", err)
-	} else {
-		isReg = binary.BigEndian.Uint32(obj.Data)
+		return false
 	}
 
-	m.registered = &isReg
+	return true
 }
 
-// IsRegistered returns if the client is registered with user discovery
-func (m *Manager) IsRegistered() bool {
-	return atomic.LoadUint32(m.registered) == 1
-}
-
-// IsRegistered returns if the client is registered with user discovery
+// isRegistered returns if the client is registered with user discovery
 func (m *Manager) setRegistered() error {
-	if !atomic.CompareAndSwapUint32(m.registered, 0, 1) {
-		return errors.New("cannot register with User Discovery when " +
-			"already registered")
-	}
-
 	data := make([]byte, 4)
 	binary.BigEndian.PutUint32(data, 1)
-
 	obj := &versioned.Object{
 		Version:   isRegisteredVersion,
 		Timestamp: netTime.Now(),
 		Data:      data,
 	}
 
-	if err := m.storage.Set(isRegisteredKey, obj); err != nil {
+	if err := m.kv.Set(isRegisteredKey, isRegisteredVersion, obj); err != nil {
 		jww.FATAL.Panicf("Failed to store that the client is "+
 			"registered: %+v", err)
 	}
diff --git a/ud/remove.go b/ud/remove.go
index 07670d738..973a997ad 100644
--- a/ud/remove.go
+++ b/ud/remove.go
@@ -2,6 +2,7 @@ package ud
 
 import (
 	"crypto/rand"
+	"fmt"
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/comms/mixmessages"
@@ -9,36 +10,36 @@ import (
 	"gitlab.com/elixxir/crypto/hash"
 	"gitlab.com/elixxir/primitives/fact"
 	"gitlab.com/xx_network/comms/connect"
-	"gitlab.com/xx_network/comms/messages"
 	"gitlab.com/xx_network/crypto/signature/rsa"
+	"gitlab.com/xx_network/primitives/id"
 )
 
-type removeFactComms interface {
-	SendRemoveFact(host *connect.Host, message *mixmessages.FactRemovalRequest) (*messages.Ack, error)
-}
-
 // RemoveFact removes a previously confirmed fact. Will fail if the fact is not
 // associated with this client.
-func (m *Manager) RemoveFact(fact fact.Fact) error {
-	jww.INFO.Printf("ud.RemoveFact(%s)", fact.Stringify())
-	return m.removeFact(fact, m.comms)
+func (m *Manager) RemoveFact(f fact.Fact) error {
+	jww.INFO.Printf("ud.RemoveFact(%s)", f.Stringify())
+
+	return m.removeFact(f, m.comms)
 }
 
-func (m *Manager) removeFact(fact fact.Fact, rFC removeFactComms) error {
-	if !m.IsRegistered() {
-		return errors.New("Failed to remove fact: " +
-			"client is not registered")
+func (m *Manager) removeFact(f fact.Fact,
+	rFC removeFactComms) error {
+
+	// Get UD host
+	udHost, err := m.getOrAddUdHost()
+	if err != nil {
+		return err
 	}
 
 	// Construct the message to send
 	// Convert our Fact to a mixmessages Fact for sending
 	mmFact := mixmessages.Fact{
-		Fact:     fact.Fact,
-		FactType: uint32(fact.T),
+		Fact:     f.Fact,
+		FactType: uint32(f.T),
 	}
 
 	// Create a hash of our fact
-	fHash := factID.Fingerprint(fact)
+	fHash := factID.Fingerprint(f)
 
 	// Sign our inFact for putting into the request
 	fSig, err := rsa.Sign(rand.Reader, m.privKey, hash.CMixHash, fHash, nil)
@@ -53,70 +54,61 @@ func (m *Manager) removeFact(fact fact.Fact, rFC removeFactComms) error {
 		FactSig:     fSig,
 	}
 
-	// get UD host
-	host, err := m.getHost()
-	if err != nil {
-		return err
-	}
-
 	// Send the message
-	_, err = rFC.SendRemoveFact(host, &remFactMsg)
+	_, err = rFC.SendRemoveFact(udHost, &remFactMsg)
 	if err != nil {
 		return err
 	}
 
 	// Remove from storage
-	return m.storage.GetUd().DeleteFact(fact)
+	return m.store.DeleteFact(f)
 }
 
-type removeUserComms interface {
-	SendRemoveUser(host *connect.Host, message *mixmessages.FactRemovalRequest) (*messages.Ack, error)
-}
+// RemoveUser removes a previously confirmed fact.
+// This call will fail if the fact is not associated with this client.
+func (m *Manager) RemoveUser(f fact.Fact) error {
+	jww.INFO.Printf("ud.RemoveUser(%s)", f.Stringify())
+	if f.T != fact.Username {
+		return errors.New(fmt.Sprintf("RemoveUser must only remove "+
+			"a username. Cannot remove fact %q", f.Fact))
+	}
 
-// RemoveUser removes a previously confirmed fact. Will fail if the fact is not
-// associated with this client.
-func (m *Manager) RemoveUser(fact fact.Fact) error {
-	jww.INFO.Printf("ud.RemoveUser(%s)", fact.Stringify())
-	return m.removeUser(fact, m.comms)
+	udHost, err := m.getOrAddUdHost()
+	if err != nil {
+		return err
+	}
+
+	return removeUser(f, m.myID, m.privKey, m.comms, udHost)
 }
 
-func (m *Manager) removeUser(fact fact.Fact, rFC removeUserComms) error {
-	if !m.IsRegistered() {
-		return errors.New("Failed to remove fact: " +
-			"client is not registered")
-	}
+func removeUser(f fact.Fact, myId *id.ID, privateKey *rsa.PrivateKey,
+	rFC removeUserComms, udHost *connect.Host) error {
 
 	// Construct the message to send
 	// Convert our Fact to a mixmessages Fact for sending
 	mmFact := mixmessages.Fact{
-		Fact:     fact.Fact,
-		FactType: uint32(fact.T),
+		Fact:     f.Fact,
+		FactType: uint32(f.T),
 	}
 
 	// Create a hash of our fact
-	fHash := factID.Fingerprint(fact)
+	fHash := factID.Fingerprint(f)
 
 	// Sign our inFact for putting into the request
-	fsig, err := rsa.Sign(rand.Reader, m.privKey, hash.CMixHash, fHash, nil)
+	fsig, err := rsa.Sign(rand.Reader, privateKey, hash.CMixHash, fHash, nil)
 	if err != nil {
 		return err
 	}
 
 	// Create our Fact Removal Request message data
 	remFactMsg := mixmessages.FactRemovalRequest{
-		UID:         m.myID.Marshal(),
+		UID:         myId.Marshal(),
 		RemovalData: &mmFact,
 		FactSig:     fsig,
 	}
 
-	// get UD host
-	host, err := m.getHost()
-	if err != nil {
-		return err
-	}
-
 	// Send the message
-	_, err = rFC.SendRemoveUser(host, &remFactMsg)
+	_, err = rFC.SendRemoveUser(udHost, &remFactMsg)
 
 	// Return the error
 	return err
diff --git a/ud/search.go b/ud/search.go
index ac978a438..7cb6bc6d5 100644
--- a/ud/search.go
+++ b/ud/search.go
@@ -5,8 +5,13 @@ import (
 	"github.com/golang/protobuf/proto"
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/cmix"
+	"gitlab.com/elixxir/client/event"
+	"gitlab.com/elixxir/client/single"
 	"gitlab.com/elixxir/crypto/contact"
+	"gitlab.com/elixxir/crypto/cyclic"
 	"gitlab.com/elixxir/crypto/factID"
+	"gitlab.com/elixxir/crypto/fastRNG"
 	"gitlab.com/elixxir/primitives/fact"
 	"gitlab.com/xx_network/primitives/id"
 	"time"
@@ -26,7 +31,11 @@ type searchCallback func([]contact.Contact, error)
 // used to search for multiple users at once; that can have a privacy reduction.
 // Instead, it is intended to be used to search for a user where multiple pieces
 // of information is known.
-func (m *Manager) Search(list fact.FactList, callback searchCallback, timeout time.Duration) error {
+func Search(list fact.FactList,
+	services cmix.Client, events event.Manager,
+	callback searchCallback,
+	rng *fastRNG.StreamGenerator, udContact contact.Contact,
+	grp *cyclic.Group, timeout time.Duration) error {
 	jww.INFO.Printf("ud.Search(%s, %s)", list.Stringify(), timeout)
 
 	factHashes, factMap := hashFactList(list)
@@ -42,20 +51,23 @@ func (m *Manager) Search(list fact.FactList, callback searchCallback, timeout ti
 		m.searchResponseHandler(factMap, callback, payload, err)
 	}
 
-	// get UD contact
-	c, err := m.getContact()
-	if err != nil {
-		return err
+	stream := rng.GetStream()
+	defer stream.Close()
+
+	p := single.RequestParams{
+		Timeout:     timeout,
+		MaxMessages: maxLookupMessages,
+		CmixParam:   cmix.GetDefaultCMIXParams(),
 	}
 
-	err = m.single.TransmitSingleUse(c, requestMarshaled, SearchTag,
-		maxSearchMessages, f, timeout)
+	rndId, ephId, err := single.TransmitRequest(udContact, LookupTag, requestMarshaled,
+		f, p, services, stream, grp)
 	if err != nil {
 		return errors.WithMessage(err, "Failed to transmit search request.")
 	}
 
-	if m.client != nil {
-		m.client.ReportEvent(1, "UserDiscovery", "SearchRequest",
+	if events != nil {
+		events.Report(1, "UserDiscovery", "SearchRequest",
 			fmt.Sprintf("Sent: %+v", request))
 	}
 
@@ -77,8 +89,8 @@ func (m *Manager) searchResponseHandler(factMap map[string]fact.Fact,
 			"failed unmarshal: %s", err)
 	}
 
-	if m.client != nil {
-		m.client.ReportEvent(1, "UserDiscovery", "SearchResponse",
+	if m.services != nil {
+		m.events.Report(1, "UserDiscovery", "SearchResponse",
 			fmt.Sprintf("Received: %+v", searchResponse))
 	}
 
diff --git a/storage/ud/facts.go b/ud/store/facts.go
similarity index 100%
rename from storage/ud/facts.go
rename to ud/store/facts.go
diff --git a/storage/ud/facts_test.go b/ud/store/facts_test.go
similarity index 100%
rename from storage/ud/facts_test.go
rename to ud/store/facts_test.go
diff --git a/storage/ud/store.go b/ud/store/store.go
similarity index 100%
rename from storage/ud/store.go
rename to ud/store/store.go
diff --git a/storage/ud/store_test.go b/ud/store/store_test.go
similarity index 100%
rename from storage/ud/store_test.go
rename to ud/store/store_test.go
diff --git a/ud/store/ud/facts.go b/ud/store/ud/facts.go
new file mode 100644
index 000000000..4346a347b
--- /dev/null
+++ b/ud/store/ud/facts.go
@@ -0,0 +1,190 @@
+///////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                          //
+//                                                                           //
+// Use of this source code is governed by a license that can be found in the //
+// LICENSE file                                                              //
+///////////////////////////////////////////////////////////////////////////////
+
+package store
+
+import (
+	"fmt"
+	"github.com/pkg/errors"
+	"gitlab.com/elixxir/primitives/fact"
+)
+
+const (
+	factTypeExistsErr               = "Fact %v cannot be added as fact type %s has already been stored. Cancelling backup operation!"
+	backupMissingInvalidFactTypeErr = "BackUpMissingFacts expects input in the order (email, phone). " +
+		"%s (%s) is non-empty but not an email. Cancelling backup operation"
+	backupMissingAllZeroesFactErr = "Cannot backup missing facts: Both email and phone facts are empty!"
+	factNotInStoreErr             = "Fact %v does not exist in store"
+)
+
+// StoreUnconfirmedFact stores a fact that has been added to UD but has not been
+// confirmed by the user. It is keyed on the confirmation ID given by UD.
+func (s *Store) StoreUnconfirmedFact(confirmationId string, f fact.Fact) error {
+	s.mux.Lock()
+	defer s.mux.Unlock()
+
+	s.unconfirmedFacts[confirmationId] = f
+	return s.saveUnconfirmedFacts()
+}
+
+// ConfirmFact will delete the fact from the unconfirmed store and
+// add it to the confirmed fact store. The Store will then be saved
+func (s *Store) ConfirmFact(confirmationId string) error {
+	s.mux.Lock()
+	defer s.mux.Unlock()
+
+	f, exists := s.unconfirmedFacts[confirmationId]
+	if !exists {
+		return errors.New(fmt.Sprintf("No fact exists in store "+
+			"with confirmation ID %q", confirmationId))
+	}
+
+	delete(s.unconfirmedFacts, confirmationId)
+	s.confirmedFacts[f] = struct{}{}
+	return s.save()
+}
+
+// BackUpMissingFacts adds a registered fact to the Store object. It can take in both an
+// email and a phone number. One or the other may be an empty string, however both is considered
+// an error. It checks for each whether that fact type already exists in the structure. If a fact
+// type already exists, an error is returned.
+// ************************************************************************
+// NOTE: This is done since BackUpMissingFacts is exposed to the
+// bindings layer. This prevents front end from using this as the method
+// to store facts on their end, which is not its intended use case. It's intended use
+// case is to store already registered facts, prior to the creation of this function.
+// We handle storage of newly registered internally using Store.ConfirmFact.
+// ************************************************************************
+// Any other fact.FactType is not accepted and returns an error and nothing is backed up.
+// If you attempt to back up a fact type that has already been backed up,
+// an error will be returned and nothing will be backed up.
+// Otherwise, it adds the fact and returns whether the Store saved successfully.
+func (s *Store) BackUpMissingFacts(email, phone fact.Fact) error {
+	s.mux.Lock()
+	defer s.mux.Unlock()
+
+	if isFactZero(email) && isFactZero(phone) {
+		return errors.New(backupMissingAllZeroesFactErr)
+	}
+
+	modifiedEmail, modifiedPhone := false, false
+
+	// Handle email if it is not zero (empty string)
+	if !isFactZero(email) {
+		// check if fact is expected type
+		if email.T != fact.Email {
+			return errors.New(fmt.Sprintf(backupMissingInvalidFactTypeErr, fact.Email, email.Fact))
+		}
+
+		// Check if fact type is already in map. See docstring NOTE for explanation
+		if isFactTypeInMap(fact.Email, s.confirmedFacts) {
+			// If an email exists in memory, return an error
+			return errors.Errorf(factTypeExistsErr, email, fact.Email)
+		} else {
+			modifiedEmail = true
+		}
+	}
+
+	if !isFactZero(phone) {
+		// check if fact is expected type
+		if phone.T != fact.Phone {
+			return errors.New(fmt.Sprintf(backupMissingInvalidFactTypeErr, fact.Phone, phone.Fact))
+		}
+
+		// Check if fact type is already in map. See docstring NOTE for explanation
+		if isFactTypeInMap(fact.Phone, s.confirmedFacts) {
+			// If a phone exists in memory, return an error
+			return errors.Errorf(factTypeExistsErr, phone, fact.Phone)
+		} else {
+			modifiedPhone = true
+		}
+	}
+
+	if modifiedPhone || modifiedEmail {
+		if modifiedEmail {
+			s.confirmedFacts[email] = struct{}{}
+		}
+
+		if modifiedPhone {
+			s.confirmedFacts[phone] = struct{}{}
+		}
+
+		return s.saveConfirmedFacts()
+	}
+
+	return nil
+
+}
+
+// DeleteFact is our internal use function which will delete the registered fact
+// from memory and storage. An error is returned if the fact does not exist in
+// memory.
+func (s *Store) DeleteFact(f fact.Fact) error {
+	s.mux.Lock()
+	defer s.mux.Unlock()
+
+	if _, exists := s.confirmedFacts[f]; !exists {
+		return errors.Errorf(factNotInStoreErr, f)
+	}
+
+	delete(s.confirmedFacts, f)
+	return s.saveConfirmedFacts()
+}
+
+// GetStringifiedFacts returns a list of stringified facts from the Store's
+// confirmedFacts map.
+func (s *Store) GetStringifiedFacts() []string {
+	s.mux.RLock()
+	defer s.mux.RUnlock()
+
+	return s.serializeConfirmedFacts()
+}
+
+// GetFacts returns a list of fact.Fact objects that exist within the
+// Store's confirmedFacts map.
+func (s *Store) GetFacts() []fact.Fact {
+	s.mux.RLock()
+	defer s.mux.RUnlock()
+
+	// Flatten the facts into a slice
+	facts := make([]fact.Fact, 0, len(s.confirmedFacts))
+	for f := range s.confirmedFacts {
+		facts = append(facts, f)
+	}
+
+	return facts
+}
+
+// serializeConfirmedFacts is a helper function which serializes Store's confirmedFacts
+// map into a list of strings. Each string in the list represents
+// a fact.Fact that has been Stringified.
+func (s *Store) serializeConfirmedFacts() []string {
+	fStrings := make([]string, 0, len(s.confirmedFacts))
+	for f := range s.confirmedFacts {
+		fStrings = append(fStrings, f.Stringify())
+	}
+
+	return fStrings
+}
+
+// fixme: consider this being a method on the fact.Fact object?
+// isFactZero tests whether a fact has been uninitialized.
+func isFactZero(f fact.Fact) bool {
+	return f.T == fact.Username && f.Fact == ""
+}
+
+// isFactTypeInMap is a helper function which determines whether a fact type exists within
+// the data structure.
+func isFactTypeInMap(factType fact.FactType, facts map[fact.Fact]struct{}) bool {
+	for f := range facts {
+		if f.T == factType {
+			return true
+		}
+	}
+
+	return false
+}
diff --git a/ud/store/ud/facts_test.go b/ud/store/ud/facts_test.go
new file mode 100644
index 000000000..071b04b1d
--- /dev/null
+++ b/ud/store/ud/facts_test.go
@@ -0,0 +1,296 @@
+///////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                          //
+//                                                                           //
+// Use of this source code is governed by a license that can be found in the //
+// LICENSE file                                                              //
+///////////////////////////////////////////////////////////////////////////////
+
+package store
+
+import (
+	"gitlab.com/elixxir/client/storage/versioned"
+	"gitlab.com/elixxir/ekv"
+	"gitlab.com/elixxir/primitives/fact"
+	"reflect"
+	"sort"
+	"testing"
+)
+
+func TestNewStore(t *testing.T) {
+
+	kv := versioned.NewKV(make(ekv.Memstore))
+
+	_, err := NewOrLoadStore(kv)
+	if err != nil {
+		t.Errorf("NewStore() produced an error: %v", err)
+	}
+
+}
+
+func TestStore_ConfirmFact(t *testing.T) {
+	kv := versioned.NewKV(make(ekv.Memstore))
+
+	expectedStore, err := NewOrLoadStore(kv)
+	if err != nil {
+		t.Errorf("NewStore() produced an error: %v", err)
+	}
+
+	confirmId := "confirm"
+
+	expected := fact.Fact{
+		Fact: "josh",
+		T:    fact.Username,
+	}
+
+	err = expectedStore.StoreUnconfirmedFact(confirmId, expected)
+	if err != nil {
+		t.Fatalf("StoreUnconfirmedFact error: %v", err)
+	}
+
+	err = expectedStore.ConfirmFact(confirmId)
+	if err != nil {
+		t.Fatalf("ConfirmFact() produced an error: %v", err)
+	}
+
+	_, exists := expectedStore.confirmedFacts[expected]
+	if !exists {
+		t.Fatalf("Fact %s does not exist in map", expected)
+	}
+
+	// Check that fact was removed from unconfirmed
+	_, exists = expectedStore.unconfirmedFacts[confirmId]
+	if exists {
+		t.Fatalf("Confirmed fact %v should be removed from unconfirmed"+
+			" map", expected)
+	}
+}
+
+func TestStore_StoreUnconfirmedFact(t *testing.T) {
+	kv := versioned.NewKV(make(ekv.Memstore))
+
+	expectedStore, err := NewOrLoadStore(kv)
+	if err != nil {
+		t.Errorf("NewStore() produced an error: %v", err)
+	}
+
+	confirmId := "confirm"
+
+	expected := fact.Fact{
+		Fact: "josh",
+		T:    fact.Username,
+	}
+
+	err = expectedStore.StoreUnconfirmedFact(confirmId, expected)
+	if err != nil {
+		t.Fatalf("StoreUnconfirmedFact error: %v", err)
+	}
+
+	// Check that fact exists in unconfirmed
+	_, exists := expectedStore.unconfirmedFacts[confirmId]
+	if !exists {
+		t.Fatalf("Confirmed fact %v should be removed from unconfirmed"+
+			" map", expected)
+	}
+}
+
+func TestStore_DeleteFact(t *testing.T) {
+	kv := versioned.NewKV(make(ekv.Memstore))
+
+	expectedStore, err := NewOrLoadStore(kv)
+	if err != nil {
+		t.Errorf("NewStore() produced an error: %v", err)
+	}
+
+	expected := fact.Fact{
+		Fact: "josh",
+		T:    fact.Username,
+	}
+
+	expectedStore.confirmedFacts[expected] = struct{}{}
+
+	_, exists := expectedStore.confirmedFacts[expected]
+	if !exists {
+		t.Fatalf("Fact %s does not exist in map", expected)
+	}
+
+	err = expectedStore.DeleteFact(expected)
+	if err != nil {
+		t.Fatalf("DeleteFact() produced an error: %v", err)
+	}
+
+	err = expectedStore.DeleteFact(expected)
+	if err == nil {
+		t.Fatalf("DeleteFact should produce an error when deleting a fact not in store")
+	}
+
+}
+
+func TestStore_BackUpMissingFacts(t *testing.T) {
+	kv := versioned.NewKV(make(ekv.Memstore))
+
+	expectedStore, err := NewOrLoadStore(kv)
+	if err != nil {
+		t.Errorf("NewStore() produced an error: %v", err)
+	}
+
+	email := fact.Fact{
+		Fact: "josh@elixxir.io",
+		T:    fact.Email,
+	}
+
+	phone := fact.Fact{
+		Fact: "6175555678",
+		T:    fact.Phone,
+	}
+
+	err = expectedStore.BackUpMissingFacts(email, phone)
+	if err != nil {
+		t.Fatalf("BackUpMissingFacts() produced an error: %v", err)
+	}
+
+	_, exists := expectedStore.confirmedFacts[email]
+	if !exists {
+		t.Fatalf("Fact %v not found in store.", email)
+	}
+
+	_, exists = expectedStore.confirmedFacts[phone]
+	if !exists {
+		t.Fatalf("Fact %v not found in store.", phone)
+	}
+
+}
+
+func TestStore_BackUpMissingFacts_DuplicateFactType(t *testing.T) {
+	kv := versioned.NewKV(make(ekv.Memstore))
+
+	expectedStore, err := NewOrLoadStore(kv)
+	if err != nil {
+		t.Errorf("NewStore() produced an error: %v", err)
+	}
+
+	email := fact.Fact{
+		Fact: "josh@elixxir.io",
+		T:    fact.Email,
+	}
+
+	phone := fact.Fact{
+		Fact: "6175555678",
+		T:    fact.Phone,
+	}
+
+	err = expectedStore.BackUpMissingFacts(email, phone)
+	if err != nil {
+		t.Fatalf("BackUpMissingFacts() produced an error: %v", err)
+	}
+
+	err = expectedStore.BackUpMissingFacts(email, fact.Fact{})
+	if err == nil {
+		t.Fatalf("BackUpMissingFacts() should not allow backing up an "+
+			"email when an email has already been backed up: %v", err)
+	}
+
+	err = expectedStore.BackUpMissingFacts(fact.Fact{}, phone)
+	if err == nil {
+		t.Fatalf("BackUpMissingFacts() should not allow backing up a "+
+			"phone number when a phone number has already been backed up: %v", err)
+	}
+
+}
+
+func TestStore_GetFacts(t *testing.T) {
+	kv := versioned.NewKV(make(ekv.Memstore))
+
+	testStore, err := NewOrLoadStore(kv)
+	if err != nil {
+		t.Errorf("NewStore() produced an error: %v", err)
+	}
+
+	emailFact := fact.Fact{
+		Fact: "josh@elixxir.io",
+		T:    fact.Email,
+	}
+
+	emptyFact := fact.Fact{}
+
+	err = testStore.BackUpMissingFacts(emailFact, emptyFact)
+	if err != nil {
+		t.Fatalf("Faild to add fact %v: %v", emailFact, err)
+	}
+
+	phoneFact := fact.Fact{
+		Fact: "6175555212",
+		T:    fact.Phone,
+	}
+
+	err = testStore.BackUpMissingFacts(emptyFact, phoneFact)
+	if err != nil {
+		t.Fatalf("Faild to add fact %v: %v", phoneFact, err)
+	}
+
+	expectedFacts := []fact.Fact{emailFact, phoneFact}
+
+	receivedFacts := testStore.GetFacts()
+
+	sort.SliceStable(receivedFacts, func(i, j int) bool {
+		return receivedFacts[i].Fact > receivedFacts[j].Fact
+	})
+
+	sort.SliceStable(expectedFacts, func(i, j int) bool {
+		return expectedFacts[i].Fact > expectedFacts[j].Fact
+	})
+
+	if !reflect.DeepEqual(expectedFacts, receivedFacts) {
+		t.Fatalf("GetFacts() did not return expected fact list."+
+			"\nExpected: %v"+
+			"\nReceived: %v", expectedFacts, receivedFacts)
+	}
+}
+
+func TestStore_GetFactStrings(t *testing.T) {
+	kv := versioned.NewKV(make(ekv.Memstore))
+
+	testStore, err := NewOrLoadStore(kv)
+	if err != nil {
+		t.Errorf("NewStore() produced an error: %v", err)
+	}
+
+	emailFact := fact.Fact{
+		Fact: "josh@elixxir.io",
+		T:    fact.Email,
+	}
+
+	emptyFact := fact.Fact{}
+
+	err = testStore.BackUpMissingFacts(emailFact, emptyFact)
+	if err != nil {
+		t.Fatalf("Faild to add fact %v: %v", emailFact, err)
+	}
+
+	phoneFact := fact.Fact{
+		Fact: "6175555212",
+		T:    fact.Phone,
+	}
+
+	err = testStore.BackUpMissingFacts(emptyFact, phoneFact)
+	if err != nil {
+		t.Fatalf("Faild to add fact %v: %v", phoneFact, err)
+	}
+
+	expectedFacts := []string{emailFact.Stringify(), phoneFact.Stringify()}
+
+	receivedFacts := testStore.GetStringifiedFacts()
+	sort.SliceStable(receivedFacts, func(i, j int) bool {
+		return receivedFacts[i] > receivedFacts[j]
+	})
+
+	sort.SliceStable(expectedFacts, func(i, j int) bool {
+		return expectedFacts[i] > expectedFacts[j]
+	})
+
+	if !reflect.DeepEqual(expectedFacts, receivedFacts) {
+		t.Fatalf("GetStringifiedFacts() did not return expected fact list."+
+			"\nExpected: %v"+
+			"\nReceived: %v", expectedFacts, receivedFacts)
+	}
+
+}
diff --git a/ud/store/ud/store.go b/ud/store/ud/store.go
new file mode 100644
index 000000000..08178e924
--- /dev/null
+++ b/ud/store/ud/store.go
@@ -0,0 +1,271 @@
+package store
+
+// This file handles the storage operations on facts.
+
+import (
+	"encoding/json"
+	"github.com/pkg/errors"
+	"gitlab.com/elixxir/client/storage/versioned"
+	"gitlab.com/elixxir/primitives/fact"
+	"gitlab.com/xx_network/primitives/netTime"
+	"strings"
+	"sync"
+)
+
+// Storage constants
+const (
+	version            = 0
+	prefix             = "udStorePrefix"
+	unconfirmedFactKey = "unconfirmedFactKey"
+	confirmedFactKey   = "confirmedFactKey"
+)
+
+// todo: reorganize the contents of these files or rename these files
+//  also a refactor would be cool cause store.Store on the higher level
+//  is stuttering.
+
+// Error constants
+const (
+	malformedFactErr = "Failed to load due to " +
+		"malformed fact"
+	loadConfirmedFactErr   = "Failed to load confirmed facts"
+	loadUnconfirmedFactErr = "Failed to load unconfirmed facts"
+	saveUnconfirmedFactErr = "Failed to save unconfirmed facts"
+	saveConfirmedFactErr   = "Failed to save confirmed facts"
+)
+
+// Store is the storage object for the higher level ud.Manager object.
+// This storage implementation is written for client side.
+type Store struct {
+	// confirmedFacts contains facts that have been confirmed
+	confirmedFacts map[fact.Fact]struct{}
+	// Stores facts that have been added by UDB but unconfirmed facts.
+	// Maps confirmID to fact
+	unconfirmedFacts map[string]fact.Fact
+	kv               *versioned.KV
+	mux              sync.RWMutex
+}
+
+// NewOrLoadStore loads the Store object from the provided versioned.KV.
+func NewOrLoadStore(kv *versioned.KV) (*Store, error) {
+
+	s := &Store{
+		confirmedFacts:   make(map[fact.Fact]struct{}, 0),
+		unconfirmedFacts: make(map[string]fact.Fact, 0),
+		kv:               kv.Prefix(prefix),
+	}
+
+	if err := s.load(); err != nil {
+		if strings.Contains(err.Error(), "object not found") ||
+			strings.Contains(err.Error(), "no such file or directory") {
+			return s, s.save()
+		}
+	}
+
+	return s, nil
+
+}
+
+/////////////////////////////////////////////////////////////////
+// SAVE FUNCTIONS
+/////////////////////////////////////////////////////////////////
+
+// save serializes the state within Store into byte data and stores
+// that data into storage via the EKV.
+func (s *Store) save() error {
+
+	err := s.saveUnconfirmedFacts()
+	if err != nil {
+		return errors.WithMessage(err, saveUnconfirmedFactErr)
+	}
+
+	err = s.saveConfirmedFacts()
+	if err != nil {
+		return errors.WithMessage(err, saveConfirmedFactErr)
+	}
+
+	return nil
+}
+
+// saveConfirmedFacts saves all the data within Store.confirmedFacts into storage.
+func (s *Store) saveConfirmedFacts() error {
+
+	data, err := s.marshalConfirmedFacts()
+	if err != nil {
+		return err
+	}
+
+	// Construct versioned object
+	now := netTime.Now()
+	obj := versioned.Object{
+		Version:   version,
+		Timestamp: now,
+		Data:      data,
+	}
+
+	// Save to storage
+	return s.kv.Set(confirmedFactKey, version, &obj)
+}
+
+// saveUnconfirmedFacts saves all data within Store.unconfirmedFacts into storage.
+func (s *Store) saveUnconfirmedFacts() error {
+	data, err := s.marshalUnconfirmedFacts()
+	if err != nil {
+		return err
+	}
+
+	// Construct versioned object
+	now := netTime.Now()
+	obj := versioned.Object{
+		Version:   version,
+		Timestamp: now,
+		Data:      data,
+	}
+
+	// Save to storage
+	return s.kv.Set(unconfirmedFactKey, version, &obj)
+
+}
+
+/////////////////////////////////////////////////////////////////
+// LOAD FUNCTIONS
+/////////////////////////////////////////////////////////////////
+
+// load is a helper function which loads all data stored in storage from
+// the save operation.
+func (s *Store) load() error {
+
+	err := s.loadUnconfirmedFacts()
+	if err != nil {
+		return errors.WithMessage(err, loadUnconfirmedFactErr)
+	}
+
+	err = s.loadConfirmedFacts()
+	if err != nil {
+		return errors.WithMessage(err, loadConfirmedFactErr)
+	}
+
+	return nil
+}
+
+// loadConfirmedFacts loads all confirmed facts from storage.
+// It is the inverse operation of saveConfirmedFacts.
+func (s *Store) loadConfirmedFacts() error {
+	// Pull data from storage
+	obj, err := s.kv.Get(confirmedFactKey, version)
+	if err != nil {
+		return err
+	}
+
+	// Place the map in memory
+	s.confirmedFacts, err = s.unmarshalConfirmedFacts(obj.Data)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// loadUnconfirmedFacts loads all unconfirmed facts from storage.
+// It is the inverse operation of saveUnconfirmedFacts.
+func (s *Store) loadUnconfirmedFacts() error {
+	// Pull data from storage
+	obj, err := s.kv.Get(unconfirmedFactKey, version)
+	if err != nil {
+		return err
+	}
+
+	// Place the map in memory
+	s.unconfirmedFacts, err = s.unmarshalUnconfirmedFacts(obj.Data)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+/////////////////////////////////////////////////////////////////
+// MARSHAL/UNMARSHAL FUNCTIONS
+/////////////////////////////////////////////////////////////////
+
+// unconfirmedFactDisk is an object used to store the data of an unconfirmed fact.
+// It combines the key (confirmationId) and fact data (stringifiedFact) into a
+// single JSON-able object.
+type unconfirmedFactDisk struct {
+	confirmationId  string
+	stringifiedFact string
+}
+
+// marshalConfirmedFacts is a marshaller which serializes the data
+//// in the confirmedFacts map into a JSON.
+func (s *Store) marshalConfirmedFacts() ([]byte, error) {
+	// Flatten confirmed facts to a list
+	fStrings := s.serializeConfirmedFacts()
+
+	// Marshal to JSON
+	return json.Marshal(&fStrings)
+}
+
+// marshalUnconfirmedFacts is a marshaller which serializes the data
+// in the unconfirmedFacts map into a JSON.
+func (s *Store) marshalUnconfirmedFacts() ([]byte, error) {
+	// Flatten unconfirmed facts to a list
+	ufdList := make([]unconfirmedFactDisk, 0, len(s.unconfirmedFacts))
+	for confirmationId, f := range s.unconfirmedFacts {
+		ufd := unconfirmedFactDisk{
+			confirmationId:  confirmationId,
+			stringifiedFact: f.Stringify(),
+		}
+		ufdList = append(ufdList, ufd)
+	}
+
+	return json.Marshal(&ufdList)
+}
+
+// unmarshalConfirmedFacts is a function which deserializes the data from storage
+// into a structure matching the confirmedFacts map.
+func (s *Store) unmarshalConfirmedFacts(data []byte) (map[fact.Fact]struct{}, error) {
+	// Unmarshal into list
+	var fStrings []string
+	err := json.Unmarshal(data, &fStrings)
+	if err != nil {
+		return nil, err
+	}
+
+	// Deserialize the list into a map
+	confirmedFacts := make(map[fact.Fact]struct{}, 0)
+	for _, fStr := range fStrings {
+		f, err := fact.UnstringifyFact(fStr)
+		if err != nil {
+			return nil, errors.WithMessage(err, malformedFactErr)
+		}
+
+		confirmedFacts[f] = struct{}{}
+	}
+
+	return confirmedFacts, nil
+}
+
+// unmarshalUnconfirmedFacts is a function which deserializes the data from storage
+// into a structure matching the unconfirmedFacts map.
+func (s *Store) unmarshalUnconfirmedFacts(data []byte) (map[string]fact.Fact, error) {
+	// Unmarshal into list
+	var ufdList []unconfirmedFactDisk
+	err := json.Unmarshal(data, &ufdList)
+	if err != nil {
+		return nil, err
+	}
+
+	// Deserialize the list into a map
+	unconfirmedFacts := make(map[string]fact.Fact, 0)
+	for _, ufd := range ufdList {
+		f, err := fact.UnstringifyFact(ufd.stringifiedFact)
+		if err != nil {
+			return nil, errors.WithMessage(err, malformedFactErr)
+		}
+
+		unconfirmedFacts[ufd.confirmationId] = f
+	}
+
+	return unconfirmedFacts, nil
+}
diff --git a/ud/store/ud/store_test.go b/ud/store/ud/store_test.go
new file mode 100644
index 000000000..4fcde42f0
--- /dev/null
+++ b/ud/store/ud/store_test.go
@@ -0,0 +1,129 @@
+package store
+
+import (
+	"bytes"
+	"gitlab.com/elixxir/client/storage/versioned"
+	"gitlab.com/elixxir/ekv"
+	"gitlab.com/elixxir/primitives/fact"
+	"reflect"
+	"testing"
+)
+
+// Test it loads a Store from storage if it exists.
+func TestNewOrLoadStore_LoadStore(t *testing.T) {
+	kv := versioned.NewKV(make(ekv.Memstore))
+
+	expectedStore, err := NewStore(kv)
+	if err != nil {
+		t.Errorf("NewStore() produced an error: %v", err)
+	}
+
+	receivedStore, err := NewOrLoadStore(kv)
+	if err != nil {
+		t.Fatalf("NewOrLoadStore() produced an error: %v", err)
+	}
+
+	if !reflect.DeepEqual(expectedStore, receivedStore) {
+		t.Errorf("NewOrLoadStore() returned incorrect Store."+
+			"\nexpected: %#v\nreceived: %#v", expectedStore,
+			receivedStore)
+
+	}
+
+}
+
+// Test that it creates a new store if an old one is not in storage.
+func TestNewOrLoadStore_NewStore(t *testing.T) {
+	kv := versioned.NewKV(make(ekv.Memstore))
+
+	receivedStore, err := NewOrLoadStore(kv)
+	if err != nil {
+		t.Fatalf("NewOrLoadStore() produced an error: %v", err)
+	}
+
+	expectedStore := &Store{
+		confirmedFacts:   make(map[fact.Fact]struct{}, 0),
+		unconfirmedFacts: make(map[string]fact.Fact, 0),
+		kv:               kv.Prefix(prefix),
+	}
+
+	if !reflect.DeepEqual(expectedStore, receivedStore) {
+		t.Errorf("NewOrLoadStore() returned incorrect Store."+
+			"\nexpected: %#v\nreceived: %#v", expectedStore,
+			receivedStore)
+
+	}
+
+}
+
+func TestStore_MarshalUnmarshal_ConfirmedFacts(t *testing.T) {
+	kv := versioned.NewKV(make(ekv.Memstore))
+
+	expectedStore, err := NewStore(kv)
+	if err != nil {
+		t.Errorf("NewStore() produced an error: %v", err)
+	}
+
+	data, err := expectedStore.kv.Get(confirmedFactKey, version)
+	if err != nil {
+		t.Errorf("get() error when getting Store from KV: %v", err)
+	}
+
+	expectedData, err := expectedStore.marshalConfirmedFacts()
+	if err != nil {
+		t.Fatalf("marshalConfirmedFact error: %+v", err)
+	}
+
+	if !bytes.Equal(expectedData, data.Data) {
+		t.Errorf("NewStore() returned incorrect Store."+
+			"\nexpected: %+v\nreceived: %+v", expectedData,
+			data.Data)
+	}
+
+	recieved, err := expectedStore.unmarshalConfirmedFacts(data.Data)
+	if err != nil {
+		t.Fatalf("unmarshalUnconfirmedFacts error: %v", err)
+	}
+
+	if !reflect.DeepEqual(recieved, expectedStore.confirmedFacts) {
+		t.Fatalf("Marshal/Unmarshal did not produce identical data"+
+			"\nExpected: %v "+
+			"\nReceived: %v", expectedStore.confirmedFacts, recieved)
+	}
+}
+
+func TestStore_MarshalUnmarshal_UnconfirmedFacts(t *testing.T) {
+	kv := versioned.NewKV(make(ekv.Memstore))
+
+	expectedStore, err := NewStore(kv)
+	if err != nil {
+		t.Errorf("NewStore() produced an error: %v", err)
+	}
+
+	data, err := expectedStore.kv.Get(unconfirmedFactKey, version)
+	if err != nil {
+		t.Errorf("get() error when getting Store from KV: %v", err)
+	}
+
+	expectedData, err := expectedStore.marshalUnconfirmedFacts()
+	if err != nil {
+		t.Fatalf("marshalConfirmedFact error: %+v", err)
+	}
+
+	if !bytes.Equal(expectedData, data.Data) {
+		t.Errorf("NewStore() returned incorrect Store."+
+			"\nexpected: %+v\nreceived: %+v", expectedData,
+			data.Data)
+	}
+
+	recieved, err := expectedStore.unmarshalUnconfirmedFacts(data.Data)
+	if err != nil {
+		t.Fatalf("unmarshalUnconfirmedFacts error: %v", err)
+	}
+
+	if !reflect.DeepEqual(recieved, expectedStore.unconfirmedFacts) {
+		t.Fatalf("Marshal/Unmarshal did not produce identical data"+
+			"\nExpected: %v "+
+			"\nReceived: %v", expectedStore.unconfirmedFacts, recieved)
+	}
+}
-- 
GitLab