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
existingPeerKeysfrom the registry, falling back to thepersistedSigningPublicKeyif the entry was lost (referenced atBLEAnnounceHandler.swiftlines 29-38). - Signature Verification: If the packet contains a signature,
env.verifySignaturevalidates it against the announced signing key, storing the boolean result insignatureValid(lines 41-46). - Trust Evaluation:
BLEAnnounceTrustPolicy.evaluate(defined inBLEAnnounceHandlingPolicy.swiftlines 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
AnnouncementPacketis persisted usingenv.persistIdentityso 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.deliverAnnounceUIEventsemits.peerConnectedevents and schedules the initial gossip sync (lines 70-74). - Packet Tracking: The packet is recorded via
env.trackPacketSeenfor 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
withRegistryBarrieronly 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →