# BitChat BLE Mesh Network Flood Control and Routing Implementation

> Discover how BitChat implements flood control and routing in its BLE mesh network using TTL-capped flooding and source-based routing for reliable multi-hop delivery. Learn more.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: internals
- Published: 2026-08-19

---

**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`](https://github.com/permissionlesstech/bitchat/blob/main/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:

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift)) handles incoming routed packets:

```swift
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

```swift
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

```swift
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

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/RelayController.swift), while source routing origination lives in [`bitchat/Services/BLE/BLESourceRouteOriginationPolicy.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLESourceRouteOriginationPolicy.swift). The forwarding implementation and fallback handling are located in [`bitchat/Services/BLE/BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift) around line 3713, with topology tracking handled by `MeshTopologyTracker` and documented in [`SOURCE_ROUTING.md`](https://github.com/permissionlesstech/bitchat/blob/main/SOURCE_ROUTING.md).