Skip to Content
Contact Discovery

Contact Discovery

How Zentalk enables users to find and connect with contacts while preserving privacy.


Overview

Traditional messaging platforms require phone numbers, email addresses, or centralized username registries to discover contacts. Zentalk takes a fundamentally different approach, enabling contact discovery without revealing personal identifiers or building a social graph on any server.

ApproachZentalkTraditional Apps
Phone number requiredNoYes (Signal, WhatsApp)
Email requiredNoSometimes (Telegram)
Address book uploadNeverCommon practice
Server knows contactsNoYes
Centralized registryNoYes

Discovery Methods

Direct Address Sharing

The primary method for contact discovery is direct sharing of wallet addresses through secure channels.

QR Code Sharing

Users can generate a QR code containing their contact information:

QR Code ContentsDescription
Wallet addressPrimary identifier (zk1...)
Display name (optional)Human-readable name
Identity key fingerprintFor verification
Protocol versionCompatibility indicator
QR Code Payload Structure: zentalk://contact? addr=zk1qxy7f8h9j2k3m4n5p6q7r8s9t0u1v2w3x4y5z6 &name=Alice &fp=37291847561928304857 &v=1

Link Sharing

For digital sharing, users can copy a contact link:

https://zentalk.app/add/zk1qxy7f8h9j2k3m4n5p6q7r8s9t0u1v2w3x4y5z6
Sharing MethodSecurity LevelUse Case
In-person QR scanHighestFace-to-face meetings
Secure messenger linkHighAlready trusted channel
Email linkMediumBusiness contacts
Social mediaLowerPublic profile sharing
NFC tapHighQuick in-person exchange

Username Lookup (Optional)

Zentalk supports optional username registration for discoverability.

How Username Registration Works

StepActionPrivacy Consideration
1User chooses usernameNot linked to real identity
2Sign username with identity keyProves ownership
3Submit to DHTDecentralized storage
4Username maps to wallet addressPublic mapping

Username Properties

PropertyValue
Format3-32 alphanumeric characters
Case sensitivityCase-insensitive (stored lowercase)
UniquenessGlobally unique
RegistrationOne per wallet address
RevocationPossible with identity key signature
ExpirationNone (permanent until revoked)
Username Registration Flow: 1. User selects username: "alice_z" 2. Client creates registration message: { username: "alice_z", wallet_address: "zk1...", timestamp: 1704067200, signature: sign(identity_key, username || address || timestamp) } 3. Submit to DHT with key = hash("zentalk:username:" || "alice_z") 4. DHT stores mapping, verifiable by anyone

Username Lookup Process

StepActionData Exposed
1User searches for “alice_z”Username only
2Query DHT for hash(“zentalk:username:alice_z”)None (hashed lookup)
3Retrieve wallet addressPublic mapping
4Fetch identity key bundleStandard key exchange

No Phone/Email Required

Zentalk deliberately avoids phone number or email-based discovery.

Problem AvoidedDescription
SIM swap attacksNo phone number means no SIM vulnerability
Carrier surveillanceNo telecom metadata
Email trackingNo email provider involvement
Real identity linkageNo government-issued identifiers
Contact graph leakageNo address book analysis

Privacy-Preserving Contact Sync

Why NOT to Upload Address Book

Many messaging apps request access to your address book and upload contacts to their servers. This creates serious privacy problems.

What Address Book Upload Reveals

Data ExposedPrivacy Impact
All contact phone numbersServer knows your entire social network
Contact namesReal names linked to numbers
Relationship patternsWho you know, how you organize them
Non-user contactsPrivacy of people who never consented
Contact frequencyMetadata about communication patterns

Attack Scenarios from Contact Upload

AttackDescription
Social graph analysisMap relationships between all users
De-anonymizationLink pseudonymous accounts via mutual contacts
SurveillanceGovernment requests for contact lists
Data breachStolen contact databases
Advertising profilesSell social graph to advertisers

Zentalk’s Position

Zentalk never requests or uploads address book data. Contact discovery is purely opt-in and user-initiated.

Hash-Based Private Set Intersection (Theoretical)

While Zentalk does not implement contact sync, this section explains how privacy-preserving contact discovery could work using cryptographic techniques, for educational purposes.

Private Set Intersection (PSI) Overview

PSI allows two parties to find common elements in their sets without revealing non-matching elements.

ParticipantInputLearns
ClientOwn contact listWhich contacts use Zentalk
ServerAll user identifiersNothing about client’s contacts

Hash-Based PSI Protocol

Simplified PSI Protocol: Client: 1. contacts = ["alice@email.com", "bob@email.com", ...] 2. hashed_contacts = [H(c || client_salt) for c in contacts] 3. blind hashed_contacts with random values 4. send blinded_hashes to server Server: 1. users = [all registered user identifiers] 2. hashed_users = [H(u || server_salt) for u in users] 3. perform PSI computation on blinded_hashes 4. return blinded results Client: 1. unblind results 2. compare with own hashed_contacts 3. matches = contacts that are Zentalk users

What Server Learns: Nothing

InformationServer Knowledge
Number of contactsHidden by padding
Contact identifiersHidden by hashing + blinding
Which contacts matchedHidden by blinding
Non-matching contactsNever revealed

Why Zentalk Doesn’t Implement PSI

ReasonExplanation
PhilosophyZentalk avoids phone/email identifiers entirely
Trust modelAny server interaction is potential metadata
SimplicityDirect address sharing is simpler and safer
User controlManual contact addition is more intentional

Adding Contacts

Scanning QR Code

The recommended method for adding contacts in person.

QR Scan Process

StepActionVerification
1Open QR scannerCamera permission required
2Scan contact’s QR codeParse zentalk:// URI
3Display contact infoShow name, address preview
4Verify fingerprint (optional)Compare with spoken fingerprint
5Confirm additionUser approves
6Send contact requestEncrypted request message
QR Code Parsing: INPUT: Camera captures QR code image OUTPUT: Contact information structure 1. Decode QR code to string 2. Parse URI: zentalk://contact?addr=...&name=...&fp=... 3. Validate wallet address format 4. Validate fingerprint format (if present) 5. Display to user for confirmation

Security Considerations

RiskMitigation
Malicious QR codeValidate address format strictly
Phishing QR codeShow address clearly before adding
Camera hijackingRequest camera permission only when needed
QR code substitutionVerify fingerprint verbally

Entering Wallet Address

For contacts shared via text or other digital means.

Manual Entry Process

StepActionValidation
1Open “Add Contact”Navigate to contact addition
2Enter wallet addresszk1qxy7f8...
3Validate formatCheck prefix, length, checksum
4Lookup identity keyQuery DHT for key bundle
5Display contact infoShow available metadata
6Confirm additionUser approves

Address Validation

CheckDescriptionError Message
PrefixMust start with zk1”Invalid address format”
Length42 characters total”Address too short/long”
ChecksumBech32 checksum valid”Invalid checksum”
CharactersValid Bech32 alphabet”Invalid characters”
Address Validation: FUNCTION validate_zentalk_address(address): IF NOT address.startswith("zk1"): RETURN error("Invalid prefix") IF length(address) != 42: RETURN error("Invalid length") IF NOT bech32_checksum_valid(address): RETURN error("Checksum failed") IF NOT all_valid_bech32_chars(address): RETURN error("Invalid characters") RETURN success

Accepting Contact Request

When someone adds you, you receive a contact request.

Contact Request UI Flow

StateUser ActionSystem Response
Request receivedNotification shown”New contact request from zk1…”
View requestOpen request detailsShow sender address, message
AcceptTap “Accept”Add to contacts, notify sender
RejectTap “Reject”Silently discard
BlockTap “Block”Add to block list

Request Decision Factors

Information AvailableUse
Wallet addressRecognize if previously known
Display nameClaimed name (unverified)
Optional messageContext for request
Mutual contacts”Known by Alice, Bob” (if enabled)

Verification Prompt

After adding a contact, Zentalk prompts for verification.

Prompt TypeTriggerUser Action
First messageBefore sending first messageOption to verify safety number
Unverified warningSending to unverified contactDismiss or verify
Key change alertContact’s key changedRe-verify required
Verification Prompt Flow: WHEN user sends first message to unverified contact: SHOW dialog: "You haven't verified [Contact Name]'s identity. For sensitive conversations, compare safety numbers in person or via a trusted channel. [Verify Now] [Send Anyway] [Cancel]"

Contact Requests

Request Format

Contact requests are encrypted messages with a specific structure.

Request Message Structure

FieldTypeDescription
typeuint8Message type (0x01 = contact request)
sender_addressbytesSender’s wallet address
sender_namestringOptional display name
messagestringOptional introduction message
timestampuint64Unix timestamp
pow_nonceuint64Proof-of-work nonce
signaturebytesEd25519 signature
Contact Request Wire Format: +--------+----------------+-------------+-------------+ | type | sender_address | sender_name | message | | 1 byte | 32 bytes | var (≤64) | var (≤256) | +--------+----------------+-------------+-------------+ | timestamp | pow_nonce | signature | | 8 bytes | 8 bytes | 64 bytes | +------------------+------------------------------------+

Encryption of Contact Request

LayerEncryptionPurpose
TransportTLS 1.3Server cannot read
MessageX3DH + AES-GCMRecipient-only decryption
Onion3-hop relay routingHide sender IP

Spam Prevention

Contact requests include anti-spam mechanisms.

Proof-of-Work Requirement

ParameterValueRationale
AlgorithmSHA-256Widely supported
Difficulty20 leading zero bits~1 second on average
Target adjustmentNone (fixed)Simplicity
ScopePer-requestFresh PoW each time
Proof-of-Work Verification: FUNCTION verify_contact_request_pow(request): challenge = hash( request.sender_address || request.recipient_address || request.timestamp || request.pow_nonce ) // Check for 20 leading zero bits IF leading_zeros(challenge) ≥ 20: RETURN valid ELSE: RETURN invalid

PoW Difficulty Analysis

DeviceTime to ComputeRequests/Day Feasible
Modern smartphone~1 second~86,400
Desktop computer~0.5 seconds~172,800
Spammer (GPU)~0.01 seconds~8,640,000
Spammer (ASIC)Impractical (not SHA-256 mining)N/A

Rate Limiting

Limit TypeThresholdResponse
Per-sender10 requests/hourReject additional
Per-recipient100 requests/hourQueue overflow
GlobalNetwork-level limitsDHT rate limiting

Combined Spam Protection

LayerProtectionBypass Difficulty
PoWComputational costGPU/ASIC investment
Rate limitingTemporal costMultiple identities
Client filteringLocal rulesNone (client control)
Block listsPermanent exclusionNew identity creation

Accept/Reject/Block Options

Users have full control over incoming contact requests.

Action Comparison

ActionEffect on SenderEffect on RecipientReversible
AcceptNotified of acceptanceAdded to contactsYes (remove contact)
RejectNo notificationRequest discardedN/A
BlockNo notificationAll future requests ignoredYes (unblock)

Block Persistence

ScopeDurationCleared By
SessionUntil app restartApp restart
PermanentUntil manual unblockUser action
Cross-deviceSynced via encrypted storageSync to all devices

Request Expiration

Contact requests have a limited lifetime.

ParameterValueRationale
Default expiration7 daysBalance availability and storage
Maximum extension30 daysPrevent indefinite storage
Sender notificationNonePrivacy (don’t confirm existence)
Storage locationRecipient’s pending queueEncrypted, local
Request Lifecycle: 1. Sender creates request with timestamp T 2. Request encrypted and sent via mesh network 3. Recipient's device stores in pending queue 4. After 7 days: request auto-deleted if not acted upon 5. No notification to sender (prevents existence confirmation)

Contact Verification

Safety Number Comparison

Safety numbers provide a cryptographic proof that you’re communicating with the intended person.

Safety Number Generation

InputDescription
Your identity keyYour Ed25519 public key
Contact’s identity keyTheir Ed25519 public key
Domain separator”ZentalkSafetyNumber”
Safety Number Derivation: FUNCTION generate_safety_number(my_identity_key, their_identity_key): // Sort keys for consistency (both parties see same number) IF my_identity_key < their_identity_key: combined = my_identity_key || their_identity_key ELSE: combined = their_identity_key || my_identity_key // Derive safety number hash = SHA-256(combined || "ZentalkSafetyNumber") // Format as readable number safety_number = format_as_numeric_blocks(hash) RETURN safety_number // e.g., "37291 84756 19283 04857 29184 73829"

Safety Number Properties

PropertyDescription
Unique per pairDifferent for Alice-Bob vs Alice-Carol
Order independentAlice sees same number as Bob
60 digitsHigh collision resistance
Numeric onlyEasy to read aloud

Verification Methods

MethodSecurityConvenienceRecommended For
In-person comparisonHighestLowHigh-risk contacts
Video callHighMediumRemote verification
Phone callMediumMediumVoice-familiar contacts
Trusted intermediaryMediumMediumMutual contacts vouch
Screenshot exchangeLowerHighLow-risk contacts

In-Person QR Verification

The most secure verification method involves scanning QR codes in person.

QR Verification Flow

StepAliceBob
1Opens verification screenOpens verification screen
2Shows QR codeScans Alice’s QR code
3Scans Bob’s QR codeShows QR code
4Both see “Verified”Both see “Verified”

QR Verification Code Contents

FieldPurpose
Identity keyFull public key for verification
Safety numberRedundant check
SignatureProves QR generated by key owner
QR Verification Process: ALICE'S DEVICE: qr_content = { identity_key: alice_identity_public_key, safety_number: compute_safety_number(alice, bob), timestamp: current_time(), signature: sign(alice_identity_private_key, above_fields) } DISPLAY qr_code(qr_content) BOB'S DEVICE: scanned = scan_qr_code() VERIFY signature with scanned.identity_key VERIFY safety_number matches locally computed value VERIFY timestamp is recent (within 5 minutes) IF all valid: MARK alice as verified DISPLAY "Verification successful"

Verified Badge Display

Verified contacts are visually distinguished in the UI.

Verification States

StateBadgeMeaning
UnverifiedNoneNever verified
VerifiedCheckmarkSafety numbers confirmed
Verification expiredWarningKey changed since verification
Re-verification neededAlertMust re-verify

Badge Display Locations

LocationDisplay
Contact listSmall badge next to name
Conversation headerBadge with verification date
Contact detailsFull verification status
Message inputWarning if unverified

Re-Verification After Key Change

When a contact’s identity key changes, previous verification is invalidated.

Key Change Detection

TriggerAction
New identity key receivedCompare with stored key
Mismatch detectedAlert user immediately
Session pausedCannot send until acknowledged

Key Change Alert

Key Change Warning Dialog: "Security Alert: [Contact Name]'s identity key has changed. This could mean: - They reinstalled Zentalk - They switched to a new device - They restored from backup - Someone is intercepting your messages Your previous verification is no longer valid. [View New Safety Number] [Block Contact] [Continue Unverified]"

Re-Verification Requirement

ScenarioPrevious StatusNew StatusRequired Action
Key changeVerifiedUnverifiedRe-verify to restore
Key changeUnverifiedUnverifiedAcknowledge warning
Device additionVerifiedVerifiedNo change (multi-device)
RecoveryVerifiedUnverifiedRe-verify (new key)

Contact Storage

Encrypted Contact List

Contacts are stored locally with full encryption.

Contact Record Structure

FieldEncryptedIndexedDescription
contact_idNoYesUnique identifier
wallet_addressYesHashedContact’s address
display_nameYesNoUser-assigned name
nicknameYesNoOptional nickname
notesYesNoUser notes
identity_keyYesNoTheir public key
verified_atYesNoVerification timestamp
added_atNoYesWhen contact was added
last_message_atNoYesFor sorting

Contact Encryption

Contact Storage Encryption: FUNCTION store_contact(contact, metadata_storage_key): // Derive per-contact key contact_key = HKDF( ikm = metadata_storage_key, salt = contact.contact_id, info = "zentalk-contact-encrypt" ) // Encrypt sensitive fields encrypted_data = AES_GCM_Encrypt( key = contact_key, plaintext = serialize( wallet_address, display_name, nickname, notes, identity_key, verified_at ), aad = contact.contact_id ) // Store with plaintext indexes IndexedDB.put("contacts", { contact_id: contact.contact_id, added_at: contact.added_at, last_message_at: contact.last_message_at, address_hash: hash(contact.wallet_address), encrypted_data: encrypted_data })

Stored on Mesh (Not Server)

Contact lists are stored on the decentralized mesh network, not on central servers.

Mesh Storage Architecture

ComponentLocationEncryption
Contact listUser’s mesh storage spaceUser’s storage key
Sync metadataDHTEncrypted pointers
Contact detailsDistributed across nodesPer-contact encryption

Mesh vs Server Storage

AspectMesh StorageServer Storage
ControlUser owns dataServer owns data
AvailabilityDistributed redundancyServer uptime dependent
PrivacyNo single entity has all dataServer sees encrypted blobs
CensorshipResistantSingle point of control
Legal requestsNo single targetSubpoena to server operator

Sync Across Devices

Contacts synchronize across all user devices.

Multi-Device Contact Sync

StepActionEncryption
1Contact added on Device AEncrypted with storage key
2Uploaded to mesh storageE2E encrypted
3Device B polls for updatesAuthenticated request
4Device B downloads and decryptsSame storage key
5Local database updatedRe-encrypted locally
Contact Sync Protocol: DEVICE A (source): 1. Add new contact locally 2. Encrypt contact with user's storage key 3. Create sync record: { type: "contact_add", encrypted_contact: ..., timestamp: ..., device_id: device_a_id } 4. Sign with device key 5. Upload to mesh storage at user's sync location DEVICE B (destination): 1. Poll sync location for new records 2. Verify signature from known device 3. Decrypt contact with storage key 4. Add to local database 5. Acknowledge sync (prevent re-download)

Conflict Resolution

Conflict TypeResolution
Same contact, different namesMost recent timestamp wins
Deleted on A, modified on BDeletion takes precedence
Verification status differsVerified takes precedence
Notes differMerge or most recent

Contact Metadata

Additional metadata users can store about contacts.

Available Metadata Fields

FieldTypeMax LengthDescription
NicknameString64 charsAlternate display name
NotesString4096 charsFree-form notes
LabelsArray10 labelsOrganization tags
Custom fieldsKey-value20 fieldsUser-defined metadata
Profile photoBlob1 MBContact’s photo (if shared)
Trust levelEnumN/APersonal trust assessment

Metadata Privacy

DataStored WhereVisible To
NicknameLocal onlyYou only
NotesLocal onlyYou only
LabelsLocal onlyYou only
Profile photoShared by contactYou + contact
Trust levelLocal onlyYou only

Blocking and Reporting

Block Mechanism (Client-Side)

Blocking is implemented entirely on the client side for maximum privacy.

How Blocking Works

ActionImplementationPrivacy
Block contactAdd address to local block listServer doesn’t know
Message from blockedClient silently discardsSender doesn’t know
Contact requestAuto-rejectedNo notification
UnblockRemove from local listImmediate effect
Block Implementation: FUNCTION block_contact(wallet_address): // Add to local block list blocked_list = get_blocked_list() blocked_list.add({ address: wallet_address, blocked_at: current_time(), reason: user_provided_reason // Optional, local only }) save_blocked_list(blocked_list) // Clear any pending messages/requests delete_pending_from(wallet_address) // No network communication - purely local FUNCTION on_message_received(message): IF sender_address IN blocked_list: DISCARD message silently RETURN // Process message normally ...

Block List Storage

PropertyValue
Storage locationLocal IndexedDB
EncryptionWith metadata storage key
SyncAcross user’s devices
Server knowledgeNone

No Server-Side Block List

Zentalk does not maintain server-side block lists.

Why No Server Block List

BenefitDescription
PrivacyServer cannot know who you’ve blocked
Censorship resistanceCannot be forced to block specific users
No social graphBlocking pattern is metadata
User controlOnly you decide who is blocked

Comparison with Centralized Blocking

AspectZentalk (Client)Centralized (Server)
Server knows blocksNoYes
Government requestsCannot complyCan reveal block lists
Malicious serverCannot revealCould reveal blocks
Block effectivenessSameSame

Reporting Abuse (Optional)

Users can optionally report abusive content to help the community.

Report Mechanism

ComponentImplementation
Report submissionEncrypted to community moderators
Content includedUser’s choice (message content optional)
Sender identityCan be anonymous
Recipient identityReported user’s address

Report Structure

FieldRequiredDescription
Report typeYesSpam, harassment, illegal content, etc.
Reported addressYesWallet address of offender
DescriptionOptionalUser’s explanation
EvidenceOptionalMessage content (user consents)
Reporter addressOptionalFor follow-up
Report Submission Flow: 1. User selects "Report" on message/contact 2. Choose report category 3. Optionally add description 4. Optionally include message evidence 5. Choose anonymous or identified report 6. Encrypt report with community moderator keys 7. Submit via 3-hop relay (hide reporter IP) 8. Moderators review (decentralized moderation)

What Happens to Reports

OutcomeAction
Spam confirmedAddress added to public spam list
Harassment confirmedWarning to community
Illegal contentReported to appropriate authorities
False reportNo action

Opt-In Nature

AspectUser Control
Submit reportsOptional
Include contentUser chooses
Reveal identityUser chooses
Use spam listsOptional filter

Contact Groups and Labels

Organizing Contacts

Users can organize contacts into groups and apply labels for easier management.

Group Features

FeatureDescription
Create groupName and optional description
Add contactsMultiple contacts per group
Nested groupsGroups within groups
Group messagingSend to all group members
Group limitsUp to 100 contacts per group

Label System

PropertyDescription
Label nameUser-defined, up to 32 characters
Label colorVisual distinction
Multiple labelsContact can have multiple labels
Label hierarchyFlat structure (no nesting)
Maximum labels50 per user

Example Organization

Group/LabelUse Case
FamilyClose family members
WorkProfessional contacts
VerifiedSecurity-conscious marking
High-trustSensitive communications
Project XTemporary project team

Privacy of Organization

Contact organization is private and never leaves the user’s devices.

Organization Data Storage

DataLocationEncryptionShared With
Group namesLocalStorage keyNo one
Group membershipLocalStorage keyNo one
Label assignmentsLocalStorage keyNo one
Organization hierarchyLocalStorage keyNo one
Organization Storage: // Groups stored separately from contacts groups_table = { group_id: uuid, name: encrypted_string, description: encrypted_string, created_at: timestamp, parent_group_id: nullable uuid } // Membership is a separate table group_membership = { group_id: uuid, contact_id: uuid, added_at: timestamp } // Labels stored similarly labels_table = { label_id: uuid, name: encrypted_string, color: string // Not sensitive } contact_labels = { contact_id: uuid, label_id: uuid, applied_at: timestamp }

What No One Can Learn

InformationProtected
How you organize contactsYes - encrypted locally
Group namesYes - encrypted locally
Which contacts are groupedYes - encrypted locally
Your labeling schemeYes - encrypted locally
Organizational changesYes - encrypted locally

Sync Privacy

When organization syncs across devices:

Sync PropertyPrivacy Guarantee
Group data encryptedEnd-to-end, only your devices
Membership encryptedEnd-to-end, only your devices
Labels encryptedEnd-to-end, only your devices
Mesh network seesEncrypted blobs only

Implementation Considerations

Contact Discovery Flow Summary

Complete Contact Addition Flow: 1. DISCOVERY - Scan QR code, OR - Enter wallet address manually, OR - Search username (if registered) 2. VALIDATION - Verify address format - Check not already in contacts - Check not in block list 3. KEY RETRIEVAL - Query DHT for contact's key bundle - Retrieve identity key, signed pre-key, one-time pre-keys - Verify key bundle signatures 4. CONTACT REQUEST - Generate proof-of-work - Create contact request message - Encrypt with X3DH - Send via 3-hop relay 5. RECIPIENT PROCESSING - Receive contact request - Verify PoW - Display to user for action 6. ACCEPTANCE - User accepts request - Contact added to encrypted local storage - Notify sender of acceptance 7. VERIFICATION (optional) - Compare safety numbers - QR code verification - Mark as verified 8. SYNC - Encrypt contact data - Upload to mesh storage - Sync to other devices

Security Properties Summary

PropertyMechanismGuarantee
Contact privacyLocal-only storageServer cannot enumerate contacts
Organization privacyEncrypted metadataGroups/labels are secret
Block privacyClient-side blockingServer doesn’t know who’s blocked
Discovery privacyNo address book uploadContact graph protected
Verification integritySafety numbersMITM detection
Spam resistancePoW + rate limitingCostly to spam

Last updated on