How BitChat Achieves Multi-Hop Mesh Networking with TTL-Based Relay Decisions
BitChat implements multi-hop mesh networking by attaching a TTL (time-to-live) field to every packet, decrementing it at each hop and making autonomous relay decisions based on remaining TTL, network density, and message type.
BitChat is an open-source Swift application that builds resilient peer-to-peer communication over Bluetooth Low Energy (BLE). Understanding how BitChat achieves multi-hop mesh networking with TTL-based relay decisions reveals a sophisticated decentralized approach that requires no central coordinator while preventing infinite packet circulation.
The TTL Field: Foundation of Hop-Limited Propagation
Every packet in BitChat carries an originTTL value that establishes its maximum propagation distance. This design enables nodes to calculate how far a packet has traveled and determine whether to continue forwarding it.
Packet Structure and Hop Counting
The MeshPingPayload struct in BitFoundation defines the TTL-bearing payload structure:
// localPackages/BitFoundation/Sources/BitFoundation/MeshPingPayload.swift
public struct MeshPingPayload {
public let originTTL: UInt8 // TTL the packet was launched with
// ...
/// Number of links a packet crossed, derived from TTL decrements
public static func hopCount(originTTL: UInt8,
receivedTTL: UInt8) -> Int? {
guard originTTL >= receivedTTL else { return nil }
return Int(originTTL - receivedTTL) + 1
}
}
When a node receives a packet, it compares receivedTTL against originTTL. The formula originTTL - receivedTTL + 1 yields the number of hops traversed. Invalid packets—where receivedTTL > originTTL—return nil from hopCount() and are discarded.
RelayController: The TTL-Based Decision Engine
The heart of BitChat's mesh networking logic resides in RelayController.swift. This component encapsulates all forwarding logic into a structured decision format.
The RelayDecision Struct
// bitechat/Services/RelayController.swift
struct RelayDecision {
let shouldRelay: Bool
let newTTL: UInt8
let delayMs: UInt64
}
Each incoming packet triggers evaluation across multiple factors:
- Remaining TTL — packets reaching TTL
0are never forwarded - Network density — dense regions apply
TransportConfig.bleFragmentRelayTtlCapDenseto clamp TTL - Message type — certain packets like
REQUEST_SYNCbypass routing regardless of TTL headroom - Fragment size — large BLE fragments face stricter caps via
TransportConfig.bleFragmentRelayTtlCap - Delay budgeting — optional back-off prevents broadcast storms
TTL Decrement and Clamping Logic
When forwarding is approved, the controller calculates the new TTL:
// Inside RelayController.swift (simplified)
let newTTL = max(0, incomingTTL &- 1) // normal hop decrement
return RelayDecision(shouldRelay: true,
newTTL: newTTL,
delayMs: 0)
The bitwise &- operator performs saturating subtraction, ensuring clean handling of edge cases.
End-to-End Message Flow
Understanding the complete propagation path clarifies how TTL-based relay decisions operate in practice:
- Origin node creates a packet with
originTTL = TransportConfig.messageTTLDefault(typically 7) - First-hop recipient receives TTL 7, computes hop count of 1, and queries
RelayController - Controller returns
shouldRelay = truewithnewTTL = 6; packet is re-encoded and broadcast - Subsequent hops repeat the process, decrementing TTL until reaching 0
- TTL exhaustion triggers
shouldRelay = false, halting propagation
This mechanism guarantees deterministic termination—no message can circulate indefinitely.
Adaptive Network Behavior
BitChat's TTL-based mesh networking adapts dynamically to environmental conditions without requiring global coordination.
Density-Aware Throttling
In dense network regions, the controller applies lower TTL caps automatically:
// Conceptual usage within RelayController
let denseRegionCap = TransportConfig.bleFragmentRelayTtlCapDense
let effectiveCap = min(receivedTTL, denseRegionCap)
let newTTL = effectiveCap > 0 ? effectiveCap - 1 : 0
This prevents broadcast storms in crowded environments while preserving reachability in sparse regions.
Security Considerations
The packet signature in BitchatPacket.swift is generated with a fixed TTL of 0, as noted in source comments. This architectural choice allows relays to modify the TTL field without invalidating cryptographic authenticity—each hop can safely decrement TTL without breaking the signature chain.
Practical Implementation Examples
Creating a TTL-Enabled Mesh Ping
import BitFoundation
let nonce = Data([0x01, 0x02, 0x03, 0x04])
let ttl: UInt8 = TransportConfig.messageTTLDefault // usually 7
guard let payload = MeshPingPayload(nonce: nonce,
originTTL: ttl) else {
fatalError("Invalid payload")
}
Making Relay Decisions on Incoming Packets
import Bitchat
func handleIncoming(_ packet: BitchatPacket) async {
let decision = RelayController.makeDecision(
ttl: packet.ttl,
packetType: packet.type,
networkDegree: currentDegree,
highDegreeThreshold: 15)
guard decision.shouldRelay else { return }
var forwarded = packet
forwarded.ttl = decision.newTTL
await BLEBroadcaster.broadcast(forwarded,
afterDelayMs: decision.delayMs)
}
Computing Traversed Hop Count
let hops = MeshPingPayload.hopCount(originTTL: packet.originTTL,
receivedTTL: packet.ttl)
print("Packet traversed \(hops ?? 0) hops")
Key Source Files
| File | Purpose |
|---|---|
localPackages/BitFoundation/Sources/BitFoundation/MeshPingPayload.swift |
Defines originTTL field and hopCount() calculation |
bitechat/Services/RelayController.swift |
Core TTL-based relay decision engine |
bitechat/Services/BLE/BLEReceivePipeline.swift |
Invokes RelayController for each inbound BLE packet |
localPackages/BitFoundation/Sources/BitFoundation/BitchatPacket.swift |
Packet structure with TTL-agnostic signature generation |
bitechat/Config/TransportConfig.swift |
Default TTL values and environment-specific caps |
Summary
- TTL fields enable hop-limited propagation without central coordination
RelayDecisionencapsulates forwarding logic including TTL decrement, clamping, and delay scheduling- Density-aware caps prevent broadcast storms in crowded network regions
- Fixed-TTL signatures preserve security while allowing relay modifications
- Autonomous node decisions scale efficiently across heterogeneous mesh topologies
Frequently Asked Questions
How does BitChat prevent infinite packet circulation?
BitChat prevents infinite circulation through TTL exhaustion. Each relay decrements the TTL by at least one. When TTL reaches zero, RelayController.makeDecision() returns shouldRelay = false, permanently halting that packet's propagation. The maximum hop count equals the originTTL value set by the sender.
What happens when received TTL exceeds origin TTL?
The MeshPingPayload.hopCount() function returns nil when receivedTTL > originTTL, signaling an invalid or corrupted packet. Receiving nodes should discard such packets immediately, as they violate the protocol's ordering invariants.
Can relays modify TTL without breaking packet signatures?
Yes. According to comments in BitchatPacket.swift, signatures are generated with a fixed TTL of 0. This design choice explicitly allows relays to decrement TTL fields without invalidating cryptographic proofs, enabling secure hop-by-hop forwarding.
How does network density affect relay decisions?
Dense networks trigger TTL clamping via TransportConfig.bleFragmentRelayTtlCapDense. The RelayController compares current node degree against highDegreeThreshold and applies lower TTL caps when density exceeds configured limits, reducing redundant transmissions without requiring global topology knowledge.
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 →