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 applies bleFragmentRelayTtlCapDense to 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 BLEService executes 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:

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 →