How BitChat's BLEAnnounceHandler Processes Incoming Announcements

BitChat's BLEAnnounceHandler processes incoming BLE announcements through a deterministic 14-step pipeline that validates packet integrity cryptographically, evaluates trust policies, atomically updates the peer registry, and manages all side effects through an injectable environment pattern.

BitChat, an open-source peer-to-peer messaging platform maintained in the permissionlesstech/bitchat repository, discovers nearby peers using Bluetooth Low Energy (BLE) announcement packets. The core logic that ingests these packets resides in BLEAnnounceHandler, located at bitchat/Services/BLE/BLEAnnounceHandler.swift. This handler is deliberately designed to be queue-agnostic and synchronously testable, delegating all side effects—such as registry writes, UI notifications, and topology updates—to closure-based dependencies supplied by BLEAnnounceHandlerEnvironment.

The 14-Step Processing Pipeline

When BLEAnnounceHandler.handle(_:from:) receives a packet, it executes a strict sequence of validation, verification, and state updates. Each step is implemented as a discrete operation with specific source file references.

Step 1: Pre-Flight Validation

The pipeline begins with BLEAnnouncePreflightPolicy.evaluate in BLEAnnounceHandlingPolicy.swift (lines 21-48). This stage decodes the raw payload, validates that the derived peer ID matches the packet’s sender, rejects self-announces, and discards stale packets before any cryptographic work begins.

Steps 2-4: Cryptographic Verification and Trust

Once pre-flight passes, the handler prepares signing keys and verifies authenticity:

  • Signature Preparation: The handler retrieves existingPeerKeys from the registry, falling back to the persistedSigningPublicKey if the entry was lost (referenced at BLEAnnounceHandler.swift lines 29-38).
  • Signature Verification: If the packet contains a signature, env.verifySignature validates it against the announced signing key, storing the boolean result in signatureValid (lines 41-46).
  • Trust Evaluation: BLEAnnounceTrustPolicy.evaluate (defined in BLEAnnounceHandlingPolicy.swift lines 71-114) makes the final trust decision. It rejects announcements with missing signatures, invalid cryptographic proofs, or key mismatches, returning a verification status used for all subsequent steps.

Steps 5-6: Direct Announce Detection and Registry Barrier

The handler determines if the packet is a direct announce by checking packet.ttl == env.messageTTL and verifies whether the inbound link is already bound to a different peer via env.linkBoundToOtherPeer (lines 76-80).

All registry mutations occur inside env.withRegistryBarrier (lines 96-125). If the trust evaluation returns unverified, the barrier exits early. Otherwise, env.upsertVerifiedAnnounce atomically inserts or updates the peer entry within the barrier block.

Steps 7-12: Topology, Persistence, and UI Side Effects

For verified announcements, the handler triggers multiple side effects through the environment:

  • Connection Logging: Debounced logs for new or re-connected peers are emitted via env.shouldEmitReconnectLog (lines 28-40).
  • Topology Updates: Verified neighbor claims are handed to env.updateTopology (lines 43-46).
  • Identity Persistence: The entire AnnouncementPacket is persisted using env.persistIdentity so the signing key survives application restarts (lines 48-53).
  • Deduplication and Response: A deterministic deduplication ID (announce-back-<peerID>) prevents duplicate processing. If the packet is new, env.sendAnnounceBack() initiates bidirectional discovery (lines 55-68).
  • UI Notification: env.deliverAnnounceUIEvents emits .peerConnected events and schedules the initial gossip sync (lines 70-74).
  • Packet Tracking: The packet is recorded via env.trackPacketSeen for later gossip synchronization (lines 76-78).

Steps 13-14: Afterglow and Result

For brand-new peers, the handler schedules an afterglow re-announce—a short random delay that pushes presence one hop further into the mesh—using env.scheduleAfterglow (lines 84-89).

Finally, the method returns a BLEAnnounceHandlingResult struct (lines 78-86) containing the peer ID, decoded announcement, and boolean flags indicating isDirect and isVerified.

The Environment-Based Architecture

BLEAnnounceHandler achieves testability by avoiding direct dependencies on BLE queues or UI frameworks. Instead, it accepts a BLEAnnounceHandlerEnvironment struct containing closures for all external operations. This design allows unit tests to inject deterministic mocks, as demonstrated in BLEAnnounceHandlerTests.swift.

Wiring the Environment

A concrete implementation (such as BLEService) constructs the environment as follows:

let env = BLEAnnounceHandlerEnvironment(
    localPeerID: { self.identityManager.localPeerID },
    messageTTL: 1,
    now: Date.init,
    existingPeerKeys: registry.existingKeys(for:),
    persistedSigningPublicKey: identityManager.persistedSigningKey,
    authenticatedSigningPublicKey: { noiseKey in 
        registry.authenticatedSigningKey(forNoiseKey: noiseKey) 
    },
    verifySignature: { packet, pubKey in
        packet.signature.map { 
            Crypto.verify($0, data: packet.payload, publicKey: pubKey) 
        } ?? false
    },
    linkState: { peerID in self.linkState(for: peerID) },
    linkBoundToOtherPeer: { packet, peerID in 
        self.isLinkBound(toOtherPeer: peerID, packet: packet) 
    },
    withRegistryBarrier: { work in registry.performBarrier(work) },
    upsertVerifiedAnnounce: registry.upsertVerifiedAnnounce,
    shouldEmitReconnectLog: { peerID, now in 
        self.reconnectLogDebouncer.shouldLog(peerID, now) 
    },
    updateTopology: topology.update,
    persistIdentity: { ann in self.identityStore.save(ann) },
    dedupContains: dedupCache.contains,
    dedupMarkProcessed: dedupCache.mark,
    deliverAnnounceUIEvents: { pid, notify, schedule in
        ui.notifyPeerConnected(pid)
        if schedule { sync.scheduleInitial(for: pid) }
        ui.refreshPeerList()
    },
    trackPacketSeen: { pkt in sync.track(pkt) },
    sendAnnounceBack: { self.sendOwnAnnounce() },
    scheduleAfterglow: { delay in self.scheduleAfterglow(delay) }
)

let handler = BLEAnnounceHandler(environment: env)
let result = handler.handle(incomingPacket, from: sourcePeerID)

Unit Testing the Handler

Because the handler is pure logic wrapped around the environment, tests can verify the full pipeline with mocked dependencies:

func testHandlesValidAnnounce() {
    let env = makeEnvironment(...)
    let handler = BLEAnnounceHandler(environment: env)
    
    let packet = makeValidAnnouncePacket()
    let result = handler.handle(packet, from: somePeerID)
    
    XCTAssertNotNil(result)
    XCTAssertTrue(result!.isVerified)
    XCTAssertTrue(result!.isDirectAnnounce)
}

Summary

  • BLEAnnounceHandler ingests BLE packets through a strictly ordered 14-step pipeline defined in BLEAnnounceHandler.swift.
  • Pre-flight validation and trust evaluation occur in BLEAnnounceHandlingPolicy.swift, separating policy from execution.
  • Registry updates are atomic, executing within withRegistryBarrier only after cryptographic verification succeeds.
  • Side effects (UI updates, topology changes, persistence) are injected via BLEAnnounceHandlerEnvironment, making the core logic testable without BLE hardware.
  • Bidirectional discovery uses deduplication IDs to prevent redundant announce-backs, while afterglow scheduling propagates presence to multi-hop neighbors.

Frequently Asked Questions

How does BLEAnnounceHandler prevent processing of spoofed or replayed announcements?

The handler rejects spoofed packets during the trust evaluation step in BLEAnnounceTrustPolicy.evaluate (BLEAnnounceHandlingPolicy.swift lines 71-114), which requires valid cryptographic signatures matching the announced public key. Replay attacks are mitigated by the deduplication system (lines 55-68) that checks dedupContains before processing and marks packets via dedupMarkProcessed.

What is the difference between a direct announce and a routed announce?

A direct announce is detected when packet.ttl == env.messageTTL (BLEAnnounceHandler.swift lines 76-80), indicating the packet originated from the immediate neighbor. Routed announces have decremented TTL values and are processed differently for topology updates, though both types undergo the same cryptographic validation.

Why does the handler use an environment struct instead of direct dependencies?

The environment pattern allows BLEAnnounceHandler to remain synchronously testable and queue-agnostic. By injecting closures like withRegistryBarrier, verifySignature, and deliverAnnounceUIEvents, the handler can run inside unit tests without requiring actual BLE hardware, database connections, or UI frameworks. This architecture is verified in BLEAnnounceHandlerTests.swift.

How does the registry barrier ensure thread safety during peer updates?

The withRegistryBarrier closure (lines 96-125) serializes access to the peer registry. All mutations—including upsertVerifiedAnnounce—execute within this barrier, preventing race conditions when multiple BLE connections simultaneously announce peers or when the UI thread queries the registry during updates.

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 →