# How Multi-Hop Message Relay Works in Bitchat’s BLE Mesh Using TTL-Based Routing

> Discover how Bitchat's BLE mesh utilizes TTL-based routing for multi-hop message relay. Learn about topology-aware caps and random jitter to prevent flooding and collisions.

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

---

**Bitchat implements multi-hop message relay by decrementing a TTL (Time-to-Live) field at every hop, with relay decisions made by the `RelayController` that applies topology-aware caps and random jitter to prevent flooding and collisions.**

Bitchat is an open-source Bluetooth Low Energy (BLE) mesh messaging application that enables decentralized communication without internet infrastructure. The repository implements a deterministic **multi-hop message relay** system using **TTL-based routing** to propagate packets across sparse or dense network topologies while preventing infinite forwarding loops.

## Packet Reception and Context Extraction

When a BLE packet arrives at a node, the `BLEService` delegates processing to the `BLEReceivePipeline`. This pipeline parses the raw advertisement data and constructs a `BLEReceivedPacketContext` containing the sender identifier, message ID, and current TTL value.

In [`BLEReceivePipeline.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEReceivePipeline.swift) (lines 13‑25), the static method `relayDecision` forwards this metadata to the routing engine. This initial extraction phase isolates the relay logic from transport-specific BLE handling, ensuring the TTL and peer information are available for routing calculations.

## The TTL-Based Routing Decision Engine

The core routing policy resides in [`RelayController.swift`](https://github.com/permissionlesstech/bitchat/blob/main/RelayController.swift) (lines 12‑95). The `decide` function implements the **TTL-based routing** logic that determines whether a packet should be forwarded and with what remaining hop budget.

The decision flow follows three strict rules:

1. **Initial Capping**: The incoming TTL is first capped by the global default `TransportConfig.messageTTLDefault`.
2. **Topology-Aware Limits**: Different traffic types receive specific TTL constraints before decrementing.
3. **Termination Check**: If the resulting TTL is ≤ 1, the packet is consumed locally and not relayed.

When the node decides to relay, the function returns a `RelayDecision` struct containing `shouldRelay` (boolean), the decremented `newTTL` (integer), and a `delayMs` value (integer) calculated from random jitter.

### Directed Traffic Handling

For **directed traffic** such as handshake messages, encrypted direct messages, and ping packets, the relay policy is aggressive. These packets are always forwarded provided the TTL allows, with the value decremented by exactly one. A small random jitter is added to the transmission delay to prevent synchronized collisions when multiple nodes relay the same packet simultaneously.

### Fragmented and Voice Traffic

**Fragmented packets** and voice frames use graph-density-aware caps defined by `bleFragmentRelayTtlCap` (sparse networks) or `bleFragmentRelayTtlCapDense` (dense networks). This prevents high-bandwidth traffic from saturating well-connected mesh regions while still allowing propagation across sparse topologies. The cap is applied before the TTL decrement.

### Broadcast Traffic

**Broadcast packets**—including node announcements and board posts—receive a calculated TTL limit based on the local node's perception of network degree. Sparse topologies retain the full hop budget for long-range propagation, while dense networks are clamped to reduce unnecessary fan-out and battery drain.

## Scheduling and Broadcasting the Relay

Once the `RelayController` approves forwarding, `BLEService.scheduleRelayIfNeeded` (lines 76‑95 in [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift)) executes the transmission. This method creates a deep copy of the original `BitchatPacket`, overwrites its `ttl` field with `decision.newTTL`, and enqueues a broadcast work item.

The scheduling system respects the `delayMs` value from the routing decision, transmitting the packet after the jitter delay expires. This guarantees that every relay hop reduces the TTL exactly once, providing a deterministic hop count that downstream receivers can reconstruct.

## Hop Count Diagnostics with Origin TTL

Bitchat leverages the TTL mechanism for network diagnostics through the `MeshPingPayload` structure defined in [`MeshPingPayload.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MeshPingPayload.swift) (lines 15‑55). Each ping packet embeds an `originTTL` field set to the initial TTL value at the source.

Receivers calculate the exact traversal distance using the static method `hopCount(originTTL:receivedTTL:)`, which computes `originTTL – receivedTTL + 1`. This metric powers the mesh ping round-trip time (RTT) measurement, allowing nodes to map network topology and link quality without additional probe traffic.

```swift
// Construct a packet with default TTL for multi-hop propagation
let pkt = BitchatPacket(
    type: MessageType.announce.rawValue,
    ttl: TransportConfig.messageTTLDefault,
    senderID: myPeerID.rawData,
    recipientID: nil,
    payload: somePayloadData,
    timestamp: UInt64(Date().timeIntervalSince1970 * 1000)
)

// Transmit via BLE service; relay logic triggers automatically
bleService.sendPacket(pkt)

// Calculate hops from a received mesh ping
func handleMeshPing(_ pkt: BitchatPacket, fromLink link: PeerID) {
    guard let ping = try? MeshPingPayload(data: pkt.payload) else { return }
    let hops = MeshPingPayload.hopCount(
        originTTL: ping.originTTL,
        receivedTTL: pkt.ttl
    )
    print("Ping traversed \(hops ?? 0) hop(s)")
}

```

## Summary

- **TTL Decrement Guarantees Termination**: Every relay hop in `BLEService` decrements the TTL by one, ensuring packets expire after a fixed number of hops and preventing infinite loops.
- **Topology-Aware Capping**: The `RelayController` applies different TTL limits for directed, fragmented, and broadcast traffic based on network density, optimizing for both sparse long-range and dense short-range scenarios.
- **Jitter Prevents Collisions**: Random delay injection in `RelayDecision` desynchronizes simultaneous relays from multiple neighbors.
- **Diagnostic Transparency**: `MeshPingPayload` carries the origin TTL, enabling precise hop-count calculation without protocol overhead.

## Frequently Asked Questions

### How does Bitchat prevent infinite packet loops in the BLE mesh?

The **TTL-based routing** mechanism decrements the Time-to-Live field at every hop. When `RelayController.decide` calculates a `newTTL` ≤ 1, the packet is consumed locally and never scheduled for broadcast. This hard limit guarantees that every packet dies after at most `TransportConfig.messageTTLDefault` hops, regardless of network cycles or redundant paths.

### What is the difference between how directed messages and broadcast messages are relayed?

**Directed messages** (handshakes, encrypted DMs, pings) are relayed with the TTL decremented by one and minimal jitter, prioritizing delivery speed. **Broadcast messages** (announces, board posts) receive a dynamic TTL cap based on local network density before decrementing; dense networks see reduced hop limits to prevent flooding, while sparse networks allow full propagation budgets.

### How can a node determine how many hops a message traveled?

Nodes call `MeshPingPayload.hopCount(originTTL:receivedTTL:)` which calculates `originTTL – receivedTTL + 1`. The `originTTL` is embedded in the ping payload at the source, while `receivedTTL` is the value in the packet header when it arrives. This subtraction yields the exact number of relay hops traversed.

### Where is the relay delay jitter calculated in the source code?

The jitter delay is calculated inside [`RelayController.swift`](https://github.com/permissionlesstech/bitchat/blob/main/RelayController.swift) within the `decide` function (around lines 12‑95). The resulting `delayMs` value is packaged into the `RelayDecision` struct and consumed by `BLEService.scheduleRelayIfNeeded` in [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift), which schedules the actual broadcast after the specified millisecond delay.