# How BLE Services in BitChat Facilitate Multi-Hop Message Relay in the Mesh Network

> Discover how BitChat's BLE services enable multi-hop message relay in mesh networks through scheduled relays, TTL routing, and crypto deduplication. Learn more.

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

---

**BitChat uses a three-layer BLE architecture—link layer, mesh engine, and feature modules—to forward messages across multiple hops through scheduled relays with jitter, TTL-based routing, and cryptographic deduplication.**

BitChat's Bluetooth Low Energy (BLE) services implement a complete mesh transport that lets messages traverse multiple intermediate devices to reach peers outside direct radio range. The system combines CoreBluetooth primitives with a protocol engine that handles packet reassembly, routing decisions, and storm-controlled forwarding. According to the BitChat source code, this design achieves deadlock-free concurrency while preserving end-to-end authenticity across arbitrarily long relay chains.

## The Three-Layer BLE Mesh Architecture

BitChat separates concerns across three tightly-coupled layers, each with distinct responsibilities and ownership rules.

### BLE Link Layer: Raw Connectivity Without Protocol Knowledge

The **BLE link layer** owns all CoreBluetooth objects and manages the physical radio interface. It scans for peripherals, handles advertising, maintains per-link back-pressure buffers, and emits `LinkEvent` notifications to the engine.

In [`bitchat/Services/BLE/BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift), the link layer operates on a dedicated `bleQueue`:

```swift
// Lines 31-38: Link-layer queue and event emission
private let bleQueue = DispatchQueue(label: "co.bitchat.ble", qos: .userInitiated)

```

This layer is intentionally **protocol-agnostic**. It knows nothing about packets, peer identities, or Noise cryptography. Its sole job is reliable byte transport and queue management.

### Mesh Engine: Centralized State on a Serial Queue

The **mesh engine** runs exclusively on a single serial `messageQueue`. It owns all protocol state:

- Packet codec and fragmentation
- Deduplication tracking
- Relay policy and routing decisions
- Peer registry and topology mapping
- Noise authentication and link bindings

As documented in [`docs/BLE-ARCHITECTURE-V3.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/BLE-ARCHITECTURE-V3.md) (lines 39-45), the engine is the **sole owner of mesh state**. When the link layer receives complete packets, it calls `ingestDecodedPacket(linkID:packet:)` to hand them to the engine, which then attributes the sender via engine-owned `BLELinkAuthState` and `BLELinkBindings`.

### Feature Modules: Registered Handlers and Routing Policy

Feature modules (courier, private-media, voice, etc.) register packet handlers with the engine. The critical **relay logic** lives in `BLERouteForwardingPolicy`, which the engine consults to decide whether and when to forward packets.

## The Multi-Hop Relay Flow

Messages traverse the mesh through five coordinated steps:

### 1. Receive and Reassemble

BLE peripherals send fragments. The link layer's `BLEFragmentAssemblyBuffer` collects these until a complete packet is decoded, then calls the engine's ingestion method.

### 2. Attribute the Sender

The engine verifies the sender's identity through its stored link bindings:

```swift
// Engine-owned link authentication
let sender = linkAuth.identity(for: linkID)
assertLinkIdentityEngineOwned() // Debug guard enforcing ownership

```

This attribution happens entirely within the engine's `messageQueue` context.

### 3. Routing Decision with Storm Protection

`BLERouteForwardingPolicy.plan()` evaluates:

- **TTL** (time-to-live) remaining
- **Hop count** already traversed
- **Mesh topology** from `MeshTopologyTracker`

If relaying is warranted, the policy hands the packet to `BLEScheduledRelayStore` with a computed jitter delay:

```swift
// In BLERouteForwardingPolicy.plan(...)
if shouldRelay {
    let jitter = computeJitter(for: packet, linkID: incomingLink)
    scheduledRelays.scheduleRelay(packet, after: jitter)
}

```

The jitter algorithm uses `highDegreeThreshold` and `messageTTL` to prevent broadcast storms.

### 4. Execute Relay on Other Links

When the jitter timer fires, the engine enqueues writes on **all other writable links**:

```swift
writeOrEnqueueIfAccepted(packet, on: linkID)

```

Because the engine owns routing policy, the same packet relays across multiple hops **without re-signing**, preserving end-to-end Noise authenticity.

### 5. Deduplication Prevents Loops

Before any re-broadcast, `MessageDeduplicator` checks the packet's inner ID:

```swift
// BLEService.swift, lines 50-52
let messageDeduplicator = MessageDeduplicator()

```

If this node has already processed the packet, it's dropped immediately. This prevents infinite relay loops in cyclic mesh topologies.

## Concurrency Safety and Queue Contracts

The architecture guarantees **deadlock-free operation** through strict queue discipline:

- The `bleQueue` **only async-dispatches** to `messageQueue` and never synchronously waits
- The engine may synchronously wait on the link layer **only** through `readLinkState` with special guards

These contracts are enforced by `dispatchPrecondition` checks and verified by `BLEQueueContractTests`. The critical sync edge in [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift) (lines 96-105) includes this guard:

```swift
onEngine {
    // Engine-confined work only
}

```

## Testing Multi-Hop Relay Without Hardware

BitChat provides a deterministic simulator for validating relay behavior. `SimulatedMesh` wires real `BLEService` instances together without physical radios.

A representative test from [`SimulatedMeshTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SimulatedMeshTests.swift) (lines 66-73):

```swift
func test_oneRelayHop() async {
    let mesh = SimulatedMesh(nodeCount: 3)   // Three BLEService instances
    await mesh.pumpUntilStable()
    XCTAssertTrue(mesh.node(2).received(packetFrom: mesh.node(0)))
}

```

This confirms that node 0's message reaches node 2 via node 1 as an intermediate relay—with proper jitter timing and storm bounds enforced.

## Practical Code Examples

### Broadcasting a Self-Signed Message

```swift
func sendMessage(_ content: String) {
    // Build and sign with Noise
    let signedPacket = noiseService.signPacket(basePacket)!
    
    // Prevent processing our own broadcast copy
    let dedupID = BLESelfBroadcastTracker.dedupID(for: signedPacket)
    messageDeduplicator.markProcessed(dedupID)
    
    // Engine enqueues for link-layer transmission
    broadcastPacket(signedPacket)
}

```

Key operations: packet signing (lines 71-78), deduplication priming (lines 84-86), and broadcast enqueue (lines 89-90) in [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift).

### Scheduling Relay with Jitter

```swift
// BLEService.swift, lines 122-124
scheduledRelays = BLEScheduledRelayStore()

// Inside routing policy execution
scheduledRelays.scheduleRelay(packet, after: computedJitter)

```

### Inbound Packet Processing Pipeline

```swift
func ingestDecodedPacket(_ packet: BitchatPacket, linkID: BLEIngressLinkID) {
    onEngine {
        // 1. Attribute sender
        let sender = linkAuth.identity(for: linkID)
        
        // 2. Plan routing
        let relayPlan = BLERouteForwardingPolicy.plan(
            packet, 
            from: sender, 
            topology: meshTopology
        )
        
        // 3. Execute (may schedule relays)
        relayPlan.execute()
    }
}

```

## Key Source Files

| File | Purpose |
|------|---------|
| [`bitchat/Services/BLE/BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift) | Core BLE service—link layer queues, relay stores, deduplication |
| [`docs/BLE-ARCHITECTURE-V3.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/BLE-ARCHITECTURE-V3.md) | High-level architecture and engine ownership model |
| [`bitchat/Services/BLE/BLERouteForwardingPolicy.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLERouteForwardingPolicy.swift) | Routing policy implementation |
| [`bitchat/Services/BLE/BLEScheduledRelayStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEScheduledRelayStore.swift) | Jittered relay scheduling |
| [`bitchatTests/Simulation/SimulatedMesh.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Simulation/SimulatedMesh.swift) | Deterministic multi-node test harness |
| [`bitchatTests/Simulation/SimulatedMeshTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Simulation/SimulatedMeshTests.swift) | Relay and storm-bound validation |

## Summary

- **Three-layer separation**: Link layer handles radios, engine owns protocol state, features register handlers
- **Jittered relay scheduling**: `BLEScheduledRelayStore` with `BLERouteForwardingPolicy` prevents broadcast storms
- **Engine-centralized routing**: All forwarding decisions happen on the serial `messageQueue` with deadlock-free queue contracts
- **Cryptographic continuity**: Messages relay without re-signing, maintaining Noise end-to-end authenticity
- **Loop prevention**: `MessageDeduplicator` drops packets already seen at each node
- **Deterministic testing**: `SimulatedMesh` validates multi-hop behavior without physical hardware

## Frequently Asked Questions

### How does BitChat prevent infinite relay loops in mesh networks?

The `MessageDeduplicator` in [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift) tracks packet inner IDs before any re-broadcast. If a node has already processed a packet, it's dropped immediately. Additionally, `BLERouteForwardingPolicy` respects TTL and hop-count limits, ensuring packets expire after traversing the configured maximum path length.

### What prevents broadcast storms when many devices relay simultaneously?

BitChat uses **jittered relay scheduling** via `BLEScheduledRelayStore`. The routing policy computes a per-packet delay based on `highDegreeThreshold` and remaining `messageTTL`. This randomizes transmission timing across nodes, spreading relay traffic over time rather than allowing synchronized flooding.

### Why does the mesh engine use a single serial queue?

The `messageQueue` serializes all protocol state mutations—peer registry, link bindings, topology, and routing decisions. This eliminates data races without complex locking. The link layer's `bleQueue` communicates via async dispatch only, and the engine uses guarded synchronous reads (`readLinkState`) when it needs link-layer information.

### Can BitChat's BLE mesh operate without internet connectivity?

Yes. The architecture is explicitly **offline-first**. All relay decisions happen locally in `BLERouteForwardingPolicy` using the `MeshTopologyTracker`. No central coordination, DHT, or internet gateway is required for multi-hop message delivery between peers within Bluetooth range of the connected mesh component.