How Bitchat Android Implements Multi-Hop Relay for Bluetooth Mesh: A Deep Dive into the Source Code

Bitchat Android implements multi-hop relay for Bluetooth mesh through a three-component architecture—BluetoothMeshService, PacketProcessor, and PacketRelayManager—that supports both source-route forwarding and TTL-based adaptive broadcast relay with up to 7 hops.

The permissionlesstech/bitchat-android repository builds a decentralized messaging mesh over Bluetooth Low Energy (BLE). Multi-hop relay enables messages to traverse intermediate devices when sender and recipient are not in direct range. This article examines the actual Kotlin implementation, following packet flow from broadcast origination to final delivery.

Core Architecture: Three Components for Mesh Relay

Bitchat's relay system delegates responsibility across three tightly-coupled classes. Understanding their interaction is essential to tracing how a packet propagates across multiple hops.

BluetoothMeshService: The Mesh Orchestrator

BluetoothMeshService (app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt) serves as the top-level API and packet entry point:

  • Exposes sendMessage() and sendFileBroadcast() for application-layer calls
  • Provides the local peer ID (myPeerID) used in routing decisions
  • Delegates all outbound broadcasts to connectionManager.broadcastPacket()
  • Hands every inbound packet to PacketProcessor for routing decisions
// From BluetoothMeshService.kt
fun sendMessage(text: String) {
    val packet = createMessagePacket(text)
    val signed = signPacket(packet)
    connectionManager.broadcastPacket(RoutedPacket(signed))
}

PacketProcessor: Security Validation and Routing Dispatch

PacketProcessor (app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt) decodes raw BLE frames and determines handler routing:

  • Validates packet security via delegate?.validatePacketSecurity
  • Handles packet types: ANNOUNCE, MESSAGE, VOICE_FRAME, FRAGMENT, and others
  • Invokes delegate?.relayPacket(routed) for packets not destined for the local peer

This is the junction where relay eligibility is determined before PacketRelayManager executes the actual forwarding logic.

PacketRelayManager: The Relay Decision Engine

PacketRelayManager (app/src/main/java/com/bitchat/android/mesh/PacketRelayManager.kt) contains the core multi-hop implementation. It manages two relay modes:

Mode Description Use Case
Source-route relay Explicit hop list in packet.route; forwards to next specified peer Guaranteed path, low latency
TTL-based broadcast relay Probabilistic rebroadcast based on network size Discovery, dynamic topology

Packet Flow Across Multiple Hops

Tracing a message from origin to destination reveals how these components coordinate:

  1. Origination: BluetoothMeshService.sendMessage() creates and signs a BitchatPacket
  2. Initial broadcast: connectionManager.broadcastPacket() transmits via BLE GATT
  3. Reception: BluetoothConnectionManager.onPacketReceived() triggers PacketProcessor.processPacket()
  4. Validation: Security checks pass; non-local packets trigger delegate?.relayPacket(routed)
  5. Relay decision: PacketRelayManager.handlePacketRelay() executes:
    • Drops if isPacketAddressedToMe is true
    • Decrements packet.ttl; drops if zero
    • For source-routed packets: finds self in packet.route, forwards to next hop
    • For non-routed packets: evaluates shouldRelayPacket() for broadcast rebroadcast

The cycle repeats at each intermediate device until TTL expiration or final delivery.

TTL Handling and Loop Prevention

PacketRelayManager enforces the BLE hop limit through strict TTL management:

// From PacketRelayManager.kt, lines 58-62
if (packet.ttl <= 0u) {
    log("Dropping packet: TTL expired")
    return
}
val decrementedTtl = packet.ttl - 1u

Each hop decrements TTL before forwarding. The maximum of 7 hops aligns with BLE mesh specifications, preventing infinite propagation in cyclic topologies.

Duplicate detection complements TTL enforcement. The source-route implementation checks for routing loops by validating that the next hop hasn't already appeared earlier in the path (lines 79-84).

Source-Route Implementation: Explicit Multi-Hop Paths

When packet.route is populated, PacketRelayManager implements deterministic forwarding:

// From PacketRelayManager.kt, lines 86-104 (conceptual)
val myIndex = route.indexOfFirst { it.contentEquals(myPeerId) }
if (myIndex == -1 || myIndex == route.lastIndex) return

val nextHopIdHex = route[myIndex + 1].toHex()
delegate?.sendToPeer(nextHopIdHex, packetWithDecrementedTtl) 
    ?: fallbackToBroadcast(packet)

Key behaviors:

  • Finds local position in the byte-array route list
  • Directs packet to nextHopIdHex via delegate?.sendToPeer
  • Falls back to broadcast if the direct hop is disconnected

Adaptive Broadcast Relay: Network-Aware Probabilistic Forwarding

When no source-route exists, shouldRelayPacket() (lines 42-70) implements controlled flooding:

// From PacketRelayManager.kt, lines 58-66
private fun shouldRelayPacket(): Boolean {
    if (!isRelayEnabled()) return false
    
    val networkSize = delegate?.getNetworkSize() ?: 0
    val probability = when {
        networkSize <= 10 -> 1.0
        networkSize <= 30 -> 0.85
        networkSize <= 50 -> 0.70
        networkSize <= 100 -> 0.55
        else -> 0.40
    }
    return Random.nextDouble() < probability
}

The probability table balances reliability in small meshes against bandwidth conservation in large deployments. Relay can be globally disabled via debug UI through isRelayEnabled().

Practical Implementation Examples

Sending a Relay-Eligible Broadcast Message

val meshService = BluetoothMeshService(context)
meshService.startServices()

// Automatically relayed through mesh based on TTL and adaptive probability
meshService.sendMessage("Propagating across hops")

Constructing an Explicit Source-Route Packet

// Define exact path: self -> intermediate -> destination
val route = listOf(
    hexStringToByteArray(myPeerId),          // hop 0 (origin)
    hexStringToByteArray("a1b2c3d4e5f6"),   // hop 1 (relay)
    hexStringToByteArray("f1e2d3c4b5a6")    // hop 2 (destination)
)

val packet = BitchatPacket(
    version = 1u,
    type = MessageType.MESSAGE.value,
    senderID = hexStringToByteArray(myPeerId),
    recipientID = null,               // null = follow route field
    timestamp = System.currentTimeMillis().toULong(),
    payload = "Routed delivery".toByteArray(),
    ttl = 7u,                         // maximum BLE hops
    route = route                     // explicit path
)

val signed = signPacketBeforeBroadcast(packet)
connectionManager.broadcastPacket(RoutedPacket(signed))

Key Source Files for Multi-Hop Relay

File Multi-Hop Responsibility
BluetoothMeshService.kt API entry point, packet origination
PacketProcessor.kt Validation, routing dispatch
PacketRelayManager.kt TTL management, source-route forwarding, adaptive broadcast logic
MessageHandler.kt Implements relayPacket() delegate interface
BluetoothConnectionManager.kt Low-level BLE transmission (broadcastPacket, sendPacketToPeer)

Summary

  • Three-component architecture: BluetoothMeshService orchestrates, PacketProcessor validates and dispatches, PacketRelayManager decides and executes forwarding
  • Two relay modes: Source-route for explicit paths, TTL-probabilistic broadcast for dynamic discovery
  • TTL enforcement: Strict decrement per hop with 7-hop BLE maximum; packets expire at zero
  • Adaptive relay probability: Network-size-driven rebroadcast rates from 100% (≤10 peers) to 40% (>100 peers)
  • Loop prevention: Duplicate-hop detection in source routes complements TTL expiration

Frequently Asked Questions

What is the maximum hop count in Bitchat's Bluetooth mesh?

Bitchat Android enforces a maximum of 7 hops, matching BLE mesh specifications. The ttl field in BitchatPacket decrements at each relay; packets with ttl == 0 are dropped immediately. This limit appears in the TTL handling logic at lines 58-62 of PacketRelayManager.kt.

How does Bitchat prevent infinite relay loops?

Two mechanisms work together: TTL expiration terminates packets after 7 hops regardless of path, and source-route duplicate detection prevents revisiting nodes when explicit routes are used. The adaptive broadcast probability also reduces rebroadcast frequency in large networks, statistically limiting propagation scope.

Can relay functionality be disabled for testing?

Yes. PacketRelayManager provides isRelayEnabled(), controllable through the debug UI. When disabled, shouldRelayPacket() returns false for all packets, making the device receive-only. This toggle is useful for isolating mesh segments during development or troubleshooting.

When should I use source-route versus broadcast relay?

Use source-route relay when you know the topology and need guaranteed delivery with minimal latency—it directs packets through specific intermediaries without probabilistic flooding. Use TTL-based broadcast relay for peer discovery, dynamic networks, or when path information is unavailable; the adaptive probability automatically adjusts for network density.

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 →