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.
- SenderID – 8 bytes (always present).
- RecipientID – 8 bytes if the
hasRecipientflag is set. - Route (v2 only) – If
hasRouteis set:- 1 byte hop count (
N) N × 8bytes of hop IDs (each truncated or padded to 8 bytes)
- 1 byte hop count (
- Compression Pre‑amble – If
isCompressedis set:- 2 bytes (v1) or 4 bytes (v2) storing the original uncompressed size
- Payload – Raw or compressed data (
payloadLengthbytes, excluding pre‑amble). - Signature – 64 bytes if
hasSignatureis 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →