# How Peer Discovery Works in the BitChat Bluetooth Mesh Network

> Discover BitChat's peer discovery process in Bluetooth mesh networks. Learn how continuous scanning, validation, and reciprocal announcements build a self-healing mesh.

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

---

**BitChat uses a three-stage pipeline—continuous BLE scanning, cryptographic pre-flight validation, and trust-based reciprocal announcements—to discover, verify, and bind peers into a self-healing mesh topology.**

BitChat is an open-source, permissionless messaging application developed by permissionlesstech that constructs a Bluetooth-LE mesh network among nearby devices. The peer discovery process within the BitChat Bluetooth mesh network coordinates scanning, security validation, and topology tracking to enable decentralized, multihop communication without internet connectivity.

## The Three-Stage Discovery Pipeline

The discovery architecture separates concerns into distinct policy-driven stages managed by `BLEService` and `BLEAnnounceHandler`:

### Continuous BLE Scanning

When the central manager state becomes **powered-on**, `BLEService.startScanning()` initiates a Bluetooth-LE scan with `allowDuplicates = true`. This ensures every advertisement is delivered immediately, which is essential for rapid mesh convergence. The scan duty cycle adapts via `BLEScanDutyPolicy.plan`, switching between continuous and duty-cycled modes based on app activity and recent traffic.

### Pre-Flight Validation

Raw advertisements are decoded into `AnnouncementPacket` structures and filtered by `BLEAnnouncePreflightPolicy.evaluate(...)`. This stage performs lightweight, signature-less sanity checks: it derives the sender's **PeerID** from the `noisePublicKey`, rejects malformed payloads, drops self-announces, and discards stale packets outside the freshness window.

### Trust Verification and Response

`BLEAnnounceTrustPolicy.evaluate(...)` enforces cryptographic security: it verifies key matches, enforces TOFU (Trust On First Use) signing-key pinning, and validates packet signatures. Once trusted, `BLEAnnounceResponsePolicy.plan(...)` orchestrates the response—logging UI events, scheduling gossip syncs, sending reciprocal announces, and triggering after-glow re-announcements for robustness.

## Step-by-Step Discovery Flow

The complete flow from advertisement to mesh entry follows nine coordinated steps implemented across [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift) and [`BLEAnnounceHandler.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEAnnounceHandler.swift):

1. **Scan Activation**  
   `centralManagerDidUpdateState(_:)` triggers `startScanning()` when Bluetooth is ready. The service uses `allowDuplicates = true` to capture every broadcast instantly.

2. **Advertisement Receipt**  
   The system delegate calls `central(_:didDiscover:advertisementData:rssi:)`. The raw payload converts to a `BitchatPacket` and passes to `BLEAnnounceHandler.handle(_:,from:)`.

3. **Pre-Flight Checks**  
   `BLEAnnouncePreflightPolicy` decodes the `AnnouncementPacket`, derives the PeerID from the `noisePublicKey`, and rejects packets for `.malformed`, `.senderMismatch`, `.selfAnnounce`, or `.stale` conditions.

4. **Cryptographic Trust**  
   `BLEAnnounceTrustPolicy` retrieves stored Noise and signing keys from the registry. It rejects on key mismatch, protects authenticated Noise sessions from key rotation attacks, verifies TOFU pinning, and validates signatures against announced signing keys.

5. **Registry Update**  
   Inside a `withRegistryBarrier` block, the handler calls `upsertVerifiedAnnounce` to persist the peer, records a "peer-connected" UI event for direct announces, and schedules the initial gossip sync.

6. **Bidirectional Discovery**  
   For direct announces (`packet.ttl == messageTTL`) where `shouldSendAnnounceBack` is true, the handler invokes `sendAnnounceBack()`. This reciprocal announcement ensures both peers confirm the link, creating a two-way edge in the mesh.

7. **After-Glow Scheduling**  
   For new peers, `scheduleAfterglow` queues a delayed re-announcement to mitigate packet loss during initial contact.

8. **Topology Tracking**  
   Verified announces carry a `directNeighbors` list. `MeshTopologyTracker.updateNeighbors(for:neighbors:at:)` stores these claims with timestamps, pruning stale entries after approximately 60 seconds.

9. **Route Computation**  
   When routing messages, `MeshTopologyTracker.computeRoute(...)` executes a BFS over confirmed, fresh edges, optionally filtering by protocol version to find multihop paths.

## Mesh Topology and Multihop Routing

The `MeshTopologyTracker` maintains the distributed routing table. It aggregates neighbor claims from announcements, timestamps each edge, and removes expired entries to prevent routing through departed nodes.

When the app needs to forward a message, the tracker performs a breadth-first search:

```swift
let tracker = MeshTopologyTracker()
let hops = tracker.computeRoute(
    from: myPeerIDData,
    to: destinationPeerIDData,
    maxHops: 8,
    requiringVersion: 2
)
if let hops = hops {
    sendMessageViaMesh(hops: hops, payload: data)
}

```

*Relevant code*: `MeshTopologyTracker.computeRoute` in [[`MeshTopologyTracker.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MeshTopologyTracker.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/MeshTopologyTracker.swift).

## Implementation Examples

### Initializing the BLE Service

Bootstrap the service to begin peer discovery:

```swift
let bleService = BLEService(
    transportConfig: TransportConfig(),
    env: BLEServiceEnvironment(...)
)
bleService.start()  // Powers up CBCentralManager and begins scanning

```

*Relevant code*: `BLEService.start()` → `centralManagerDidUpdateState` → `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).

### Handling Incoming Advertisements

Process discovered peripherals through the validation pipeline:

```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)
}

```

The `bleAnnounceHandler` coordinates `BLEAnnouncePreflightPolicy`, `BLEAnnounceTrustPolicy`, and `BLEAnnounceResponsePolicy` defined in [[`BLEAnnounceHandlingPolicy.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEAnnounceHandlingPolicy.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEAnnounceHandlingPolicy.swift).

### Configuring Duty-Cycled Scanning

Optimize battery usage in background modes:

```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)
}

```

*Relevant code*: `BLEScanDutyPolicy.plan` in [[`BLEScanDutyPolicy.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEScanDutyPolicy.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEScanDutyPolicy.swift).

## Summary

- **Continuous scanning** with `allowDuplicates = true` ensures immediate detection of nearby peers, while `BLEScanDutyPolicy` balances discovery speed against battery life.
- **Defense-in-depth validation** through `BLEAnnouncePreflightPolicy` and `BLEAnnounceTrustPolicy` prevents spoofing, replay attacks, and key-mismatch exploits before peers enter the mesh.
- **Bidirectional bonding** via reciprocal announces guarantees symmetric links, ensuring both parties recognize the connection before message routing begins.
- **Self-healing topology** via `MeshTopologyTracker` maintains fresh neighbor tables with 60-second pruning and computes BFS-based multihop routes on demand.

## Frequently Asked Questions

### How does BitChat prevent spoofed peer announcements?

BitChat cryptographically binds each `AnnouncementPacket` to a specific identity. `BLEAnnounceTrustPolicy` enforces TOFU signing-key pinning and verifies Ed25519 signatures against the announced key. If the derived PeerID from the `noisePublicKey` does not match the packet's declared sender, or if the signing key conflicts with previously pinned keys, the packet is rejected before the peer enters the registry.

### What triggers the after-glow mechanism in peer discovery?

The after-glow mechanism triggers for brand-new peers immediately after the first successful validation. `BLEAnnounceResponsePolicy.plan(...)` sets `shouldSendAnnounceBack = true` for direct announces, and `BLEAnnounceHandler` calls `scheduleAfterglow` to queue a delayed re-announcement. This redundancy improves reliability against packet loss during the critical initial contact window.

### How does the mesh support multihop messaging without internet connectivity?

The `MeshTopologyTracker` aggregates neighbor lists from all verified announces to build a distributed graph of the local mesh. When sending to a non-adjacent peer, `computeRoute(...)` performs a BFS over confirmed edges to discover intermediate hops. Each node stores only its direct neighbors' claims, enabling decentralized routing decisions without a central directory or internet access.

### Can BitChat discover peers while running in the background?

Yes, via adaptive duty-cycling implemented in `BLEScanDutyPolicy`. When `dutyEnabled` is true and the app enters background state, the policy calculates an on/off duty cycle based on connected peer count and recent traffic. This allows continuous (albeit intermittent) scanning to maintain mesh connectivity while respecting iOS background execution limits and preserving battery life.