BitChat BLE Mesh Network Flood Control and Routing Implementation
BitChat prevents broadcast storms while ensuring reliable multi-hop delivery by combining TTL-capped flooding via RelayController.decide() with source-based routing through BLESourceRouteOriginationPolicy.route() and BLERouteForwardingPolicy.
BitChat is an open-source peer-to-peer messaging application available at permissionlesstech/bitchat that builds a resilient Bluetooth Low Energy (BLE) mesh network. The implementation uses a hybrid approach where controlled flooding handles broadcast traffic and topology discovery, while source-based routing optimizes directed unicast traffic. This architecture automatically scales bandwidth consumption based on mesh density and traffic priority.
Flood Control Policy (RelayController)
The RelayController class centralizes all relay decisions in a pure-function utility located at bitchat/Services/RelayController.swift. Every node evaluates packets using RelayController.decide(), which returns a structured decision including whether to relay, the new TTL, and a calculated jitter delay.
Relay Decision Logic
The decision engine evaluates multiple boolean flags and network metrics to determine packet handling:
let decision = RelayController.decide(
ttl: packet.ttl,
senderIsSelf: isSelf,
recipientIsSelf: isRecipient,
isEncrypted: encrypted,
isDirectedEncrypted: directedEnc,
isFragment: fragment,
isDirectedFragment: directedFragment,
isHandshake: handshake,
isAnnounce: announce,
isRequestSync: requestSync,
isUrgentBoardPost: urgentBoard,
isVoiceFrame: voice,
degree: meshDegree,
highDegreeThreshold: denseThreshold)
The function implements aggressive deduplication and TTL enforcement. TTL ≤ 1, self-sent packets, and self-received packets trigger an immediate no relay decision. Request-Sync packets are strictly link-local and never relayed under any circumstances.
Traffic-Specific Rules
Different packet types receive specialized handling to balance delivery guarantees against bandwidth consumption:
- Handshake, Directed Encrypted, and Directed Fragment packets always relay with TTL decremented by 1, but apply short jitter windows (10–35 ms for handshakes, 20–60 ms for other directed traffic) to prevent collision storms during connection establishment.
- Fragment and Voice frames use density-aware TTL caps. When
degree ≥ highDegreeThreshold, the controller appliesbleFragmentRelayTtlCapDenseto prevent high-degree nodes from amplifying broadcast traffic; otherwise, it uses the standard fragment TTL cap. - Broadcast and Announce packets apply degree-aware clamping (TTL 2–5 for dense graphs versus full TTL for thin chains) with larger jitter windows ranging from 10–220 ms depending on local mesh degree.
This policy ensures that flood bandwidth scales inversely with topology density, preserving low latency for time-critical streams while preventing uncontrolled broadcast storms in densely connected meshes.
Source-Based Routing Implementation
When traffic targets a specific recipient, BitChat upgrades packets to version 2 with explicit source routes, avoiding unnecessary flooding for directed communication.
Route Origination Policy
The BLESourceRouteOriginationPolicy.route() function gates route attachment through strict validation criteria defined in bitchat/Services/BLE/BLESourceRouteOriginationPolicy.swift:
| Validation Gate | Requirement |
|---|---|
| Local Author | packet.senderID == localPeerIDData |
| Valid Recipient | Recipient ID must be present, exactly 8 bytes, and not equal to 0xFF… broadcast address |
| TTL Headroom | packet.ttl > 1 |
| Indirect Target | !isRecipientConnected(recipient) |
| Failure Cache | shouldAttemptRoute(recipient) returns true (failures cached for 60 s) |
| Route Availability | computeRoute(recipient) returns non-empty hop list |
Only when all gates pass does the system encode the hop list into the packet header as the Source Route field, converting the packet to v2 format with the HAS_ROUTE flag set.
Forwarding and Fallback Mechanisms
The BLERouteForwardingPolicy used inside BLEService (around line 3713 in bitchat/Services/BLE/BLEService.swift) handles incoming routed packets:
if packet.hasRoute && packet.version >= 2 {
if let nextHop = packet.nextHop(for: localPeerID) {
sendToPeer(nextHop, packet) // Unicast to next hop
} else {
deliverLocally(packet) // Final destination reached
}
} else {
// No route present → use standard flood policy
if RelayController.decide(...).shouldRelay {
broadcast(packet)
}
}
When the designated next hop is unreachable, the policy falls back to broadcast flooding, guaranteeing eventual delivery even when selected routes fail. This hybrid approach ensures that source routing optimizes the common case while flooding provides a reliability backstop.
Topology Discovery and Route Validation
Source routing depends on an accurate mesh graph maintained by MeshTopologyTracker. Nodes broadcast their direct-neighbor lists via the IdentityAnnouncement TLV (0x04), and edges are only considered confirmed when both peers list each other bidirectionally. Unconfirmed edges are excluded from route calculations.
The topology tracker refreshes the graph every 60 seconds, ensuring that computed routes traverse only live, bidirectional links. This prevents black-holed packets and reduces the incidence of fallback flooding.
Failure Handling and Route Cache
When a routed unicast fails to receive acknowledgment (no inbound packet from the recipient within 10 s), BLESourceRouteFailureCache records a temporary failure for that recipient. Subsequent transmissions to the same target automatically fall back to flooding for 60 seconds, after which the system retries source routing.
This failure-cache mechanism prevents persistent routing attempts through broken paths while allowing automatic recovery when topology changes restore connectivity.
Code Examples
Obtaining a Relay Decision
let decision = RelayController.decide(
ttl: 7,
senderIsSelf: true,
recipientIsSelf: false,
isEncrypted: true,
isDirectedEncrypted: true,
isFragment: false,
isDirectedFragment: false,
isHandshake: false,
isAnnounce: false,
isRequestSync: false,
isUrgentBoardPost: false,
isVoiceFrame: false,
degree: 4, // Current node degree
highDegreeThreshold: 5) // Dense-graph threshold
if decision.shouldRelay {
scheduleRelay(after: decision.delayMs, ttl: decision.newTTL)
}
For a directed encrypted packet in this configuration, the decision returns shouldRelay = true, TTL reduced by 1, and a jitter delay of approximately 20–60 ms.
Attaching a Source Route
guard let route = BLESourceRouteOriginationPolicy.route(
for: packet,
to: recipient,
localPeerIDData: myPeerID,
isRecipientConnected: mesh.isConnected(to:),
shouldAttemptRoute: mesh.shouldRoute(to:),
computeRoute: mesh.computeRoute(to:)) else {
// Send as standard flood/direct-write
bleService.send(packet)
return
}
packet.attachRoute(route) // Encode hop list into header
bleService.send(packet) // Transmit as v2 source-routed packet
Forwarding with Fallback
if packet.hasRoute && packet.version >= 2 {
if let nextHop = packet.nextHop(for: localPeerID) {
sendToPeer(nextHop, packet)
} else {
deliverLocally(packet)
}
} else {
// Standard flood path
if RelayController.decide(...).shouldRelay {
broadcast(packet)
}
}
Summary
- RelayController.decide() implements density-aware flood control by capping TTLs and adding calculated jitter based on traffic type and local mesh degree.
- BLESourceRouteOriginationPolicy.route() validates six gating criteria before converting packets to v2 source-routed format, ensuring routes target valid, indirect recipients with available paths.
- BLERouteForwardingPolicy inside
BLEServiceexecutes unicast forwarding for routed packets with automatic fallback to broadcasting when next-hop nodes are unreachable. - MeshTopologyTracker maintains a validated graph of bidirectional edges, refreshing every 60 seconds to ensure route validity.
- BLESourceRouteFailureCache provides 60-second failure memory to prevent persistent routing through broken paths while enabling automatic recovery.
Frequently Asked Questions
What triggers BitChat to use source routing instead of flooding?
BitChat upgrades to source routing when BLESourceRouteOriginationPolicy.route() validates that the sender is local, the recipient is an 8-byte non-broadcast ID not directly connected, sufficient TTL remains (>1), no recent failure exists for that recipient, and MeshTopologyTracker computes a valid hop list. If any gate fails, the packet transmits via standard flood control.
How does BitChat prevent broadcast storms in dense mesh networks?
The RelayController.decide() function applies degree-aware TTL clamping (reducing maximum hops to 2–5 for high-degree nodes) and scales jitter windows based on local connectivity. Fragment and voice traffic use separate dense-graph TTL caps, while request-sync packets never relay, ensuring controlled bandwidth scaling as node density increases.
What happens when a source-routed packet encounters a dead hop?
When BLERouteForwardingPolicy detects an unreachable next hop (typically via timeout), the system automatically falls back to broadcast flooding according to RelayController rules. Additionally, the failure is cached for 60 seconds in BLESourceRouteFailureCache, causing subsequent packets to that recipient to use flooding until the cache expires and routing retries.
Where is the routing logic centralized in the BitChat codebase?
Flood control logic resides in bitchat/Services/RelayController.swift, while source routing origination lives in bitchat/Services/BLE/BLESourceRouteOriginationPolicy.swift. The forwarding implementation and fallback handling are located in bitchat/Services/BLE/BLEService.swift around line 3713, with topology tracking handled by MeshTopologyTracker and documented in SOURCE_ROUTING.md.
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 →