# How BitChat's BLE Mesh Handles Multi-Hop Message Relaying with TTL

> Discover how BitChat's BLE mesh network expertly manages multi-hop message relaying using TTL. Learn about hop-limited flooding and hop count enforcement.

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

---

**BitChat implements hop-limited flooding through an 8-bit TTL field in `BitchatPacket` and the `RelayController.decide()` method, which decrements TTL at each hop while enforcing hard caps via `TransportConfig.messageTTLDefault` to prevent infinite loops.**

BitChat is an open-source, peer-to-peer messaging application from permissionlesstech/bitchat built on Bluetooth Low Energy (BLE) mesh networking. Understanding how **BitChat's BLE mesh multi-hop message relaying with TTL** works is essential for developers building decentralized networks that avoid broadcast storms and endless routing loops while maximizing reach across BLE's limited range.

## TTL-Based Packet Structure

Every message traversing the BitChat mesh carries an 8-bit `ttl` field within the `BitchatPacket` structure defined in [`BitchatPacket.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BitchatPacket.swift). This counter limits how many hops a message may travel before automatically expiring.

Critically, while the TTL is part of the packet payload, it is **excluded from the cryptographic signature**. The `toBinaryDataForSigning()` method serializes the packet for signing purposes while omitting the TTL field, allowing intermediate nodes to decrement the counter without invalidating the signature. This design ensures message authenticity remains intact even as the TTL mutates during relay.

## Source Routing and Hop Count Calculation

BitChat tracks message trajectories using optional source routing. Each packet may carry a `route` field containing a list of hop identifiers already traversed. When a node receives a packet, it calculates the current hop count using the formula:

```

hopCount = originTTL - receivedTTL

```

This logic is implemented in [`MeshPingPayload.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MeshPingPayload.swift) within the `hopCount` property. By comparing the original TTL value (stored in the route metadata) against the decremented value received from a neighbor, nodes determine exactly how many relays the packet has crossed, enabling network topology analysis without additional protocol overhead.

## RelayController Flood Control Logic

The `RelayController` class in [`RelayController.swift`](https://github.com/permissionlesstech/bitchat/blob/main/RelayController.swift) centralizes all flood-control policy decisions. Its `decide()` method receives the current TTL alongside boolean flags describing the packet type and the node's graph degree (neighbor count), then returns a `RelayDecision` structure containing `shouldRelay` and the `newTTL` value.

The controller applies several strict policies:

- **Hard TTL Cap**: All packets are clamped to `TransportConfig.messageTTLDefault` (typically 7 hops) regardless of their initial value.
- **Terminal Check**: Packets with `ttl ≤ 1` or self-originated packets are immediately rejected and never forwarded.
- **Decrement Rule**: For eligible packets, the controller computes `newTTL = ttlCap - 1` for the next hop.
- **Fragment Limits**: Voice frames and message fragments receive stricter limits via `bleFragmentRelayTtlCap` and `bleFragmentRelayTtlCapDense` to preserve bandwidth on BLE's constrained MTU.
- **Traffic Shaping**: The controller introduces jitter-based delays to spread broadcast traffic and prevent synchronized storms in dense networks.

## Multi-Hop Relay Flow

When a message propagates through the BitChat mesh, it follows a deterministic relay process:

1. **Origin Creation**: The sender constructs a `BitchatPacket` with an initial TTL set to `TransportConfig.messageTTLDefault`.
2. **Relay Evaluation**: Upon receipt, `BLEService` passes the packet to `RelayController.decide()` along with the node's current degree and packet flags.
3. **TTL Update**: If `shouldRelay` returns true, the service updates the packet TTL to `decision.newTTL`.
4. **Neighbor Broadcast**: The updated packet is broadcast to all BLE peripherals satisfying the fan-out selector.
5. **Hierarchical Decay**: Each receiving neighbor repeats step 2, causing the TTL to decrement until it reaches the terminal threshold or the packet is dropped by policy.

This decrementing counter guarantees that packets cannot circulate indefinitely, creating a deterministic boundary for broadcast propagation.

## Special Cases and Bandwidth Optimization

BitChat applies distinct relay rules based on message type and network topology:

- **Link-Local Control**: `REQUEST_SYNC` packets carry `TTL = 0` and are strictly link-local, never relayed regardless of other flags.
- **Handshake Prioritization**: Handshake and directed encrypted packets bypass normal jitter delays to establish secure sessions quickly.
- **Dense Graph Management**: In highly connected networks, broadcast packets receive aggressive TTL clamping proportional to node degree, preventing congestion while maintaining connectivity.

## Implementation Example

The following Swift code demonstrates creating a relay-eligible packet and processing it through the controller:

```swift
// 1️⃣ Create a packet that will travel the mesh
let payload = Data(/* … */)
let packet = BitchatPacket(
    type: MessageType.text.rawValue,
    senderID: myPeerID.rawValue,
    recipientID: nil,               // broadcast
    timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
    payload: payload,
    signature: nil,
    ttl: TransportConfig.messageTTLDefault, // e.g. 7 hops
    route: nil
)

// 2️⃣ Ask the relay controller whether we may forward it
let decision = RelayController.decide(
    ttl: packet.ttl,
    senderIsSelf: true,
    recipientIsSelf: false,
    isEncrypted: false,
    isDirectedEncrypted: false,
    isFragment: false,
    isDirectedFragment: false,
    isHandshake: false,
    isAnnounce: true,
    isRequestSync: false,
    degree: myNeighbourCount,
    highDegreeThreshold: TransportConfig.highDegreeThreshold
)

// 3️⃣ If the decision says “relay”, update the packet’s TTL and send
if decision.shouldRelay {
    var forward = packet
    forward.ttl = decision.newTTL          // decrement TTL
    bleService.sendMessage(forward)        // BLEService handles neighbour fan‑out
}

```

Internally, `BLEService.handleReceivedPacket` implements the same pattern: extracting the packet, evaluating it through `RelayController.decide`, and either forwarding it with decremented TTL or discarding it based on the returned policy.

## Summary

- **BitChat's BLE mesh** relies on an 8-bit TTL field in [`BitchatPacket.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BitchatPacket.swift) to limit message propagation distance and prevent infinite loops.
- The `RelayController.decide()` method enforces hard caps via `TransportConfig.messageTTLDefault` and decrements TTL at each relay hop.
- **Source routing** enables hop count calculation using `originTTL - receivedTTL` as implemented in [`MeshPingPayload.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MeshPingPayload.swift).
- Special packet types like `REQUEST_SYNC` use `TTL = 0` for link-local communication, while fragments face additional restrictions via `bleFragmentRelayTtlCap`.
- The combination of TTL decrementing, degree-based clamping, and jitter delays creates a bandwidth-aware multi-hop relay mechanism suitable for BLE's constrained environment.

## Frequently Asked Questions

### What happens when a BitChat packet's TTL reaches 1?

When `RelayController.decide()` encounters a packet with `ttl ≤ 1`, it returns a decision where `shouldRelay` is false. The packet is consumed locally but never forwarded to neighbors, effectively terminating its path through the mesh and preventing infinite routing loops.

### Why is the TTL field excluded from the packet signature in BitChat?

The TTL must change at each hop to count down toward zero, but modifying any signed field would invalidate the cryptographic signature. In [`BitchatPacket.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BitchatPacket.swift), the `toBinaryDataForSigning()` method serializes the packet for signing while specifically excluding the mutable TTL field, allowing intermediate nodes to decrement the counter without breaking message authenticity.

### How does BitChat calculate the number of hops a message has traveled?

Nodes derive the hop count by subtracting the received TTL from the original TTL (`originTTL - receivedTTL`). This calculation is available in [`MeshPingPayload.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MeshPingPayload.swift) through the `hopCount` property, allowing nodes to determine network path length and topology without additional protocol overhead.

### What is the difference between `messageTTLDefault` and the fragment TTL caps?

`TransportConfig.messageTTLDefault` sets the maximum hop count for standard messages (typically 7 hops). However, fragments and voice frames use lower caps—`bleFragmentRelayTtlCap` and `bleFragmentRelayTtlCapDense`—to reduce bandwidth consumption on the limited BLE MTU, ensuring that large segmented payloads do not congest the mesh network.