BitChat Binary Protocol: Complete Guide to Message Encoding and Fragmentation

The BitChat binary protocol uses a fixed 16-26 byte header with optional fields, supports packets up to 1MiB through fragmentation into BLE-sized chunks, and requires Version 2 for all fragments to preserve source routing.

Every message in the BitChat network travels as a compact binary packet designed for low-bandwidth BLE mesh networks. The protocol balances header efficiency with extensibility, using explicit versioning and optional fields to keep small packets small while supporting复杂 routing and encryption features.

Protocol Header Structure

The packet header occupies 16 bytes minimum and 26 bytes maximum for standard packets, with additional bytes for version-specific extensions.

Field Size Description
Version 1 byte 0x01 for v1, 0x02 for v2
Type 1 byte Packet class: announce (0x01), fragmentStart (0x2C), etc.
TTL 1 byte Starts at 7, decremented by each relay
Timestamp 4 bytes Unix-epoch seconds (big-endian)
Flags 1 byte Bitfield: encryption, signature presence, etc.
Sender ID 8 bytes First 8 bytes of SHA-256(Noise static key)
Recipient ID 8 bytes Optional; omitted for broadcast packets
Payload variable Data, encrypted, or signed content
Signature 64 bytes Optional; Ed25519 covering all bytes except TTL

The signature deliberately excludes the TTL byte so relays can decrement it without invalidating cryptographic verification. This design choice is documented in the white-paper's Packet Format section.

Version 2 packets may include an explicit source route immediately after the flags field—the only in-band routing mechanism in the protocol.

Padding Rules

Padding follows PKCS#7 with strict limits:

  • Padded types: noiseEncrypted and noiseHandshake only
  • Bucket sizes: 256, 512, 1024, or 2048 bytes
  • Single-byte length field: Maximum 255 padding bytes
  • Overflow behavior: Packets requiring >255 bytes padding transmit unpadded

All other packet types—public messages, announcements, fragments, files, voice frames—emit at natural length. This preserves bandwidth for the most common traffic types.

Packet Fragmentation for BLE MTU

When payloads exceed the 469-byte BLE link MTU, the router fragments them into independently routable packets.

Fragment Header Fields

Field Size Purpose
Fragment ID 8 bytes Random group identifier
Index / Total 2 bytes (1+1) Zero-based position and count
Inherited header variable Full parent header (version, type, TTL, timestamp, flags, IDs, route)
Payload portion variable MTU-fitted chunk

Fragmentation Requirements

All fragments must use Version 2, regardless of the original packet's version. This requirement exists because:

  • Source routes are v2-only features
  • Fragment reassembly without routing information forces fallback to flooding
  • Consistent routing preserves bandwidth across the mesh

As implemented in [docs/SOURCE_ROUTING.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/SOURCE_ROUTING.md#L109-L112), the identical Route field must appear on every fragment.

Reassembly Limits

The BLE mesh layer enforces strict resource boundaries:

  • 128 concurrent assemblies per node
  • 1 MiB total size cap per reassembly
  • 30-second timeout for fragment arrival

These limits prevent memory exhaustion and denial-of-service through partial transmissions.

Swift Implementation Examples

Building a Version 2 Header

func makeHeader(
    type: UInt8,
    ttl: UInt8,
    timestamp: UInt32,
    flags: UInt8,
    senderID: Data,
    recipientID: Data? = nil,
    route: [UInt8]? = nil
) -> Data {
    var header = Data()
    header.append(0x02)                 // version 2
    header.append(type)                 // packet type
    header.append(ttl)                  // TTL (modified by relays)
    header.append(contentsOf: timestamp.bigEndianBytes) // 4 bytes
    header.append(flags)                // flags bitfield
    header.append(senderID)             // 8-byte sender ID
    if let rID = recipientID { header.append(rID) }
    if let r = route { header.append(contentsOf: r) }
    return header
}

Fragmenting Oversized Payloads

func fragmentPayload(
    header: Data,
    payload: Data,
    mtu: Int = 469
) -> [Data] {
    let fragmentOverhead = 8 + 2        // ID + index/total
    let maxFragmentPayload = mtu - header.count - fragmentOverhead
    
    guard payload.count > maxFragmentPayload else {
        return [header + payload]       // fits without fragmentation
    }

    let fragmentID = Data(UUID().uuidString.prefix(8).utf8)
    var fragments: [Data] = []
    var offset = 0
    var index: UInt8 = 0
    let total = UInt8((payload.count + maxFragmentPayload - 1) / maxFragmentPayload)

    while offset < payload.count {
        let chunkSize = min(maxFragmentPayload, payload.count - offset)
        var fragment = Data()
        fragment.append(fragmentID)     // 8-byte group ID
        fragment.append(index)          // position
        fragment.append(total)          // total count
        fragment.append(header)         // full inherited header
        fragment.append(payload.subdata(in: offset..<offset+chunkSize))
        fragments.append(fragment)
        offset += chunkSize
        index += 1
    }
    return fragments
}

Key Source Files

File Relevance
WHITEPAPER.md Packet Format and Fragmentation sections define the binary layout
docs/SOURCE_ROUTING.md Fragment inheritance of v2 and route fields
docs/ARCHITECTURE_V2.md BLE mesh layer context and reassembly buffer
bitchatTests/ChatViewModelTests.swift Header formatting validation
bitchat/Views/Media/MediaMessageView.swift UI consumption of encoded headers

Summary

  • BitChat binary protocol uses a compact, extensible header with versioned fields and optional components
  • Signature excludes TTL to allow relay modification without cryptographic re-signing
  • Padding applies selectively to encrypted traffic only, with single-byte limits
  • Fragmentation requires Version 2 and identical source routes on all fragments
  • Reassembly limits (128 concurrent, 1MiB max, 30s timeout) protect edge devices

Frequently Asked Questions

What is the minimum and maximum BitChat packet header size?

The minimum header is 16 bytes (version, type, TTL, timestamp, flags, sender ID, no recipient). The maximum standard header reaches 26 bytes with recipient ID and source route. Signatures add 64 bytes optionally.

Why does the BitChat protocol signature exclude the TTL field?

Relays decrement TTL on every hop. Excluding this byte from the Ed25519 signature allows modification without invalidating the cryptographic proof. This trade-off accepts that TTL is unauthenticated but prevents per-hop re-signing overhead.

How does BitChat handle packets larger than the BLE MTU?

Packets exceeding 469 bytes are fragmented into independent sub-packets. Each fragment carries an 8-byte group ID, index/total counters, and a complete copy of the parent header. Receivers buffer fragments for up to 30 seconds with 1MiB total limits.

What happens if fragment reassembly exceeds the 1MiB limit?

The assembly aborts immediately. This protects nodes from memory exhaustion attacks. Senders must keep payload sizes below this threshold or implement application-level segmentation outside the BitChat protocol layer.

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 →