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 0 are never forwarded
  • Network density — dense regions apply TransportConfig.bleFragmentRelayTtlCapDense to clamp TTL
  • Message type — certain packets like REQUEST_SYNC bypass 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:

  1. Origin node creates a packet with originTTL = TransportConfig.messageTTLDefault (typically 7)
  2. First-hop recipient receives TTL 7, computes hop count of 1, and queries RelayController
  3. Controller returns shouldRelay = true with newTTL = 6; packet is re-encoded and broadcast
  4. Subsequent hops repeat the process, decrementing TTL until reaching 0
  5. 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
  • RelayDecision encapsulates 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:

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 →