How BLE Mesh Source Routing Works in BitChat: A Technical Deep Dive

BitChat's BLE mesh source routing enables efficient unicast packet delivery across Bluetooth Low Energy networks by embedding a pre-computed path of relay nodes directly in each packet, with automatic fallback to broadcast flooding when routes fail.

BitChat is an open-source, permissionless messaging system that leverages Bluetooth Low Energy (BLE) mesh networking to enable device-to-device communication without centralized servers. Starting with protocol version 2, the project introduced source routing—a deterministic pathfinding mechanism that dramatically reduces network congestion compared to naive flooding. This article explains how BLE mesh source routing works in BitChat based on the actual source code implementation.

BLE Mesh Source Routing Protocol Overview

In BitChat's mesh architecture, every device can function as a relay for other peers. Before version 2, all packets were broadcast-flooded: every node retransmitted every packet it received, creating exponential traffic growth in dense networks.

Source routing changes this model entirely. When a packet includes a RecipientID and carries the HAS_ROUTE flag, the sender pre-computes an optimal path through known relays and embeds that path in the packet itself. Intermediate nodes simply read the route list to determine their next hop—no per-packet routing decisions required.

Packet Structure for Source-Routed Messages

BitChat's protocol version 2 defines a variable-length packet format that accommodates the source route field. The exact layout is documented in [docs/SOURCE_ROUTING.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/SOURCE_ROUTING.md).


┌─────────────────┬─────────────────┬─────────────────┬─────────────────┐
│     Header      │   Fixed Fields  │  Source Route   │     Payload     │
│    (16 bytes)   │    (16 bytes)   │  (variable)     │   (variable)    │
└─────────────────┴─────────────────┴─────────────────┴─────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│ Header breakdown:                                                    │
│ - Version (2)                                                       │
│ - 4-byte payload length                                             │
│ - HAS_ROUTE flag                                                    │
└─────────────────────────────────────────────────────────────────────┘

The Source Route field contains:

  1. Count (1 byte): Number of intermediate relays
  2. Relay IDs (Count × 8 bytes): Peer IDs of intermediate hops

Critical invariant: The route list contains only intermediate relays—never the sender or final recipient. This ensures compact routes and unambiguous parsing.

All fields including the route list are covered by an Ed25519 signature appended to the packet. Any modification to the route by a malicious relay invalidates the signature, causing the destination to drop the packet.

Mesh Topology Discovery

Before any routing can occur, nodes must discover the network topology. BitChat implements this through neighbor advertisements in IdentityAnnouncement packets:

  • Each node appends a TLV (type 0x04) listing its direct neighbors
  • These announcements are collected by MeshTopologyTracker
  • Edges become confirmed only when both peers announce each other

This bidirectional confirmation guarantees that routing paths only traverse links with verified connectivity in both directions.

The MeshTopologyTracker maintains this graph in memory, providing the foundation for all path calculations when originating source-routed packets.

Route Processing in BLEService.swift

The core routing logic executes in handleIncomingPacket within [bitchat/Services/BLE/BLEService.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift) (approximately lines 330-350). When a packet arrives not addressed to the local peer, the service performs this decision chain:

Step 1: Version and Flag Validation

// Pseudocode based on source structure
if packet.version >= 2 && packet.flags.contains(.hasRoute) {
    processSourceRoutedPacket(packet)
} else {
    broadcastPacket(packet) // Legacy flood path
}

Step 2: Self-Location in Route List

The service extracts the Count and relay IDs, then searches for the local PeerID in the list.

Step 3: Next-Hop Resolution

  • If found at index i: next hop = relay at index i + 1
  • If local ID is the last entry: next hop = RecipientID

Step 4: Unicast Forward

// Simplified representation of routing logic
func forwardRoutedPacket(_ packet: Packet, route: [PeerID]) {
    guard let myIndex = route.firstIndex(of: myPeerID) else {
        broadcastPacket(packet) // Defensive fallback
        return
    }
    
    let nextHop: PeerID
    if myIndex == route.count - 1 {
        nextHop = packet.recipientID
    } else {
        nextHop = route[myIndex + 1]
    }
    
    sendMessage(to: nextHop, packet: packet)
}

Step 5: Flood Fallback

If sendMessage(to:) fails (peer unavailable, link broken), the packet falls back to broadcastPacket. This guarantees delivery at the cost of network efficiency—a deliberate reliability tradeoff.

Origination Policy: When to Create Source Routes

BitChat's iOS client applies strict conditions before originating a source route. The BLESourceRouteOriginationPolicy enforces six requirements:

Condition Purpose
SenderID equals local ID Prevents relays from re-signing packets they didn't author
RecipientID ≠ broadcast Source routing only applies to private (unicast) traffic
TTL > 1 Ensures sufficient hop budget for multi-hop routes
Recipient not directly connected Avoids route overhead when direct link exists
Path ≤ 4 hops with v2-capable peers Limits latency and guarantees decode capability
No recent failure in BLESourceRouteFailureCache Prevents retry loops on broken paths

If any condition fails, the packet enters the legacy flood path, maintaining backward compatibility with version 1 peers.

Handling Fragmented Packets

Large payloads—file transfers, extended messages—are fragmented across multiple packets. The [BLEFragmentHandler.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFragmentHandler.swift) ensures critical routing consistency:

  • All fragments inherit the parent packet's version 2 header
  • All fragments carry the exact same route list

This design prevents partial delivery: without it, individual fragments might fall back to flooding at different rates, causing reassembly failures at the destination.

Security and Failure Handling

Signature Protection

The Ed25519 signature covers the complete packet: header, sender/recipient IDs, source route, and payload. Relay nodes cannot modify routes without detection. The destination verifies this signature and drops any packet with integrity failures.

Route Failure Caching

The BLESourceRouteFailureCache implements adaptive back-off:

  1. Timeout detection: 10 seconds without acknowledgment from next hop
  2. Failure recorded with timestamp
  3. Route origination suppressed for 60 seconds
  4. Retry permitted after back-off expires

This balances rapid failure response against premature abandonment of potentially viable paths.

Summary

BitChat's BLE mesh source routing delivers these key capabilities:

  • Deterministic unicast paths embedded directly in packets, eliminating per-hop routing decisions
  • Bidirectional edge confirmation via MeshTopologyTracker for reliable topology knowledge
  • Graceful degradation to broadcast flooding when routes fail or peers lack version 2 support
  • Cryptographic integrity protecting route lists from tampering by intermediate relays
  • Adaptive failure handling through BLESourceRouteFailureCache to avoid persistent broken paths

The implementation spans five primary source files, with BLEService.swift containing the critical packet processing loop that makes source routing operational.

Frequently Asked Questions

How does BitChat prevent malicious relays from altering source routes?

The Ed25519 signature covers the entire packet including the source route list. Any modification by an intermediate relay invalidates the signature, causing the destination to reject the packet. Relays can only forward packets intact or drop them entirely—they cannot redirect traffic without detection.

What happens when a source-routed packet cannot reach its next hop?

BitChat implements a two-level fallback: first, the specific sendMessage(to:) call fails and triggers the flood fallback in handleIncomingPacket. The packet is then broadcast to all neighbors. If this still fails (or the original route repeatedly fails), the BLESourceRouteFailureCache records the failure and suppresses route origination to that recipient for 60 seconds.

Why does BitChat limit source routes to 4 hops maximum?

The BLESourceRouteOriginationPolicy enforces a 4-hop limit for two reasons: latency control, as each BLE hop introduces measurable delay in a mesh network, and version compatibility, ensuring every relay in the path has demonstrated version 2 capability and can properly parse source-routed packets. Longer paths risk including legacy peers that would drop or mishandle the packet.

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 →