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

> Explore BitChat's Bluetooth mesh peer discovery. Learn about its three-stage pipeline: scanning, validation, and trust-based response for secure, dynamic connections.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: deep-dive
- Published: 2026-08-07

---

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

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

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

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

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/TransportConfig.swift)) to preserve battery while maintaining mesh connectivity.