How Peer Discovery Works in the BitChat Bluetooth Mesh Network

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 and 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:

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/bitchat/Services/MeshTopologyTracker.swift).

Implementation Examples

Initializing the BLE Service

Bootstrap the service to begin peer discovery:

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/bitchat/Services/BLE/BLEService.swift).

Handling Incoming Advertisements

Process discovered peripherals through the validation pipeline:

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/bitchat/Services/BLE/BLEAnnounceHandlingPolicy.swift).

Configuring Duty-Cycled Scanning

Optimize battery usage in background modes:

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

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 →