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()andsendFileBroadcast()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
PacketProcessorfor 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:
- Origination:
BluetoothMeshService.sendMessage()creates and signs aBitchatPacket - Initial broadcast:
connectionManager.broadcastPacket()transmits via BLE GATT - Reception:
BluetoothConnectionManager.onPacketReceived()triggersPacketProcessor.processPacket() - Validation: Security checks pass; non-local packets trigger
delegate?.relayPacket(routed) - Relay decision:
PacketRelayManager.handlePacketRelay()executes:- Drops if
isPacketAddressedToMeis 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
- Drops if
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
nextHopIdHexviadelegate?.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:
BluetoothMeshServiceorchestrates,PacketProcessorvalidates and dispatches,PacketRelayManagerdecides 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →