diff --git a/go.mod b/go.mod
index 84ed766a410405b2465dc08a5aac8068b2034814..1a876c763bbf43a2ef81a7cecfc7aa9fb032025c 100644
--- a/go.mod
+++ b/go.mod
@@ -18,13 +18,13 @@ require (
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/spf13/viper v1.7.1
 	gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228
-	gitlab.com/elixxir/comms v0.0.3
-	gitlab.com/elixxir/crypto v0.0.5-0.20201109234712-7e64de16970d
+	gitlab.com/elixxir/comms v0.0.4-0.20201116233755-b476dea10095
+	gitlab.com/elixxir/crypto v0.0.5-0.20201118204646-9b23991834c6
 	gitlab.com/elixxir/ekv v0.1.3
 	gitlab.com/elixxir/primitives v0.0.3-0.20201116174806-97f190989704
-	gitlab.com/xx_network/comms v0.0.3
+	gitlab.com/xx_network/comms v0.0.4-0.20201118225304-345dad24bb1e
 	gitlab.com/xx_network/crypto v0.0.4
-	gitlab.com/xx_network/primitives v0.0.2
+	gitlab.com/xx_network/primitives v0.0.3-0.20201116234927-44e42fc91e7c
 	golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
 	google.golang.org/protobuf v1.25.0
 	gopkg.in/ini.v1 v1.61.0 // indirect
diff --git a/go.sum b/go.sum
index 5e5de048d2a894d2bb57d6dcccd3d2cbc5501294..afb0e6e6c207fafa138923b071915ff264c16160 100644
--- a/go.sum
+++ b/go.sum
@@ -252,16 +252,15 @@ github.com/zeebo/pcg v1.0.0 h1:dt+dx+HvX8g7Un32rY9XWoYnd0NmKmrIzpHF7qiTDj0=
 github.com/zeebo/pcg v1.0.0/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
 gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228 h1:Gi6rj4mAlK0BJIk1HIzBVMjWNjIUfstrsXC2VqLYPcA=
 gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228/go.mod h1:H6jztdm0k+wEV2QGK/KYA+MY9nj9Zzatux/qIvDDv3k=
-gitlab.com/elixxir/comms v0.0.3 h1:7cFvBZddX/8JSY5MvfPpg21niV88IpeqQkoKs15erZM=
-gitlab.com/elixxir/comms v0.0.3/go.mod h1:5p7oz4yFrK037rPap6ooaWrloJrzuVZ4jnzOdvgyqnU=
+gitlab.com/elixxir/comms v0.0.4-0.20201116233755-b476dea10095 h1:YptJAYLxqy5CAJIcM9kOwfxmJ2D2A8uXWTT8rgXYG+E=
+gitlab.com/elixxir/comms v0.0.4-0.20201116233755-b476dea10095/go.mod h1:spFKl7jsMy8M6NDvhJ27IJ+CnZ/07JHJCYpYsG8JQ4o=
 gitlab.com/elixxir/crypto v0.0.0-20200804182833-984246dea2c4 h1:28ftZDeYEko7xptCZzeFWS1Iam95dj46TWFVVlKmw6A=
 gitlab.com/elixxir/crypto v0.0.0-20200804182833-984246dea2c4/go.mod h1:ucm9SFKJo+K0N2GwRRpaNr+tKXMIOVWzmyUD0SbOu2c=
 gitlab.com/elixxir/crypto v0.0.3 h1:znCt/x2bL4y8czTPaaFkwzdgSgW3BJc/1+dxyf1jqVw=
 gitlab.com/elixxir/crypto v0.0.3/go.mod h1:ZNgBOblhYToR4m8tj4cMvJ9UsJAUKq+p0gCp07WQmhA=
-gitlab.com/elixxir/crypto v0.0.4 h1:8eWjvUepCU2PiqZM2NFYo6rFg1w8KWO1hMDwMNFEqoI=
-gitlab.com/elixxir/crypto v0.0.4/go.mod h1:ZNgBOblhYToR4m8tj4cMvJ9UsJAUKq+p0gCp07WQmhA=
-gitlab.com/elixxir/crypto v0.0.5-0.20201109234712-7e64de16970d h1:9Peb/peftTVeO5gYqi37sZycMEiu05+2VZ/j8d5lldI=
-gitlab.com/elixxir/crypto v0.0.5-0.20201109234712-7e64de16970d/go.mod h1:ZNgBOblhYToR4m8tj4cMvJ9UsJAUKq+p0gCp07WQmhA=
+gitlab.com/elixxir/crypto v0.0.5-0.20201110193609-6b5e881867b4/go.mod h1:ZNgBOblhYToR4m8tj4cMvJ9UsJAUKq+p0gCp07WQmhA=
+gitlab.com/elixxir/crypto v0.0.5-0.20201118204646-9b23991834c6 h1:HEJHC6gyVMdCZ1PSJkFDScHnsrWAMF+PFxyL2zpNrgU=
+gitlab.com/elixxir/crypto v0.0.5-0.20201118204646-9b23991834c6/go.mod h1:BqvmtLM4eW+3NNOVK7U3COnnxqhJZxdCv4yziCuYhlA=
 gitlab.com/elixxir/ekv v0.1.3 h1:OE+LBMIhjGUMwc6hHJzYvEPNJQV7t1vMnJyIgxUMUo8=
 gitlab.com/elixxir/ekv v0.1.3/go.mod h1:e6WPUt97taFZe5PFLPb1Dupk7tqmDCTQu1kkstqJvw4=
 gitlab.com/elixxir/primitives v0.0.0-20200731184040-494269b53b4d/go.mod h1:OQgUZq7SjnE0b+8+iIAT2eqQF+2IFHn73tOo+aV11mg=
@@ -269,13 +268,12 @@ gitlab.com/elixxir/primitives v0.0.0-20200804170709-a1896d262cd9/go.mod h1:p0Vel
 gitlab.com/elixxir/primitives v0.0.0-20200804182913-788f47bded40/go.mod h1:tzdFFvb1ESmuTCOl1z6+yf6oAICDxH2NPUemVgoNLxc=
 gitlab.com/elixxir/primitives v0.0.1 h1:q61anawANlNAExfkeQEE1NCsNih6vNV1FFLoUQX6txQ=
 gitlab.com/elixxir/primitives v0.0.1/go.mod h1:kNp47yPqja2lHSiS4DddTvFpB/4D9dB2YKnw5c+LJCE=
-gitlab.com/elixxir/primitives v0.0.2 h1:PvyOOp/A6tCtmU7YnGhCCPRdmEogEzCi0Li/WfiVjGo=
-gitlab.com/elixxir/primitives v0.0.2/go.mod h1:3fxFHSlQhkV4vs+S0dZEz3Om3m+40WX8L806yvSnNFc=
 gitlab.com/elixxir/primitives v0.0.3-0.20201116174806-97f190989704 h1:JkFREumz8skDqkCjjzZnlf5tg+PBiMB9kfVn9z0VEfE=
 gitlab.com/elixxir/primitives v0.0.3-0.20201116174806-97f190989704/go.mod h1:3fxFHSlQhkV4vs+S0dZEz3Om3m+40WX8L806yvSnNFc=
 gitlab.com/xx_network/comms v0.0.0-20200805174823-841427dd5023/go.mod h1:owEcxTRl7gsoM8c3RQ5KAm5GstxrJp5tn+6JfQ4z5Hw=
-gitlab.com/xx_network/comms v0.0.3 h1:ch1eJI4WXUE/Kz0Kp9uDWX16B+hfVFmdHY+EOai4Wzc=
-gitlab.com/xx_network/comms v0.0.3/go.mod h1:YViGbRj7FjJYoaO4NpALGEd9dK/l8uUT000FEBbUTL8=
+gitlab.com/xx_network/comms v0.0.4-0.20201110022115-4a6171cad07d/go.mod h1:YViGbRj7FjJYoaO4NpALGEd9dK/l8uUT000FEBbUTL8=
+gitlab.com/xx_network/comms v0.0.4-0.20201118225304-345dad24bb1e h1:sJjDnRQmAmojq64OS3ZIvhDgaY1nTRYAo/3GeR+yyCI=
+gitlab.com/xx_network/comms v0.0.4-0.20201118225304-345dad24bb1e/go.mod h1:YViGbRj7FjJYoaO4NpALGEd9dK/l8uUT000FEBbUTL8=
 gitlab.com/xx_network/crypto v0.0.3/go.mod h1:DF2HYvvCw9wkBybXcXAgQMzX+MiGbFPjwt3t17VRqRE=
 gitlab.com/xx_network/crypto v0.0.4 h1:lpKOL5mTJ2awWMfgBy30oD/UvJVrWZzUimSHlOdZZxo=
 gitlab.com/xx_network/crypto v0.0.4/go.mod h1:+lcQEy+Th4eswFgQDwT0EXKp4AXrlubxalwQFH5O0Mk=
@@ -284,6 +282,8 @@ gitlab.com/xx_network/primitives v0.0.0-20200804183002-f99f7a7284da h1:CCVslUwNC
 gitlab.com/xx_network/primitives v0.0.0-20200804183002-f99f7a7284da/go.mod h1:OK9xevzWCaPO7b1wiluVJGk7R5ZsuC7pHY5hteZFQug=
 gitlab.com/xx_network/primitives v0.0.2 h1:r45yKenJ9e7PylI1ZXJ1Es09oYNaYXjxVy9+uYlwo7Y=
 gitlab.com/xx_network/primitives v0.0.2/go.mod h1:cs0QlFpdMDI6lAo61lDRH2JZz+3aVkHy+QogOB6F/qc=
+gitlab.com/xx_network/primitives v0.0.3-0.20201116234927-44e42fc91e7c h1:mYId667WIN97E6KhPw4HDYyCjWzsG7gCM/HLTNTCXZQ=
+gitlab.com/xx_network/primitives v0.0.3-0.20201116234927-44e42fc91e7c/go.mod h1:cs0QlFpdMDI6lAo61lDRH2JZz+3aVkHy+QogOB6F/qc=
 gitlab.com/xx_network/ring v0.0.2 h1:TlPjlbFdhtJrwvRgIg4ScdngMTaynx/ByHBRZiXCoL0=
 gitlab.com/xx_network/ring v0.0.2/go.mod h1:aLzpP2TiZTQut/PVHR40EJAomzugDdHXetbieRClXIM=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
diff --git a/ud/lookup.go b/ud/lookup.go
index c75c52e83f7e55c55207a32429736eba6564ecfd..9730bedd0068b88b3915429c98283903c089490c 100644
--- a/ud/lookup.go
+++ b/ud/lookup.go
@@ -35,9 +35,9 @@ func (m *Manager) lookupProcess(c chan message.Receive, quitCh <-chan struct{})
 			}
 
 			// Get the appropriate channel from the lookup
-			m.inProgressMux.RLock()
+			m.inProgressLookupMux.RLock()
 			ch, ok := m.inProgressLookup[lookupResponse.CommID]
-			m.inProgressMux.RUnlock()
+			m.inProgressLookupMux.RUnlock()
 			if !ok {
 				jww.WARN.Printf("Dropped a lookup response from user "+
 					"discovery due to unknown comm ID: %d",
@@ -83,9 +83,9 @@ func (m *Manager) Lookup(uid *id.ID, callback lookupCallback, timeout time.Durat
 
 	// Register the request in the response map so it can be processed on return
 	responseChan := make(chan *LookupResponse, 1)
-	m.inProgressMux.Lock()
+	m.inProgressLookupMux.Lock()
 	m.inProgressLookup[commID] = responseChan
-	m.inProgressMux.Unlock()
+	m.inProgressLookupMux.Unlock()
 
 	// Send the request
 	rounds, _, err := m.net.SendE2E(msg, params.GetDefaultE2E())
@@ -113,7 +113,7 @@ func (m *Manager) Lookup(uid *id.ID, callback lookupCallback, timeout time.Durat
 		select {
 		// Return an error if the round fails
 		case <-roundFailChan:
-			err = errors.New("One or more rounds failed to resolved; " +
+			err = errors.New("One or more rounds failed to resolve; " +
 				"lookup not delivered")
 
 		// Return an error if the timeout is reached
@@ -138,9 +138,9 @@ func (m *Manager) Lookup(uid *id.ID, callback lookupCallback, timeout time.Durat
 		}
 
 		// Delete the response channel from the map
-		m.inProgressMux.Lock()
+		m.inProgressLookupMux.Lock()
 		delete(m.inProgressLookup, commID)
-		m.inProgressMux.Unlock()
+		m.inProgressLookupMux.Unlock()
 
 		// Call the callback last in case it is blocking
 		callback(c, err)
diff --git a/ud/lookup_test.go b/ud/lookup_test.go
index 6b18671f7c862f3796834270c80b65b9ac3d630b..791e500468e57f4a3424e0ea8d0646ba45742408 100644
--- a/ud/lookup_test.go
+++ b/ud/lookup_test.go
@@ -28,11 +28,11 @@ import (
 // Happy path.
 func TestManager_Lookup(t *testing.T) {
 	// Set up manager
-	m := Manager{
+	m := &Manager{
 		rng:              fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG),
 		grp:              cyclic.NewGroup(large.NewInt(107), large.NewInt(2)),
 		storage:          storage.InitTestingSession(t),
-		udID:             id.NewIdFromUInt(rand.Uint64(), id.User, t),
+		udID:             &id.UDB,
 		inProgressLookup: map[uint64]chan *LookupResponse{},
 		net:              newTestNetworkManager(t),
 	}
@@ -111,14 +111,14 @@ func TestManager_Lookup(t *testing.T) {
 	}
 }
 
-// Error path: the LookupResponse returns an error.
-func TestManager_Lookup_LookupResponseError(t *testing.T) {
+// Error path: the callback returns an error.
+func TestManager_Lookup_CallbackError(t *testing.T) {
 	// Set up manager
-	m := Manager{
+	m := &Manager{
 		rng:              fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG),
 		grp:              cyclic.NewGroup(large.NewInt(107), large.NewInt(2)),
 		storage:          storage.InitTestingSession(t),
-		udID:             id.NewIdFromUInt(rand.Uint64(), id.User, t),
+		udID:             &id.UDB,
 		inProgressLookup: map[uint64]chan *LookupResponse{},
 		net:              newTestNetworkManager(t),
 	}
@@ -175,11 +175,11 @@ func TestManager_Lookup_LookupResponseError(t *testing.T) {
 // Error path: the round event chan times out.
 func TestManager_Lookup_EventChanTimeout(t *testing.T) {
 	// Set up manager
-	m := Manager{
+	m := &Manager{
 		rng:              fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG),
 		grp:              cyclic.NewGroup(large.NewInt(107), large.NewInt(2)),
 		storage:          storage.InitTestingSession(t),
-		udID:             id.NewIdFromUInt(rand.Uint64(), id.User, t),
+		udID:             &id.UDB,
 		inProgressLookup: map[uint64]chan *LookupResponse{},
 		net:              newTestNetworkManager(t),
 	}
@@ -226,11 +226,11 @@ func TestManager_Lookup_EventChanTimeout(t *testing.T) {
 
 // Happy path.
 func TestManager_lookupProcess(t *testing.T) {
-	m := Manager{
+	m := &Manager{
 		rng:              fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG),
 		grp:              cyclic.NewGroup(large.NewInt(107), large.NewInt(2)),
 		storage:          storage.InitTestingSession(t),
-		udID:             id.NewIdFromUInt(rand.Uint64(), id.User, t),
+		udID:             &id.UDB,
 		inProgressLookup: map[uint64]chan *LookupResponse{},
 		net:              newTestNetworkManager(t),
 	}
@@ -280,11 +280,11 @@ func TestManager_lookupProcess(t *testing.T) {
 
 // Error path: dropped lookup response due to incorrect message.Receive.
 func TestManager_lookupProcess_NoLookupResponse(t *testing.T) {
-	m := Manager{
+	m := &Manager{
 		rng:              fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG),
 		grp:              cyclic.NewGroup(large.NewInt(107), large.NewInt(2)),
 		storage:          storage.InitTestingSession(t),
-		udID:             id.NewIdFromUInt(rand.Uint64(), id.User, t),
+		udID:             &id.UDB,
 		inProgressLookup: map[uint64]chan *LookupResponse{},
 		net:              newTestNetworkManager(t),
 	}
diff --git a/ud/manager.go b/ud/manager.go
index 10dcc2a8131fb64b6ad4bcc5dbc76922cf6f30cd..fac9e1eb85a4350d6e5445d464b541cbd0dda503 100644
--- a/ud/manager.go
+++ b/ud/manager.go
@@ -25,8 +25,11 @@ type Manager struct {
 
 	udID *id.ID
 
-	inProgressLookup map[uint64]chan *LookupResponse
-	inProgressMux    sync.RWMutex
+	inProgressLookup    map[uint64]chan *LookupResponse
+	inProgressLookupMux sync.RWMutex
+
+	inProgressSearch    map[uint64]chan *SearchResponse
+	inProgressSearchMux sync.Mutex
 
 	net interfaces.NetworkManager
 
diff --git a/ud/remove.go b/ud/remove.go
index 6d7994d662143f06281a11228fb00cf85cbba207..ae9e555147571ddae87cbccf816f877226817178 100644
--- a/ud/remove.go
+++ b/ud/remove.go
@@ -4,9 +4,9 @@ import (
 	"crypto/rand"
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/ptypes/any"
-	"gitlab.com/elixxir/client/interfaces/contact"
 	"gitlab.com/elixxir/comms/mixmessages"
 	"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"
@@ -16,11 +16,11 @@ type removeFactComms interface {
 	SendDeleteMessage(host *connect.Host, message *messages.AuthenticatedMessage) (*messages.Ack, error)
 }
 
-func (m *Manager) RemoveFact(fact contact.Fact) error {
+func (m *Manager) RemoveFact(fact fact.Fact) error {
 	return m.removeFact(fact, nil)
 }
 
-func (m *Manager) removeFact(fact contact.Fact, rFC removeFactComms) error {
+func (m *Manager) removeFact(fact fact.Fact, rFC removeFactComms) error {
 	// Construct the message to send
 	// Convert our Fact to a mixmessages Fact for sending
 	mmFact := mixmessages.Fact{
diff --git a/ud/remove_test.go b/ud/remove_test.go
index 7ec08c4e28542cb036e2e9c78afe6e5d39314b39..68ff8fd0b3910d0c53e05fcab4b5462a048623fe 100644
--- a/ud/remove_test.go
+++ b/ud/remove_test.go
@@ -1,9 +1,9 @@
 package ud
 
 import (
-	"gitlab.com/elixxir/client/interfaces/contact"
 	"gitlab.com/elixxir/comms/client"
 	"gitlab.com/elixxir/crypto/cyclic"
+	"gitlab.com/elixxir/primitives/fact"
 	"gitlab.com/xx_network/comms/connect"
 	"gitlab.com/xx_network/comms/messages"
 	"gitlab.com/xx_network/crypto/csprng"
@@ -60,7 +60,7 @@ func TestRemoveFact(t *testing.T) {
 		privKey: cpk,
 	}
 
-	f := contact.Fact{
+	f := fact.Fact{
 		Fact: "testing",
 		T:    2,
 	}
diff --git a/ud/search.go b/ud/search.go
new file mode 100644
index 0000000000000000000000000000000000000000..6dc632aa059d709f19ca7ae8666ad1f270d42613
--- /dev/null
+++ b/ud/search.go
@@ -0,0 +1,152 @@
+package ud
+
+import (
+	"github.com/golang/protobuf/proto"
+	"github.com/pkg/errors"
+	"gitlab.com/elixxir/client/interfaces/contact"
+	"gitlab.com/elixxir/client/interfaces/message"
+	"gitlab.com/elixxir/client/interfaces/params"
+	"gitlab.com/elixxir/comms/network/dataStructures"
+	"gitlab.com/elixxir/crypto/factID"
+	"gitlab.com/elixxir/primitives/fact"
+	"gitlab.com/elixxir/primitives/states"
+	"gitlab.com/xx_network/primitives/id"
+	"time"
+)
+
+type searchCallback func([]contact.Contact, error)
+
+// Search...
+func (m *Manager) Search(list fact.FactList, callback searchCallback, timeout time.Duration) error {
+	// Get the ID of this comm so it can be connected to its response
+	commID := m.getCommID()
+
+	factHashes, factMap := hashFactList(list)
+
+	// Build the request
+	request := &SearchSend{
+		Fact:   factHashes,
+		CommID: commID,
+	}
+
+	requestMarshaled, err := proto.Marshal(request)
+	if err != nil {
+		return errors.WithMessage(err, "Failed to form outgoing search request")
+	}
+
+	msg := message.Send{
+		Recipient:   m.udID,
+		Payload:     requestMarshaled,
+		MessageType: message.UdSearch,
+	}
+
+	// Register the request in the response map so it can be processed on return
+	responseChan := make(chan *SearchResponse)
+	m.inProgressSearchMux.Lock()
+	m.inProgressSearch[commID] = responseChan
+	m.inProgressSearchMux.Unlock()
+
+	// Send the request
+	rounds, _, err := m.net.SendE2E(msg, params.GetDefaultE2E())
+	if err != nil {
+		return errors.WithMessage(err, "Failed to send the search request")
+	}
+
+	// Register the round event to capture if the round fails
+	roundFailChan := make(chan dataStructures.EventReturn, len(rounds))
+
+	for _, round := range rounds {
+		// Subtract a millisecond to ensure this timeout will trigger before the
+		// one below
+		m.net.GetInstance().GetRoundEvents().AddRoundEventChan(round,
+			roundFailChan, timeout-1*time.Millisecond, states.FAILED)
+	}
+
+	// Start the go routine which will trigger the callback
+	go func() {
+		timer := time.NewTimer(timeout)
+
+		var err error
+		var c []contact.Contact
+
+		select {
+		// Return an error if the round fails
+		case <-roundFailChan:
+			err = errors.New("One or more rounds failed to resolve; " +
+				"search not delivered")
+
+		// Return an error if the timeout is reached
+		case <-timer.C:
+			err = errors.New("Response from User Discovery did not come " +
+				"before timeout")
+
+		// Return the contacts if one is returned
+		case response := <-responseChan:
+			if response.Error != "" {
+				err = errors.Errorf("User Discovery returned an error on "+
+					"search: %s", response.Error)
+			} else {
+				c, err = m.parseContacts(response.Contacts, factMap)
+			}
+		}
+
+		// Delete the response channel from the map
+		m.inProgressSearchMux.Lock()
+		delete(m.inProgressSearch, commID)
+		m.inProgressSearchMux.Unlock()
+
+		// Call the callback last in case it is blocking
+		callback(c, err)
+	}()
+
+	return nil
+}
+
+// hashFactList hashes each fact in the FactList into a HashFact and returns a
+// slice of the HashFacts. Also returns a map of Facts keyed on fact hashes to
+// be used for the callback return.
+func hashFactList(list fact.FactList) ([]*HashFact, map[string]fact.Fact) {
+	hashes := make([]*HashFact, len(list))
+	hashMap := make(map[string]fact.Fact, len(list))
+
+	for i, f := range list {
+		hashes[i] = &HashFact{
+			Hash: factID.Fingerprint(f),
+			Type: int32(f.T),
+		}
+		hashMap[string(factID.Fingerprint(f))] = f
+	}
+
+	return hashes, hashMap
+}
+
+// parseContacts parses the list of Contacts in the SearchResponse and returns a
+// list of contact.Contact with their ID and public key.
+func (m *Manager) parseContacts(response []*Contact, hashMap map[string]fact.Fact) ([]contact.Contact, error) {
+	contacts := make([]contact.Contact, len(response))
+
+	// Convert each contact message into a new contact.Contact
+	for i, c := range response {
+		// Unmarshal user ID bytes
+		uid, err := id.Unmarshal(c.UserID)
+		if err != nil {
+			return nil, errors.Errorf("Failed to parse Contact user ID: %+v", err)
+		}
+
+		// Create new Contact
+		contacts[i] = contact.Contact{
+			ID:       uid,
+			DhPubKey: m.grp.NewIntFromBytes(c.PubKey),
+			Facts:    []fact.Fact{},
+		}
+
+		// Assign each Fact with a matching hash to the Contact
+		for _, hashFact := range c.TrigFacts {
+			if f, exists := hashMap[string(hashFact.Hash)]; exists {
+				contacts[i].Facts = append(contacts[i].Facts, f)
+			}
+		}
+	}
+
+	return contacts, nil
+}
diff --git a/ud/search_test.go b/ud/search_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..ee6b77658bd8ebe8fb176514f9698052bc0d4e32
--- /dev/null
+++ b/ud/search_test.go
@@ -0,0 +1,244 @@
+package ud
+
+import (
+	"github.com/golang/protobuf/proto"
+	"gitlab.com/elixxir/client/interfaces/contact"
+	"gitlab.com/elixxir/client/interfaces/message"
+	"gitlab.com/elixxir/client/storage"
+	"gitlab.com/elixxir/crypto/cyclic"
+	"gitlab.com/elixxir/crypto/factID"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	"gitlab.com/elixxir/primitives/fact"
+	"gitlab.com/xx_network/crypto/csprng"
+	"gitlab.com/xx_network/crypto/large"
+	"gitlab.com/xx_network/primitives/id"
+	"reflect"
+	"testing"
+	"time"
+)
+
+// Happy path.
+func TestManager_Search(t *testing.T) {
+	// Set up manager
+	m := &Manager{
+		rng:              fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG),
+		grp:              cyclic.NewGroup(large.NewInt(107), large.NewInt(2)),
+		storage:          storage.InitTestingSession(t),
+		udID:             &id.UDB,
+		inProgressSearch: map[uint64]chan *SearchResponse{},
+		net:              newTestNetworkManager(t),
+	}
+
+	// Generate callback function
+	callbackChan := make(chan struct {
+		c   []contact.Contact
+		err error
+	})
+	callback := func(c []contact.Contact, err error) {
+		callbackChan <- struct {
+			c   []contact.Contact
+			err error
+		}{c: c, err: err}
+	}
+
+	// Generate fact list
+	factList := fact.FactList{
+		{Fact: "fact1", T: fact.Username},
+		{Fact: "fact2", T: fact.Email},
+		{Fact: "fact3", T: fact.Phone},
+	}
+
+	// Trigger lookup response chan
+	responseContacts := []*Contact{
+		{
+			UserID: id.NewIdFromUInt(5, id.User, t).Bytes(),
+			PubKey: []byte{42},
+			TrigFacts: []*HashFact{
+				{Hash: factID.Fingerprint(factList[0]), Type: int32(factList[0].T)},
+				{Hash: factID.Fingerprint(factList[1]), Type: int32(factList[1].T)},
+				{Hash: factID.Fingerprint(factList[2]), Type: int32(factList[2].T)},
+			},
+		},
+	}
+	go func() {
+		time.Sleep(1 * time.Millisecond)
+		m.inProgressSearch[0] <- &SearchResponse{
+			Contacts: responseContacts,
+			Error:    "",
+		}
+	}()
+
+	// Run the search
+	err := m.Search(factList, callback, 20*time.Millisecond)
+	if err != nil {
+		t.Errorf("Search() returned an error: %+v", err)
+	}
+
+	// Generate expected Send message
+	factHashes, factMap := hashFactList(factList)
+	payload, err := proto.Marshal(&SearchSend{
+		Fact:   factHashes,
+		CommID: m.commID - 1,
+	})
+	if err != nil {
+		t.Fatalf("Failed to marshal SearchSend: %+v", err)
+	}
+	expectedMsg := message.Send{
+		Recipient:   m.udID,
+		Payload:     payload,
+		MessageType: message.UdSearch,
+	}
+
+	// Verify the message is correct
+	if !reflect.DeepEqual(expectedMsg, m.net.(*testNetworkManager).msg) {
+		t.Errorf("Failed to send correct message."+
+			"\n\texpected: %+v\n\treceived: %+v",
+			expectedMsg, m.net.(*testNetworkManager).msg)
+	}
+
+	// Verify the callback is called
+	select {
+	case cb := <-callbackChan:
+		if cb.err != nil {
+			t.Errorf("Callback returned an error: %+v", cb.err)
+		}
+
+		expectedContacts, err := m.parseContacts(responseContacts, factMap)
+		if err != nil {
+			t.Fatalf("parseResponseContacts() returned an error: %+v", err)
+		}
+		if !reflect.DeepEqual(expectedContacts, cb.c) {
+			t.Errorf("Failed to get expected Contacts."+
+				"\n\texpected: %v\n\treceived: %v", expectedContacts, cb.c)
+		}
+	case <-time.After(100 * time.Millisecond):
+		t.Error("Callback not called.")
+	}
+
+	if _, exists := m.inProgressSearch[m.commID-1]; exists {
+		t.Error("Failed to delete SearchResponse from inProgressSearch.")
+	}
+}
+
+// Error path: the callback returns an error.
+func TestManager_Search_CallbackError(t *testing.T) {
+	// Set up manager
+	m := &Manager{
+		rng:              fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG),
+		grp:              cyclic.NewGroup(large.NewInt(107), large.NewInt(2)),
+		storage:          storage.InitTestingSession(t),
+		udID:             &id.UDB,
+		inProgressSearch: map[uint64]chan *SearchResponse{},
+		net:              newTestNetworkManager(t),
+	}
+
+	// Generate callback function
+	callbackChan := make(chan struct {
+		c   []contact.Contact
+		err error
+	})
+	callback := func(c []contact.Contact, err error) {
+		callbackChan <- struct {
+			c   []contact.Contact
+			err error
+		}{c: c, err: err}
+	}
+
+	// Generate fact list
+	factList := fact.FactList{
+		{Fact: "fact1", T: fact.Username},
+		{Fact: "fact2", T: fact.Email},
+		{Fact: "fact3", T: fact.Phone},
+	}
+
+	// Trigger lookup response chan
+	go func() {
+		time.Sleep(1 * time.Millisecond)
+		m.inProgressSearch[0] <- &SearchResponse{
+			Contacts: nil,
+			Error:    "Error",
+		}
+	}()
+
+	// Run the search
+	err := m.Search(factList, callback, 10*time.Millisecond)
+	if err != nil {
+		t.Errorf("Search() returned an error: %+v", err)
+	}
+
+	// Verify the callback is called
+	select {
+	case cb := <-callbackChan:
+		if cb.err == nil {
+			t.Error("Callback did not return an expected error.")
+		}
+
+		if cb.c != nil {
+			t.Errorf("Failed to get expected Contacts."+
+				"\n\texpected: %v\n\treceived: %v", nil, cb.c)
+		}
+	case <-time.After(100 * time.Millisecond):
+		t.Error("Callback not called.")
+	}
+
+	if _, exists := m.inProgressSearch[m.commID-1]; exists {
+		t.Error("Failed to delete SearchResponse from inProgressSearch.")
+	}
+}
+
+// Error path: the round event chan times out.
+func TestManager_Search_EventChanTimeout(t *testing.T) {
+	// Set up manager
+	m := &Manager{
+		rng:              fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG),
+		grp:              cyclic.NewGroup(large.NewInt(107), large.NewInt(2)),
+		storage:          storage.InitTestingSession(t),
+		udID:             &id.UDB,
+		inProgressSearch: map[uint64]chan *SearchResponse{},
+		net:              newTestNetworkManager(t),
+	}
+
+	// Generate callback function
+	callbackChan := make(chan struct {
+		c   []contact.Contact
+		err error
+	})
+	callback := func(c []contact.Contact, err error) {
+		callbackChan <- struct {
+			c   []contact.Contact
+			err error
+		}{c: c, err: err}
+	}
+
+	// Generate fact list
+	factList := fact.FactList{
+		{Fact: "fact1", T: fact.Username},
+		{Fact: "fact2", T: fact.Email},
+		{Fact: "fact3", T: fact.Phone},
+	}
+
+	// Run the search
+	err := m.Search(factList, callback, 10*time.Millisecond)
+	if err != nil {
+		t.Errorf("Search() returned an error: %+v", err)
+	}
+
+	// Verify the callback is called
+	select {
+	case cb := <-callbackChan:
+		if cb.err == nil {
+			t.Error("Callback did not return an expected error.")
+		}
+
+		if cb.c != nil {
+			t.Errorf("Failed to get expected Contacts."+
+				"\n\texpected: %v\n\treceived: %v", nil, cb.c)
+		}
+	case <-time.After(100 * time.Millisecond):
+		t.Error("Callback not called.")
+	}
+
+	if _, exists := m.inProgressSearch[m.commID-1]; exists {
+		t.Error("Failed to delete SearchResponse from inProgressSearch.")
+	}
+}