How Peer Discovery Works in BitChat's BLE Mesh: Link-Layer Scanning to Secure Binding

BitChat's peer discovery relies on a continuous Bluetooth Low Energy (BLE) scan-and-advertise loop managed by the BLELinkLayer, which emits discovery events to a BLEPeerRegistryStore where the mesh engine decides which devices to connect to and secure via Noise handshakes.

Peer discovery in BitChat's BLE mesh forms the foundation of its offline, permissionless messaging protocol. According to the permissionlesstech/bitchat source code, the implementation isolates BLE-specific radio operations from higher-level mesh logic through a dedicated link-layer architecture. This design enables deterministic peer detection across iOS devices using CoreBluetooth while maintaining thread safety through strict queue isolation.

BLEService and Queue Isolation

The BLEService class owns the sole CoreBluetooth import in the codebase and manages the BLELinkLayer through two specialized role extensions. The link layer runs on a dedicated serial queue (bleQueue-confined) to ensure radio operations never block the mesh engine. This isolation is implemented in bitchat/Services/BLE/BLEService+LinkLayerCentralRole.swift for scanning and bitchat/Services/BLE/BLEService+LinkLayerPeripheralRole.swift for advertising.

While scanning, the central role listens for a custom service UUID that uniquely identifies BitChat nodes. Simultaneously, the peripheral role advertises this same UUID, creating an always-on discovery mechanism that requires no internet connectivity. As documented in docs/BLE-ARCHITECTURE-V3.md, this "radio" operates independently from the mesh engine, feeding discovery events upward through a strictly defined pipeline.

From Radio Events to Peer Registration

When the link layer detects an advertising peer, it emits a LinkEvent via the emitLinkEvent method. The mesh engine consumes these events through handleLinkEvent, registering discovered devices in the BLEPeerRegistryStore.

This registry is lock-backed and engine-confined, storing peer identifiers, capabilities, and advertised metadata. All queries—including isPeerConnected checks and nickname resolution—flow through this centralized store rather than accessing CoreBluetooth directly. This separation ensures that the mesh protocol can inspect the peer list from any thread without interfering with the serial BLE queue.

Connection Initiation and Secure Handshakes

Peer discovery alone does not establish connections; the mesh engine must issue a LinkCommand to promote a discovered peer to an active link. Upon physical connection, the link layer reports a linkUp event, triggering a Noise-based handshake managed by BLELinkAuthState and BLELinkBindings.

These binding structures are engine-owned rather than BLE-queue-owned, preventing race conditions between authentication and message transmission. As implemented in the architecture documented in docs/BLE-ARCHITECTURE-V3.md, this ownership model guarantees that cryptographic identities cannot be altered between the handshake completion and the first encrypted transmission.

Implementation: Starting Discovery and Handling Events

To initiate the discovery cycle, the transport layer invokes the shared service:

// Start BLE discovery (called from the app's transport layer)
BLEService.shared.startScanningAndAdvertising()

The link layer then emits discovery events that the engine handles through a centralized event processor:

// The link-layer emits discovery events; the engine registers the peer
func handleLinkEvent(_ event: BLELinkEvent) {
    switch event {
    case .peerDiscovered(let peripheral):
        // Register peer in the engine-confined registry
        peerRegistry.register(peripheral.identifier, capabilities: peripheral.capabilities)
    case .linkUp(let linkID):
        // Initiate Noise handshake for the newly-connected link
        noiseService.startHandshake(with: linkID)
    default: break
    }
}

Applications can query the current peer set through the thread-safe registry:

// Query the current set of discovered peers (engine-confined, thread-safe)
let nearbyPeers = peerRegistry.connectedPeers()
print("Discovered \(nearbyPeers.count) BLE peers")

Summary

  • Link-layer isolation: BLEService manages CoreBluetooth through BLELinkLayer extensions running on a dedicated serial queue (bleQueue-confined).
  • Always-on discovery: Simultaneous scanning and advertising using a custom service UUID identifies nearby BitChat nodes without internet connectivity.
  • Event-driven registration: Discovery events flow through emitLinkEvent to handleLinkEvent, populating the thread-safe BLEPeerRegistryStore.
  • Deferred connections: The mesh engine issues LinkCommand to initiate physical connections only after filtering discovered peers through the registry.
  • Secure binding: Post-connection Noise handshakes use engine-owned BLELinkAuthState and BLELinkBindings to prevent cryptographic race conditions.

Frequently Asked Questions

How does BitChat handle concurrent BLE scanning and advertising?

The BLELinkLayer runs both operations on a dedicated serial queue (bleQueue-confined) defined in BLEService+LinkLayerCentralRole.swift and BLEService+LinkLayerPeripheralRole.swift. This queue isolation prevents radio operations from blocking the mesh engine while allowing simultaneous central and peripheral roles on iOS devices.

What triggers the mesh engine to initiate a connection after peer discovery?

After the BLEPeerRegistryStore registers a discovered peer via handleLinkEvent, the mesh engine evaluates whether the device belongs to the same geohash channel or meets other application-specific criteria. If matched, the engine issues a LinkCommand to the link layer to establish the physical BLE connection, which then emits a linkUp event upon success.

The BLEPeerRegistryStore is engine-confined and lock-backed, separating high-level peer metadata from low-level BLE operations. This design allows the mesh protocol to query isPeerConnected and capabilities without accessing CoreBluetooth directly, and enables deterministic testing via bitchatTests/Simulation/SimulatedMesh.swift without requiring physical radios.

How does the simulator test peer discovery without physical Bluetooth radios?

The SimulatedMesh.swift test harness reproduces the emitLinkEvent → handleLinkEvent pipeline and BLEPeerRegistryStore interactions using mock objects. This allows developers to verify peer discovery logic, registry state transitions, and Noise handshake sequences without requiring actual BLE hardware, making the discovery pipeline fully testable in CI environments.

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 →