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

> Learn how BitChat's BLE mesh discovers peers. Understand the link-layer scanning and secure binding process for seamless connections.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: internals
- Published: 2026-08-02

---

**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.

## The Link-Layer Foundation: Continuous Scanning and Advertising

### 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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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:

```swift
// 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:

```swift
// 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:

```swift
// 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.

### Why is the peer registry stored separately from the link layer?

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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Simulation/SimulatedMesh.swift) without requiring physical radios.

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

The [`SimulatedMesh.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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.