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:
noiseEncryptedandnoiseHandshakeonly - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →