# How Multi-Hop Message Relay Works in BitChat's BLE Mesh with Source Routing

> Discover how BitChat's BLE mesh uses multi-hop message relay with source routing. Learn how senders embed paths and relays forward messages or fall back to flooding for reliable communication.

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

---

**Multi-hop message relay in BitChat's BLE mesh uses source routing (v2 packets) where the sender computes and embeds the full path through intermediate peers; relays forward only to the next hop while falling back to flooding if any hop is unreachable.**

BitChat's **source-routing extension** enables deterministic multi-hop delivery across its Bluetooth Low Energy (BLE) mesh without flooding every packet. This article explains the complete mechanism—from packet format to relay logic—based on the permissionlesstech/bitchat source code.

## BitChat Packet Versions and the Route Field

BitChat defines two packet versions with different routing capabilities.

### Version 1: Flood-Only Packets

- 2-byte payload length field
- No routing bits; packets rely on **classic flood** mechanism

### Version 2: Source-Routed Packets

- 4-byte length field
- **HAS_ROUTE** flag enabling variable-length *Source Route* field (`COUNT + HOPS`)

The `BinaryProtocol` enforces strict version compatibility. In `BinaryProtocol.encode` (lines 53–66), only packets with `version >= 2` may set the `HAS_ROUTE` flag:

```swift
// BinaryProtocol.swift — encoding validation
if version < 2 && hasRoute {
    throw BinaryProtocolError.invalidVersionForRouting
}

```

Decoding ignores the flag for version 1 packets (`BinaryProtocol.decodeCore`, lines 19–22), ensuring backward compatibility.

The complete specification lives in [[`docs/SOURCE_ROUTING.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/SOURCE_ROUTING.md)](https://github.com/permissionlesstech/bitchat/blob/main/docs/SOURCE_ROUTING.md).

## Topology Discovery: Building the Mesh Graph

Every node advertises its **direct neighbors** via an `IdentityAnnouncement` TLV (type 0x04). The `MeshTopologyTracker` service constructs a **bidirectional adjacency map**, ignoring edges not confirmed by both endpoints.

This graph feeds the shortest-path search used during packet origination.

## Origination Policy: When to Attach a Route

`BLESourceRouteOriginationPolicy` adds a route only when **all six conditions** are met:

1. `SenderID == myPeerID` (locally authored packet)
2. `RecipientID` present (directed unicast, not broadcast)
3. `TTL > 1` (at least one hop available)
4. Recipient is **not** a direct neighbor
5. Confirmed v2 path exists with ≤ 4 intermediate hops, where every hop speaks v2
6. No recent route-failure entry for that recipient

If any condition fails, the packet sends **unrouted** and floods normally.

```swift
// Origination — building a source-routed packet
let route = MeshTopologyTracker.shared.shortestPath(
    from: myPeerID,
    to: recipientID,
    maxHops: 4
)

let packet = BitchatPacket(
    version: 2,
    type: MessageType.chatMessage.rawValue,
    ttl: 5,
    timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
    senderID: myPeerID.id,
    recipientID: recipientID.id,
    route: route.map { $0.id },  // Intermediate hops only
    payload: payload,
    signature: nil
)

if let routed = BLESourceRouteOriginationPolicy.route(packet, localPeerID: myPeerID) {
    BLEService.shared.sendPacket(routed)
}

```

## Relay Decision Logic in BLERouteForwardingPolicy

When `BLEService` receives a non-local packet, it invokes `forwardAlongRouteIfNeeded` (lines 3926–3939), which delegates to `BLERouteForwardingPolicy.plan`:

### Step-by-Step Forwarding Rules

1. **Suppress flood** for link-local `REQUEST_SYNC` or locally-destined packets
2. **No route** → **allow flood**
3. `TTL <= 1` → suppress flood (dead packet)
4. **Locate self in route list**:
   - **Not on route**: forward to `route[0]` (first hop)
   - **Last hop before destination**: forward directly to `RecipientID`
   - **Intermediate hop**: forward to `route[localIndex + 1]` (next hop)
5. **Forward only if next hop is connected**; otherwise fall back to **allow flood**

The `plan` method returns three fields:

| Field | Purpose |
|-------|---------|
| `shouldSuppressFloodRelay` | `true` skips normal flood after forwarding |
| `forwardPacket` | Packet with decremented `TTL` |
| `nextHop` | `PeerID` of next hop, or `nil` if flooding |

```swift
// Relay-side handling
func handleIncoming(_ raw: Data) {
    guard let pkt = BinaryProtocol.decode(raw) else { return }
    
    let suppressed = BLEService.shared.forwardAlongRouteIfNeeded(pkt)
    
    if !suppressed {
        BLEService.shared.floodPacket(pkt)  // Normal flood fallback
    }
}

```

On successful forward, `BLEService.sendPacketDirected` transmits to the calculated next hop and returns `true` to skip flooding.

## Failure Handling and Route Cache

Two mechanisms ensure reliability when source routes fail:

### Immediate Fallback

If the next hop is unreachable during `plan` execution, the policy returns `.allowFloodRelay`, triggering **re-broadcast to all neighbors**.

### Source-Route Failure Cache

`BLESourceRouteFailureCache` records routed sends that receive no response within 10 seconds. Subsequent sends to that peer flood for 60 seconds before attempting source routing again.

```swift
// Record potential failure after routed send
BLESourceRouteFailureCache.shared.recordIfNoResponse(
    to: recipientID,
    within: 10.0
)

```

## End-to-End Multi-Hop Flow

1. **Topology discovery**: Neighbor TLVs build the bidirectional mesh graph
2. **Origination**: `BLESourceRouteOriginationPolicy` runs BFS, constructs `route`, upgrades to v2, signs, and hands to `BLEService`
3. **Relay**: Each intermediate device executes `BLERouteForwardingPolicy.plan`, forwards to next hop, and suppresses flood on success
4. **Destination**: Validates signature (covering the route) and processes payload

This design provides **deterministic, low-overhead unicast** across up to four hops while preserving the flood safety net of the original mesh.

## Summary

- **Source routing** in BitChat uses v2 packets with embedded `HAS_ROUTE` flag and hop list
- **`BLESourceRouteOriginationPolicy`** attaches routes only for qualified unicasts with ≤ 4 confirmed v2 hops
- **`BLERouteForwardingPolicy`** directs relays to forward strictly to next hop when connected
- **Flood fallback** activates when routes are stale, hops are unreachable, or TTL expires
- **`BLESourceRouteFailureCache`** prevents repeated failed routed attempts with 60-second cooldown

## Frequently Asked Questions

### What is the maximum hop count for source-routed packets in BitChat?

BitChat limits source routes to **4 intermediate hops**. The origination policy enforces this maximum to balance delivery reliability against latency and packet overhead. Beyond 4 hops, packets send unrouted and rely on classic flooding.

### How does BitChat prevent routing loops with source routing?

Source routing inherently prevents loops because the **sender computes the complete path** and embeds it in the packet. Relays never make independent forwarding decisions—they simply locate themselves in the precomputed list and forward to the specified next hop. The `TTL` field provides additional protection; packets with `TTL <= 1` are never forwarded.

### What happens if an intermediate relay in the route goes offline?

If a relay's next hop is **not currently connected**, `BLERouteForwardingPolicy` returns `.allowFloodRelay`, causing the packet to **flood to all neighbors** instead of following the broken route. Additionally, the failure is recorded in `BLESourceRouteFailureCache`, triggering flood-only mode for that destination for 60 seconds while the mesh topology updates.

### Why does BitChat use source routing instead of dynamic routing tables?

Source routing minimizes **per-relay state and computation**—relays need not maintain or consult routing tables, reducing memory and processing overhead on BLE-constrained devices. The sender bears the path computation cost once, and intermediate devices execute simple position-based forwarding. This tradeoff suits BitChat's decentralized, smartphone-based mesh where topology changes frequently.