How BitChat's Bluetooth Mesh Peer Discovery Works: A Technical Deep Dive

BitChat uses a three-stage pipeline—scanning, pre‑flight validation, and trust‑based response—to discover peers with cryptographic identity verification, reciprocal announces for bidirectional bonding, and dynamic mesh topology tracking.

BitChat is an open‑source messaging app that builds a Bluetooth‑LE mesh network without requiring internet infrastructure. The peer discovery process is the foundation of this mesh, determining how devices find each other, validate identities, and establish routable connections. This article explains how permissionlesstech/bitchat implements peer discovery based on its actual Swift source code.


Overview of the Discovery Pipeline

The peer discovery process in BitChat operates through three coordinated stages that work together to build and maintain the mesh topology.

Stage Purpose Key Component
Scanning Continuously receive BLE advertisements BLEService.startScanning() in [BLEService.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift)
Pre‑flight validation Filter malformed, stale, or self‑targeted packets BLEAnnouncePreflightPolicy.evaluate(...) in [BLEAnnounceHandlingPolicy.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEAnnounceHandlingPolicy.swift)
Trust & response Verify cryptographically, persist identity, respond BLEAnnounceTrustPolicy.evaluate(...) and BLEAnnounceResponsePolicy.plan(...) in [BLEAnnounceHandlingPolicy.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEAnnounceHandlingPolicy.swift)

The mesh topology tracker (MeshTopologyTracker) consumes the output of these stages to maintain neighbor lists and compute multi‑hop routes.


Stage 1: BLE Scanning and Advertisement Capture

Starting the Scan

When the central manager's state becomes powered‑on, BLEService initiates scanning through startScanning(). The scan is configured with allowDuplicates = true, ensuring every repeat broadcast is delivered immediately rather than being coalesced by iOS.

// Central manager state transition triggers scanning
func centralManagerDidUpdateState(_ central: CBCentralManager) {
    if central.state == .poweredOn {
        startScanning()
    }
}

The scanning behavior adapts to power and activity conditions through BLEScanDutyPolicy.plan(...), which chooses between continuous and duty‑cycled modes based on:

  • Whether the app is active or backgrounded
  • Number of currently connected peers
  • Recent traffic volume
let plan = BLEScanDutyPolicy.plan(
    dutyEnabled: true,
    appIsActive: UIApplication.shared.applicationState == .active,
    connectedCount: bleService.connectedPeerCount,
    hasRecentTraffic: trafficMonitor.recentTraffic
)

switch plan {
case .continuous:
    bleService.startScanning()
case .dutyCycle(let on, let off):
    bleService.startDutyCycleScanning(onDuration: on, offDuration: off)
}

Configuration constants like bleDutyOnDuration are defined in [TransportConfig.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/TransportConfig.swift).

Receiving Advertisements

When a BLE advertisement arrives, the system calls central(_:didDiscover:advertisementData:rssi:). The raw payload is decoded into a BitchatPacket and routed to BLEAnnounceHandler.handle(_:,from:).

func central(
    _ central: CBCentralManager,
    didDiscover peripheral: CBPeripheral,
    advertisementData: [String: Any],
    rssi RSSI: NSNumber
) {
    guard let packet = BitchatPacket.from(advertisementData) else { return }
    let peerID = PeerID(publicKey: packet.senderID)
    _ = bleAnnounceHandler.handle(packet, from: peerID)
}

Stage 2: Pre‑Flight Packet Validation

Before any cryptographic verification, BLEAnnouncePreflightPolicy.evaluate(...) performs fast sanity checks to discard obviously invalid packets. This lightweight filter prevents wasted compute on malformed or malicious data.

The validation steps include:

  1. Decode the payload into an AnnouncementPacket
  2. Derive the sender's PeerID from the noisePublicKey contained in the announcement
  3. Reject packets matching any of these conditions:
    • Decoding failure → .malformed
    • Derived PeerID ≠ advertised PeerID → .senderMismatch
    • Packet originated from local device → .selfAnnounce
    • Timestamp outside freshness window → .stale

This pre‑flight stage is critical for mesh health: it prevents identity spoofing attempts where a peer might advertise a PeerID that doesn't cryptographically match their public key.


Stage 3: Cryptographic Trust Verification and Response

Signature and Key Validation

BLEAnnounceTrustPolicy.evaluate(...) enforces the security model for peer identity:

  • Key mismatch rejection: If previously recorded Noise or signing keys exist, they must match
  • Authenticated signing‑key protection: Already‑authenticated Noise sessions reject signing key changes
  • TOFU (Trust On First Use) pinning: Once a peer's signing key is pinned, subsequent announces with different keys are rejected
  • Signature verification: Signed packets are cryptographically verified against the announced signing key

Registry Update and UI Notification

Inside a thread‑safe barrier (withRegistryBarrier), the handler:

  • Inserts or updates peer records via upsertVerifiedAnnounce
  • Records "peer‑connected" UI events for direct announces from new or re‑connected peers
  • Schedules initial gossip synchronization
  • Persists identity data for offline verification

Reciprocal Announce and After‑Glow

For direct announces (packet.ttl == messageTTL), the response policy may trigger:

  • sendAnnounceBack(): Sends a reciprocal announce to establish bidirectional discovery
  • scheduleAfterglow(): Schedules a delayed re‑announce for new peers to improve robustness against packet loss

The reciprocal announce guarantees that both peers have confirmed each other's presence before routing traffic.


Mesh Topology Tracking and Route Computation

Every verified announce carries a directNeighbors list. MeshTopologyTracker.updateNeighbors(for:neighbors:at:) stores these claims with timestamps, prunes entries older than approximately 60 seconds, and provides fresh routes for multi‑hop messaging.

let tracker = MeshTopologyTracker()
let hops = tracker.computeRoute(
    from: myPeerIDData,
    to: destinationPeerIDData,
    maxHops: 8,
    requiringVersion: 2   // Only route through v2−capable nodes
)

if let hops = hops {
    // `hops` contains intermediate RoutingIDs to forward through
    sendMessageViaMesh(hops: hops, payload: data)
}

The route computation performs BFS over confirmed, fresh edges, with optional filtering by protocol version to ensure compatibility across the path.


Connection Establishment

When a direct announce arrives via an existing peripheral link (hasPeripheral == true), the service may initiate a connection to the remote peripheral. Announces received on links already bound to other peers are ignored—this mid‑rediscovery protection prevents connection thrashing during topology changes.


Summary

BitChat's peer discovery in its Bluetooth mesh network delivers:

  • Immediate discovery through allowDuplicates = true scanning with adaptive duty‑cycling
  • Defense in depth via pre‑flight sanity checks, TOFU key pinning, and signature verification
  • Bidirectional confirmation through reciprocal announces that guarantee two‑way visibility
  • Self‑healing topology with MeshTopologyTracker pruning stale neighbors and computing fresh routes
  • Security‑first design where malformed, stale, or cryptographically invalid packets are rejected before affecting mesh state

Frequently Asked Questions

How does BitChat prevent fake peer identities in its Bluetooth mesh?

BitChat derives each peer's PeerID from their noisePublicKey and verifies it matches the advertised identity during pre‑flight validation. BLEAnnounceTrustPolicy enforces TOFU signing‑key pinning and rejects packets with mismatched keys, preventing identity spoofing even if an attacker captures announces.

What happens when BitChat detects a stale or replayed announce packet?

BLEAnnouncePreflightPolicy rejects packets with timestamps outside the configured freshness window with status .stale. This prevents replay attacks where old announces might be rebroadcast to confuse the mesh topology or impersonate departed peers.

Can BitChat discover peers through intermediate devices, or only direct Bluetooth range?

BitChat supports multi‑hop discovery through MeshTopologyTracker. Once direct neighbors are established, each announce carries a directNeighbors list. The tracker aggregates these claims and computeRoute(...) finds paths up to 8 hops using BFS over confirmed edges, extending reach beyond single‑hop Bluetooth range.

How does BitChat balance battery life with rapid peer discovery?

BLEScanDutyPolicy.plan(...) dynamically selects between continuous scanning and duty‑cycled operation based on app activity state, connected peer count, and recent traffic. Background modes use shorter on‑periods and longer off‑periods (configurable in TransportConfig.swift) to preserve battery while maintaining mesh connectivity.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →