How BitChat Handles Multi-Hop Relay: TTL and Packet Forwarding Explained

BitChat uses a deterministic relay policy in RelayController.decide that combines per-hop TTL decrement with mesh topology awareness to control flooding and ensure reliable delivery across sparse and dense networks.

The BitChat protocol implements a sophisticated multi-hop relay system that balances message reachability with network congestion control. This article examines how the protocol initializes, caps, and decrements TTL values, and how it applies different forwarding rules based on packet type and local node degree.

TTL Initialization and Packet Header Storage

Every outgoing message in BitChat receives a default TTL of 7 unless the sender explicitly overrides it. This value is defined in TransportConfig.swift:

// TransportConfig.swift
static let messageTTLDefault = 7

The TTL is embedded in the packet header and travels with the message. Each relaying node decrements this counter by exactly one before forwarding. This simple mechanism provides an upper bound on how far any packet can propagate through the mesh.

The Core Decision Engine: RelayController.decide

All forwarding logic converges in RelayController.decide (bitchat/Services/RelayController.swift). This function evaluates twelve Boolean flags and the current node's degree to determine three outcomes: whether to relay, what TTL to assign, and how long to delay.

Early Termination Rules

The controller applies aggressive filtering before expensive processing:

  • Link-local packets (REQUEST_SYNC with TTL 0) are dropped immediately (lines 28-32)
  • TTL ≤ 1 after capping triggers suppression (lines 34-37)
  • Self-originated or self-addressed packets are never forwarded
// RelayController.swift - early suppression logic
if effectiveTTL <= 1 || senderIsSelf || recipientIsSelf {
    return RelayDecision(shouldRelay: false, newTTL: 0, delayMs: 0)
}

Traffic-Specific Forwarding Policies

BitChat distinguishes four traffic classes, each with distinct TTL handling.

Directed and Handshake Traffic

Session-critical packets—handshake, directedEncrypted, and directedFragment—receive unconditional forwarding with minimal latency:

  • TTL decremented by exactly 1
  • Jitter: 10-35 ms for handshakes, 20-60 ms for other directed traffic

This ensures reliable path establishment and encrypted session continuity across multiple hops.

Fragment and Live-Voice Traffic

Media streams use graph-aware TTL capping to prevent flooding in dense networks:

Mesh Density Degree Threshold TTL Cap
Sparse < bleHighDegreeThreshold (6) bleFragmentRelayTtlCap = 7
Dense ≥ bleHighDegreeThreshold (6) bleFragmentRelayTtlCapDense = 5

The controller computes:

let cap = (degree >= highDegreeThreshold) 
    ? TransportConfig.bleFragmentRelayTtlCapDense 
    : TransportConfig.bleFragmentRelayTtlCap
let ttlLimit = min(ttl, cap)
let newTTL = ttlLimit - 1

This density-aware clamping prevents live voice fragments from overwhelming well-connected nodes while preserving reach in sparse chains.

Broadcast Traffic

Public announces, board posts, and urgent messages follow a degree-dependent TTL limit:

  • High-degree nodes (dense mesh): TTL limited to min(ttlCap, 5), with floor of 2
  • Low-degree nodes (sparse chains): retain full incoming TTL
  • Urgent board posts and announces: preferred initial TTL = 7, otherwise = 6
// RelayController.swift lines 70-84
let ttlCap = isUrgentBoardPost || isAnnounce ? 7 : 6
let ttlLimit: Int
if degree >= highDegreeThreshold {
    ttlLimit = min(ttlCap, 5)
} else {
    ttlLimit = ttlCap
}
let newTTL = max(ttlLimit - 1, 0)

Jitter Scheduling: Preventing Synchronized Floods

To avoid thundering herds, BitChat adds degree-proportional random delays:

Node Degree Delay Range
0-2 10-40 ms
3-5 60-150 ms
6-9 80-180 ms
≥ 10 100-220 ms

Higher-degree nodes—those most at risk of contributing to broadcast storms—wait longer before relaying. This spreads traffic temporally and improves duplicate suppression effectiveness.

Hop-Count Calculation for Diagnostics

The Mesh Ping diagnostic tool derives actual hop count from TTL values:

// MeshPingPayload.swift lines 50-56
var hopCount: Int {
    return originTTL - receivedTTL + 1
}

By comparing the originally transmitted TTL against the received value, the protocol computes precise path length without dedicated hop-count fields.

Signature Handling and TTL Mutability

Because TTL changes during relay, it must be excluded from cryptographic signatures. In BitchatPacket.swift (lines 49-53), the signing process zeros the TTL field before hashing, then restores the actual value for transmission:

let packetForSigning = self.withTTL(0)  // TTL excluded from signature
let signature = try privateKey.sign(packetForSigning.serialized)
// Transmit with original TTL value

This allows in-flight TTL modification without invalidating authentication.

Practical Examples

Forwarding a Public Announce

let decision = RelayController.decide(
    ttl: 7,
    senderIsSelf: false,
    recipientIsSelf: false,
    isEncrypted: false,
    isDirectedEncrypted: false,
    isFragment: false,
    isDirectedFragment: false,
    isHandshake: false,
    isAnnounce: true,
    isRequestSync: false,
    isUrgentBoardPost: false,
    isVoiceFrame: false,
    degree: 4,
    highDegreeThreshold: TransportConfig.bleHighDegreeThreshold
)
// Low-degree node (4 < 6): relays with TTL 6, delay 60-150 ms

Relaying Voice in Dense Mesh

let voiceDecision = RelayController.decide(
    ttl: 7,
    // ... other flags ...
    isFragment: true,
    isVoiceFrame: true,
    degree: 8,  // exceeds threshold
    highDegreeThreshold: 6
)
// Dense network: TTL capped at 5, then decremented to 4
// Delay: 80-180 ms

Summary

  • TTL default is 7 in TransportConfig.messageTTLDefault, decremented once per hop
  • RelayController.decide implements all forwarding logic with traffic-class differentiation
  • Link-local packets (TTL 0) and expired TTL (≤ 1) are dropped immediately
  • Directed/handshake traffic receives priority forwarding with minimal jitter
  • Media fragments use density-aware TTL caps (7 sparse, 5 dense)
  • Broadcast traffic applies degree-dependent limits to control flooding
  • Jitter scales with node degree to prevent synchronized storms
  • Hop count derives from originTTL - receivedTTL + 1 in diagnostics
  • Signatures exclude TTL to permit in-flight modification

Frequently Asked Questions

What happens when a packet's TTL reaches zero in BitChat?

Packets with TTL ≤ 1 after capping are suppressed and never forwarded. This hard limit prevents infinite propagation. The RelayController returns shouldRelay: false for these cases, effectively terminating the flood.

How does BitChat prevent broadcast storms in dense networks?

Three mechanisms work together: TTL caps reduce propagation depth for high-degree nodes, degree-proportional jitter desynchronizes relays, and traffic-specific policies apply stricter limits to high-bandwidth media fragments than to small control messages.

Can senders override the default TTL in BitChat?

Yes. While TransportConfig.messageTTLDefault provides a standard value of 7, the packet construction API accepts arbitrary TTL values. However, the RelayController clamps any incoming TTL to min(providedTTL, messageTTLDefault) before processing, preventing abuse.

Why does BitChat zero the TTL field when signing packets?

Cryptographic signatures must remain valid across hops, but TTL necessarily changes during relay. By setting TTL to 0 for the signing operation and restoring the actual value afterward, the protocol achieves both authenticated origin verification and mutable hop limits.

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 →