How Multi-Hop Relays Extend BitChat's Bluetooth Range: A Technical Deep Dive

Multi-hop relays in BitChat create a Bluetooth Low Energy mesh network where messages hop from device to device, extending communication range far beyond the standard 10-meter Bluetooth limit through TTL-controlled forwarding, signed topology announcements, and deterministic relay policies.

BitChat is an open-source, permissionless messaging protocol developed by permissionlesstech/bitchat that enables offline communication via Bluetooth Low Energy (BLE). While a single Bluetooth connection is typically limited to roughly 10 meters, the implementation of multi-hop relays allows the network to function as a mesh, forwarding encrypted messages through intermediate devices to reach peers that are several hops away. This article examines the specific Swift implementations and architectural decisions that enable this range extension.

Bluetooth Mesh Architecture Overview

BitChat separates concerns between the radio hardware and the routing logic to make multi-hop relaying reliable and testable. According to docs/BLE-ARCHITECTURE-V3.md (lines 31-38), the architecture isolates all CoreBluetooth interaction within the BLELinkLayer, while a dedicated mesh engine running on a serial queue owns all protocol state and routing decisions.

This layered design means the same deterministic multi-hop routing algorithm runs both in simulation environments and on physical devices. The mesh engine maintains neighbor tables and topology maps, allowing the system to forward packets across multiple intermediate hops without requiring direct radio contact between the original sender and final recipient.

Topology Discovery via Signed Announcements

Before messages can traverse multiple hops, devices must discover the network topology. Each node periodically broadcasts a SignedAnnouncement containing up to ten neighbor IDs, as documented in WHITEPAPER.md (lines 71-73).

// Every device emits a signed announcement roughly every 4 seconds (isolated)
// or every 15-30 seconds when part of a connected mesh.
func broadcastAnnouncement() {
    let announcement = SignedAnnouncement(neighbors: knownNeighborIDs)
    bleService.send(announcement, ttl: maxTTL)
}

These announcements are flooded through the mesh, allowing every node to build a shallow view of the local graph. When a bidirectional path exists between sender and recipient, packets are source-routed along that specific path. If no direct path is known, the system falls back to controlled flooding, ensuring messages can still reach distant nodes via multi-hop relays even when the complete topology is unknown.

The Relay Decision Engine

The core intelligence for multi-hop forwarding resides in bitchat/Services/RelayController.swift (lines 39-48). The RelayController.decide() method evaluates every inbound packet to determine whether it should be forwarded, how much TTL (Time To Live) to preserve, and what delay to insert for network desynchronization.

let decision = RelayController.decide(
    ttl: packet.ttl,
    senderIsSelf: senderID == localPeerID,
    recipientIsSelf: PeerID(hexData: packet.recipientID) == localPeerID,
    isEncrypted: packet.type == MessageType.noiseEncrypted.rawValue,
    isDirectedEncrypted: /* DM, courier, ping/pong, nostrCarrier */ true,
    isFragment: packet.type == MessageType.fragment.rawValue,
    isDirectedFragment: packet.type == MessageType.fragment.rawValue && packet.recipientID != nil,
    isHandshake: packet.type == MessageType.noiseHandshake.rawValue,
    isAnnounce: packet.type == MessageType.announce.rawValue,
    degree: currentDegree,
    highDegreeThreshold: 6
)

The method returns a RelayDecision struct containing shouldRelay, a decremented newTTL, and a jitter-derived delayMs. Directed traffic such as DMs, Noise handshakes, and courier envelopes receive "always-relay" treatment, guaranteeing they traverse several hops without being truncated by the TTL cap.

Integration with the Receive Pipeline

When a packet arrives via BLE, bitchat/Services/BLE/BLEReceivePipeline.swift (lines 46-57) consults the relay controller to determine forwarding behavior. The pipeline calls relayDecision(), which encapsulates the logic for multi-hop extension.

let relayDecision = BLEReceivePipeline.relayDecision(
    for: packet,
    senderID: senderID,
    localPeerID: localPeerID,
    degree: meshDegree,
    highDegreeThreshold: 6
)

if relayDecision.shouldRelay {
    // Enqueue the packet to be sent after `relayDecision.delayMs`
    transport.scheduleRelay(packet,
                            ttl: relayDecision.newTTL,
                            after: relayDecision.delayMs)
}

If the decision indicates shouldRelay, the pipeline schedules the packet for transmission to the next hop with the updated TTL and a slight jitter delay. This process repeats at each intermediate device until the message reaches its destination or the TTL expires.

Fragmentation for Multi-Hop Delivery

BLE's link layer MTU is approximately 469 bytes, which poses challenges for multi-hop transmission of larger payloads. As noted in WHITEPAPER.md (lines 65-68), BitChat implements fragmentation and reassembly to allow large messages to travel many hops without exceeding the low-MTU limit.

Packets larger than the BLE MTU are split into fragments that are independently relayed through the mesh. Each fragment is treated as a distinct packet by the RelayController, receiving its own TTL and relay decision. Intermediate nodes reassemble fragments before forwarding when possible, ensuring that payload size does not constrain the physical distance a message can travel via multi-hop relays.

Privacy and Security Across Hops

A critical aspect of the multi-hop design is that intermediate relays see only opaque ciphertext. Because end-to-end encryption is applied at the application layer, relay devices cannot read message contents. The transport layer re-encrypts packets for each hop's BLE connection, preserving privacy while still allowing the BLELinkLayer to route packets through untrusted intermediate devices, as referenced in bitchat/Services/BLE/BLEService.swift (lines 4987-5023).

Summary

  • Mesh Architecture: The separation of BLELinkLayer from the mesh engine enables testable, deterministic multi-hop routing that runs identically in simulation and production.
  • Topology Awareness: Signed announcements containing neighbor IDs propagate across hops, enabling source-routed delivery when paths are known and controlled flooding when they are not.
  • Deterministic Relaying: RelayController.decide() implements TTL-aware forwarding with jitter delays, ensuring messages traverse multiple hops without causing network congestion.
  • Fragmentation Support: Large payloads are split into MTU-sized fragments that can be independently relayed and reassembled, preventing size limitations from restricting range extension.
  • Privacy Preservation: End-to-end encryption ensures intermediate multi-hop relays cannot read message contents, only forward opaque packets.

Frequently Asked Questions

How many hops can a BitChat message traverse?

BitChat implements a TTL-based mechanism where RelayController decrements the TTL at each hop. While the exact maximum depends on the initial TTL value and network conditions, the combination of source routing for known paths and controlled flooding for discovery allows messages to reliably travel several hops. The jitter-based delays and TTL caps prevent infinite propagation while ensuring sufficient reach to extend well beyond the 10-meter Bluetooth limit.

What is the difference between source routing and flooding in BitChat?

When a device has a complete topology map from received announcements, it uses source routing, encoding the specific multi-hop path into the packet header. If the destination is unknown, the system falls back to flooding, where BLEReceivePipeline broadcasts the packet to all neighbors except the sender. The RelayController applies different policies to each: source-routed packets follow a predetermined path, while flooded packets rely on TTL limits to control propagation scope.

How does BitChat prevent infinite relay loops?

The TTL (Time To Live) field in every packet prevents infinite loops by decrementing at each hop. When RelayController.decide() processes a packet, it returns a RelayDecision with newTTL set to the current value minus one. If the TTL reaches zero, the packet is discarded rather than relayed. Additionally, the system tracks senderIsSelf to prevent nodes from re-broadcasting packets they originated, and degree-based thresholds help high-connectivity nodes throttle broadcast traffic.

Are multi-hop relays in BitChat battery-intensive?

The relay system is designed for efficiency through jitter-based desynchronization and selective forwarding. Rather than immediately rebroadcasting every received packet, RelayController introduces a small, randomized delay (delayMs) that prevents synchronized flooding storms. Furthermore, only directed messages (DMs, handshakes) receive "always-relay" status with no TTL cap, while broadcast traffic is throttled, conserving battery life on devices acting as intermediate relays.

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 →