How BitChat's BLE Mesh Handles Multi-Hop Message Relaying with TTL
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. 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 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 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 ≤ 1or self-originated packets are immediately rejected and never forwarded. - Decrement Rule: For eligible packets, the controller computes
newTTL = ttlCap - 1for the next hop. - Fragment Limits: Voice frames and message fragments receive stricter limits via
bleFragmentRelayTtlCapandbleFragmentRelayTtlCapDenseto 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:
- Origin Creation: The sender constructs a
BitchatPacketwith an initial TTL set toTransportConfig.messageTTLDefault. - Relay Evaluation: Upon receipt,
BLEServicepasses the packet toRelayController.decide()along with the node's current degree and packet flags. - TTL Update: If
shouldRelayreturns true, the service updates the packet TTL todecision.newTTL. - Neighbor Broadcast: The updated packet is broadcast to all BLE peripherals satisfying the fan-out selector.
- 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_SYNCpackets carryTTL = 0and 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:
// 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.swiftto limit message propagation distance and prevent infinite loops. - The
RelayController.decide()method enforces hard caps viaTransportConfig.messageTTLDefaultand decrements TTL at each relay hop. - Source routing enables hop count calculation using
originTTL - receivedTTLas implemented inMeshPingPayload.swift. - Special packet types like
REQUEST_SYNCuseTTL = 0for link-local communication, while fragments face additional restrictions viableFragmentRelayTtlCap. - 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, 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 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.
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 →