BitchatProtocol Binary Packet Format and Wire Encoding Specification

The Bitchat protocol employs a compact binary envelope defined in BinaryProtocol.swift that consists of a fixed 14‑byte (v1) or 16‑byte (v2) header followed by optional variable‑length sections, encoding all multi‑byte integers in network byte order (big‑endian).

The permissionlesstech/bitchat repository implements a lightweight mesh messaging protocol optimized for Bluetooth Low Energy (BLE) transports. At its core, the BitchatProtocol defines a binary wire format that balances compactness with extensibility, allowing devices to exchange routed, signed, and compressed messages while staying within typical BLE MTU constraints.

Fixed Header Structure

Every packet begins with a fixed-size header that determines the packet version and structure. The header is 14 bytes for protocol version 1 (v1) and 16 bytes for version 2 (v2), with the latter expanding the payload length field from 2 to 4 bytes.

Offset Size Field Description
0 1 byte Version Protocol version: 1 or 2.
1 1 byte Type Application-defined message type.
2 1 byte TTL Hop-limit for mesh routing.
3–10 8 bytes Timestamp Unix epoch in seconds as UInt64.
11 1 byte Flags Bitfield indicating optional sections.
12–13 (v1)12–15 (v2) 2 or 4 bytes PayloadLength Size of payload plus any compression pre‑amble.

All numeric values are stored in network byte order (big-endian) to ensure cross-platform compatibility. This layout is defined in localPackages/BitFoundation/Sources/BitFoundation/BinaryProtocol.swift (lines 23–30), with the encoding logic implemented in the encode method (lines 81–108).

Flag Bits and Optional Fields

Byte 11 of the header contains a bitfield that signals which optional fields follow the header. The flags are evaluated during encoding (lines 89–96) and decoding (lines 115–122) of BinaryProtocol.swift.

Bit Name Meaning
0 hasRecipient An 8‑byte recipient ID follows the sender ID.
1 hasSignature A 64‑byte Ed25519 signature follows the payload.
2 isCompressed Payload is zlib-compressed and preceded by original size.
3 hasRoute Routing hop list present (v2 only).
4 isRSR Resend‑Request flag (reserved for future use).
5–7 Reserved Currently unused.

Variable-Length Payload Sections

After the header, the packet contains ordered variable sections based on the flag bits. The decodeCore method (lines 70–141) reads these sections sequentially, validating sizes before allocation.

  1. SenderID – 8 bytes (always present).
  2. RecipientID – 8 bytes if the hasRecipient flag is set.
  3. Route (v2 only) – If hasRoute is set:
    • 1 byte hop count (N)
    • N × 8 bytes of hop IDs (each truncated or padded to 8 bytes)
  4. Compression Pre‑amble – If isCompressed is set:
    • 2 bytes (v1) or 4 bytes (v2) storing the original uncompressed size
  5. Payload – Raw or compressed data (payloadLength bytes, excluding pre‑amble).
  6. Signature – 64 bytes if hasSignature is set.

Compression and Padding

The protocol automatically compresses payloads exceeding 256 bytes using zlib when CompressionUtil.shouldCompress returns true. When compression is active, the encoder stores the original size in the pre‑amble (handled at lines 36–49) so the decoder can verify expansion ratios and allocate buffers safely (lines 60–78).

After encoding, MessagePadding.pad appends alignment bytes to optimize for BLE transmission. You can disable padding by passing padding: false to the encode method, as invoked at the end of the encode function (lines 48–52).

Encoding and Decoding in Swift

The following Swift example demonstrates creating a v2 packet with routing and signature, encoding it to binary, and decoding it on the receiving side.

import Foundation
import BitFoundation

// Build a v2 packet with recipient, route, and signature
let sender = Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08])
let recipient = Data([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11])
let payload = "Hello, Bitchat!".data(using: .utf8)!
let signature = Data(repeating: 0x9F, count: 64)
let route = [
    Data([0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17]),
    Data([0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27])
]

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

// Encode to binary (with default padding)
guard let binary = packet.toBinaryData() else {
    fatalError("Encoding failed")
}

// Decode on receiver side
guard let received = BitchatPacket.from(binary) else {
    fatalError("Decoding failed")
}

print("Type:", received.type)
print("Payload:", String(data: received.payload, encoding: .utf8)!)

The toBinaryData() method invokes BinaryProtocol.encode, which constructs the header, sets flag bits, optionally compresses the payload, serializes the route list, and applies padding. The BitchatPacket.from(_:) initializer calls BinaryProtocol.decode to reverse the process.

Protocol Limits and Constraints

The binary format enforces strict size boundaries to accommodate BLE transports, as documented in BinaryProtocol.swift (lines 58–62):

  • Maximum packet size: 65,535 bytes (limited by 16‑bit length field)
  • Minimum packet size: 21 bytes (header plus sender ID)
  • Typical BLE packet: Optimized to fit within 512 bytes

Summary

  • BitchatProtocol uses a fixed 14‑byte (v1) or 16‑byte (v2) header followed by optional sections toggled via bitflags.
  • All multi‑byte integers use network byte order (big-endian).
  • Optional fields include recipient ID (8 bytes), 64‑byte signatures, zlib-compressed payloads with size pre‑ambles, and v2 routing tables.
  • Automatic padding aligns packets for BLE transmission via MessagePadding.pad.
  • Implementation resides in localPackages/BitFoundation/Sources/BitFoundation/BinaryProtocol.swift.

Frequently Asked Questions

What byte order does BitchatProtocol use for multi‑byte fields?

All multi‑byte numbers in the Bitchat binary packet format are encoded in network byte order (big-endian). This ensures consistent interpretation across different processor architectures and is explicitly handled in the encode and decodeCore methods of BinaryProtocol.swift.

How does the protocol handle large payloads?

When a payload exceeds 256 bytes, CompressionUtil.shouldCompress evaluates whether to apply zlib compression. If compressed, the original size is stored in a 2‑byte (v1) or 4‑byte (v2) pre‑amble before the compressed data, allowing the decoder to validate the expansion ratio and allocate the correct buffer size.

What is the maximum size of a Bitchat packet?

The protocol supports packets up to 65,535 bytes due to the 16‑bit payload length field in v1. However, the implementation optimizes for BLE networks where packets typically remain under 512 bytes. The minimum valid packet size is 21 bytes, comprising the header and mandatory 8‑byte sender ID.

How is mesh routing information encoded?

In protocol version 2, routing information is appended when the hasRoute flag (bit 3) is set. The route section consists of a 1‑byte hop count followed by that many 8‑byte hop identifiers. This structure allows intermediate nodes to append their IDs as the message traverses the mesh without altering the fixed header.

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 →