BitChat Mesh Protocol Binary Packet Format: Structure and Implementation

The BitChat mesh protocol uses a compact, versioned binary wire format defined in BinaryProtocol.swift, where packets are represented by the BitchatPacket struct and serialized through static encode/decode methods, supporting optional compression, cryptographic signatures, and source routing across v1 and v2 specifications.

The BitChat mesh network, maintained in the permissionlesstech/bitchat repository, implements a lightweight communication layer optimized for Bluetooth Low Energy (BLE) constraints. Understanding the BitChat mesh protocol binary packet format is essential for developers building compatible clients or analyzing network traffic. The format prioritizes minimal overhead while remaining extensible through a flags-based variable header system.

Binary Packet Structure Overview

The binary format defines a fixed header followed by optional variable-length fields. According to the implementation in BinaryProtocol.swift, the header occupies 14 bytes for v1 packets and 16 bytes for v2 packets, with the specific layout documented in lines 23-30 of the source file.

Header Components

Every packet begins with a standardized header containing routing and metadata information:

  • Version (1 byte): Protocol version identifier, currently 1 or 2
  • Type (1 byte): Application-level message type (e.g., chat messages, file transfers)
  • TTL (1 byte): Hop-limit for mesh routing; decremented on each relay to prevent infinite loops
  • Timestamp (8 bytes): Unix epoch time in seconds, encoded as big-endian unsigned 64-bit integer
  • Flags (1 byte): Bit-field indicating optional sections:
    • 0x01 – Recipient ID present
    • 0x02 – Signature present (Ed25519)
    • 0x04 – Payload compressed
    • 0x08 – Route present (v2+ only)
    • 0x10 – RSR (Return Source Route) flag
  • Payload Length (2 bytes in v1, 4 bytes in v2): Size of the payload including optional original-size field when compressed

Variable-Length Fields

Following the header, the parser reads optional fields based on the flags byte, as implemented in lines 52-57 of BinaryProtocol.swift:

  • Sender ID (8 bytes): Fixed-size identifier for the packet origin, padded or truncated as needed
  • Recipient ID (8 bytes, optional): Target recipient identifier; omitted for broadcast messages when hasRecipient flag is unset
  • Route (variable, v2+): Source-routing information consisting of a 1-byte hop count followed by count × 8 bytes of hop identifiers
  • Original Size (2 bytes v1 / 4 bytes v2, conditional): Uncompressed payload size present when compression flag is set
  • Payload (variable): Raw or compressed message data
  • Signature (64 bytes, optional): Ed25519 cryptographic signature present when hasSignature flag is set
  • Padding (variable): Zero-padding to nearest block size for BLE-MTU alignment, handled by MessagePadding post-serialization

Protocol Versions and Feature Differences

The BitChat mesh protocol maintains backward compatibility while extending capabilities through versioned specifications.

Version 1 Specification

Version 1 provides the foundational mesh communication format with specific constraints:

  • Fixed 14-byte header with 16-bit payload length field
  • Maximum packet size of 65,535 bytes, limited by the 16-bit length encoding
  • No route support; packets rely on flood-based or table-driven routing without source-route embedding
  • Compression support with 2-byte original size fields

Version 2 Enhancements

Version 2 expands the protocol to support larger payloads and advanced routing, as detailed in lines 99-115 of BinaryProtocol.swift:

  • Expanded 16-byte header with 32-bit payload length field, enabling packets exceeding 65KB
  • Source routing capabilities via the route field (flag 0x08), allowing explicit hop-by-hop path specification
  • 32-bit original size fields for compression metadata
  • Support for up to 255 explicit routing hops in the source-route list

Encoding and Decoding Implementation

The BinaryProtocol class in BinaryProtocol.swift provides static methods for serialization and deserialization, interacting with the BitchatPacket struct defined in BitchatPacket.swift.

Serialization Flow

The encoding process follows a structured pipeline:

  1. Header Assembly: Write version, type, TTL, timestamp, and calculate initial flags
  2. Variable Field Writing: Append sender ID, conditionally write recipient ID and route based on flags
  3. Compression Evaluation: If payload exceeds 256 bytes, CompressionUtil.shouldCompress determines whether to compress; if compressed, set isCompressed flag and prepend original size
  4. Payload Appending: Write compressed or raw payload data
  5. Signature Injection: Append 64-byte Ed25519 signature if hasSignature flag is set
  6. Padding Application: Apply MessagePadding to align to BLE-MTU block sizes
import Foundation
import BitFoundation

let senderID = Data([0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08])
let recipientID = Data([0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10])
let payload = "Hello, mesh!".data(using: .utf8)!

let packet = BitchatPacket(
    type: 0x01,
    senderID: senderID,
    recipientID: recipientID,
    timestamp: UInt64(Date().timeIntervalSince1970),
    payload: payload,
    signature: nil,
    ttl: 8,
    version: 2,
    route: nil,
    isRSR: false
)

guard let binary = BinaryProtocol.encode(packet) else {
    fatalError("Encoding failed")
}
print("Encoded packet size:", binary.count)

Deserialization Flow

Decoding reverses the encoding process with validation:

  1. Header Parsing: Extract version, type, TTL, timestamp, flags, and payload length from the fixed header
  2. Flag Evaluation: Determine which optional fields exist based on the flags byte
  3. Field Extraction: Read sender ID, conditional recipient ID, and route (if v2+ with route flag)
  4. Decompression: If isCompressed flag is set, read original size and decompress payload using the inverse of CompressionUtil
  5. Signature Verification: Validate 64-byte Ed25519 signature if present
  6. Object Construction: Instantiate BitchatPacket with extracted values
if let decoded = BinaryProtocol.decode(binary) {
    print("Version:", decoded.version)
    print("Sender:", decoded.senderID.hexString)
    print("Payload:", String(data: decoded.payload, encoding: .utf8) ?? "<binary>")
}

Optional Features and Extensions

The binary format supports several optional capabilities controlled through the flags byte, enabling efficient bandwidth utilization and security.

Payload Compression

When payload data exceeds 256 bytes, the encoder evaluates compression via CompressionUtil.shouldCompress. If compression reduces size, the encoder:

  • Sets the 0x04 compression flag
  • Prepends the uncompressed original size (2 bytes for v1, 4 bytes for v2) before the compressed payload
  • Automatically decompresses during decoding when the flag is detected, as implemented in lines 36-48 and 60-71 of BinaryProtocol.swift

Message Padding for BLE MTU

To accommodate Bluetooth Low Energy maximum transmission unit constraints, the protocol supports optional padding via MessagePadding. This adds zero-padding after serialization to align packets to specific block sizes, ensuring efficient radio utilization without fragmenting frames.

Practical Implementation Examples

Adding Source Routes (Version 2)

For directed routing through specific mesh nodes, v2 packets support explicit route embedding:

let hop1 = Data([0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA])
let hop2 = Data([0xBB,0xBB,0xBB,0xBB,0xBB,0xBB,0xBB,0xBB])

var routedPacket = packet
routedPacket.route = [hop1, hop2]   // Up to 255 hops supported
routedPacket.version = 2            // Required for route support

let routedBinary = BinaryProtocol.encode(routedPacket)!

Working with Signed Packets

To create authenticated packets, populate the signature field with a 64-byte Ed25519 signature and set the hasSignature flag (0x02):

var signedPacket = packet
signedPacket.signature = ed25519SignatureData  // 64 bytes
// BinaryProtocol.encode automatically sets flag 0x02 when signature is non-nil
let signedBinary = BinaryProtocol.encode(signedPacket)

Summary

  • The BitChat mesh protocol binary packet format uses a fixed 14-byte (v1) or 16-byte (v2) header with variable-length optional fields controlled by a flags byte.
  • Version 2 introduces 32-bit payload lengths and source routing capabilities not available in Version 1.
  • Compression automatically activates for payloads exceeding 256 bytes, with original size metadata prepended to the payload.
  • Cryptographic signatures use Ed25519 (64 bytes) and are conditionally included based on the flags field.
  • All encoding/decoding logic resides in BinaryProtocol.swift, while packet structure is defined in BitchatPacket.swift within the BitFoundation local package.

Frequently Asked Questions

What is the maximum payload size supported by the BitChat binary format?

Version 1 supports payloads up to 65,535 bytes due to its 16-bit length field, while Version 2 expands this to approximately 4 GB using a 32-bit length field. However, practical limits are constrained by BLE-MTU sizes and mesh network bandwidth, with compression recommended for payloads exceeding 256 bytes.

How does the protocol handle packet routing in mesh networks?

The protocol supports two routing modes: broadcast (omitting recipient ID) and unicast (including 8-byte recipient ID). Version 2 adds explicit source routing through an optional route field containing up to 255 hop identifiers, enabling predetermined path traversal through specific mesh nodes.

Where is the binary packet format defined in the source code?

The wire format is implemented in localPackages/BitFoundation/Sources/BitFoundation/BinaryProtocol.swift, which provides static encode and decode methods. The data model is defined in BitchatPacket.swift within the same directory, while unit tests covering version handling, compression, and padding reside in BinaryProtocolTests.swift and BinaryProtocolPaddingTests.swift.

What compression algorithm does BitChat use for large payloads?

The source code references CompressionUtil.shouldCompress for determining compression eligibility, with the isCompressed flag (0x04) indicating compressed payloads. The implementation automatically handles compression for payloads exceeding 256 bytes and stores the original uncompressed size before the payload data to facilitate verification during decompression.

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 →