Understanding the BitChat Mesh Packet Structure: Complete Wire Format Guide
A BitChat mesh packet is a fixed-size header followed by optional variable sections controlled by flag bits, defined by the BitchatPacket Swift struct and encoded via the BinaryProtocol in the permissionlesstech/bitchat repository.
The BitChat project implements a peer-to-peer mesh networking protocol where every message traversing the network is wrapped in a BitchatPacket structure. According to the source code in localPackages/BitFoundation/Sources/BitFoundation/, packets use a compact binary format optimized for transport over BLE and other low-bandwidth mesh transports. This guide breaks down the exact structure of a BitChat mesh packet, explaining every header byte, variable section, and flag bit as implemented in the actual source.
Overview of the BitChat Mesh Packet Format
A BitChat mesh packet follows a simple, layered arrangement: a fixed-size header followed by optional variable sections whose presence is signaled by flag bits in the header. The wire format supports two protocol versions — version 1 and version 2 — with the primary difference being payload length sizing and the addition of a routing section in version 2.
The logical fields are defined in BitchatPacket.swift, while the binary serialization and deserialization logic lives in BinaryProtocol.swift. Together, these files implement the complete structure that every node in the mesh expects to parse upon delivery.
Fixed-Size Packet Header
The header is the first portion of any BitChat mesh packet and carries the metadata needed for routing, timestamping, and flag decoding. Its size depends on the protocol version: 14 bytes for version 1 and 16 bytes for version 2.
| Offset | Size | Field | Meaning |
|---|---|---|---|
| 0 | 1 B | Version | Protocol version (1 or 2) |
| 1 | 1 B | Type | application-specific message type |
| 2 | 1 B | TTL | Hop limit; decremented at each relay |
| 3–10 | 8 B | Timestamp | Unix-epoch time, big-endian |
| 11 | 1 B | Flags | Bitfield indicating optional sections |
| 12–13 | 2 B | PayloadLength (v1) | Length of payload or compressed payload |
| 12–15 | 4 B | PayloadLength (v2) | Same as above but 32-bit for larger packets |
See the exact header layout in the
headerDescriptionsection ofBinaryProtocol.swiftat line 23–30 of the source.
The Flags byte is the most important control element. It determines whether the following variable sections appear in the packet. The relevant flag bits are hasRecipient, hasSignature, isCompressed, hasRoute, and isRSR.
Optional Variable Sections Controlled by Flags
After the header, the packet contains sections whose presence depends on the flag bitfield. The sections appear in a fixed order, so the decoder can step through them deterministically once flags are parsed.
| Section | Size (bytes) | Presence Condition |
|---|---|---|
| SenderID | 8 B | Always present (origin node identifier) |
| RecipientID | 8 B | Present if hasRecipient flag is set |
| Route | 1 B + N × 8 B | Present if hasRoute flag is set (v2+ only); first byte is hop count, each hop is an 8-byte node ID |
| Payload | variable | Always present; may be compressed if isCompressed is set |
| Original Payload Size | 2 B (v1) or 4 B (v2) | Included only when the payload is compressed |
| Signature | 64 B | Present if hasSignature flag is set |
Notably, SenderID and the Payload are never optional in practice — every packet must carry a source identifier and a message body. The other sections are only added when needed for directed routing, transport compression, or cryptographic authenticity. The flags are recalculated during encoding based on which optional properties are actually populated on the Swift struct.
Overall Packet Layout
Putting the header and the optional sections together produces this simplified wire layout:
[Version][Type][TTL][Timestamp][Flags][PayloadLength]
[SenderID] [RecipientID?] [Route?] [Payload] [OrigSize?] [Signature?]
All multi-byte integer fields are encoded using big-endian network byte order. This ensures cross-platform decode regardless of CPU architecture. The Timestamp is the Unix epoch time represented as a UInt64; the route hop list is read as an array of 8-byte node ID values.
Swift Representation: The BitchatPacket Struct
The logical model backing this structure is the BitchatPacket Swift struct, defined in localPackages/BitFoundation/Sources/BitFoundation/BitchatPacket.swift at lines 11–27.
public struct BitchatPacket: Codable {
public let version: UInt8 // 1 or 2
public let type: UInt8
public let senderID: Data // 8 bytes
public let recipientID: Data? // 8 bytes if present
public let timestamp: UInt64
public let payload: Data
public var signature: Data? // 64 bytes if signed
public var ttl: UInt8
public var route: [Data]? // array of 8-byte hops (v2+ only)
public var isRSR: Bool // Reliable-store-and-forward flag
}
The optional properties map directly to the optional wire sections: if a given optional property is set on the struct, the corresponding flag bit is flipped and the data is written to the wire. Well, the route is only written in version 2; in version 1 the `hasRoute`` flag is ignored even if the property is populated.
Note the isRSR flag, which stands for Reliable Store and Forward — this is a distinctive BitChat feature that indicates the packet deserves special delayed delivery handling in the mesh, distinct from other flags.
Encoding and Decoding with BinaryProtocol
The actual binary translation is handled by the BinaryProtocol class in localPackages/BitFoundation/Sources/BitFoundation/BinaryProtocol.swift:
BinaryProtocol.encode(_:)takes aBitchatPacketand produces the binary wire format complete with compression, route sanitization, and flag calculation from the optional fields present.BinaryProtocol.decode(_:)reads the same buffer back into aBitchatPacket, performing length validation, safety checks, and optional decompression.
During encoding, the routine also enforces protocol rules: for version 1 packets, the route array is discarded even if present; for version 2, route section is emitted only when hasRoute is set. Compression is applied by the payload if flagged.
Practical Code Examples
The following examples demonstrate how to create, serialize, and decode a BitChat mesh packet, using convenience methods defined in BitchatPacket.swift.
Creating a packet and serializing it:
import Foundation
import BitFoundation // contains BitchatPacket & BinaryProtocol
let sender = Data([0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08])
let payload = "Hello, mesh!".data(using: .utf8)!
let packet = BitchatPacket(
type: 0x10, // custom message type
senderID: sender,
recipientID: nil, // broadcast
timestamp: UInt64(Date().timeIntervalSince1970),
payload: payload,
signature: nil,
ttl: 5,
version: 2, // allow route field
route: [Data([0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA])],
isRSR: false
)
if let wireData = packet.toBinaryData() {
// `wireData` is ready to be transmitted over BLE or other mesh transports
print("Encoded packet, \(wireData.count) bytes")
}
Decoding a received packet:
if let receivedPacket = BitchatPacket.from(wireData) {
print("Version:", receivedPacket.version)
print("Sender:", receivedPacket.senderID.hexString)
if let recipient = receivedPacket.recipientID {
print("Recipient:", recipient.hexString)
}
print("Payload:", String(data: receivedPacket.payload, encoding: .utf8) ?? "<binary>")
}
Both code snippets rely on the convenience methods packet.toBinaryData() and BitchatPacket.from(_:) which wrap the BinaryProtocol encoder and decoder internally.
Key Source Files
| File | Purpose |
|---|---|
BitchatPacket.swift |
Swift model describing the logical fields of a mesh packet |
BigProtocol.swift |
Implements the binary wire format (header layout, flag handling, compression, route encoding) |
BinaryProtocolTests.swift |
Unit tests exercising encoding/decoding, routing, compression, and version differences |
BitchatPacketTests.swift |
Tests constructing packets with optional fields and verifying round-trip integrity |
These source files together define, implement, and validate the complete structure of a BitChat mesh packet. Referencing them gives you the authoritative details beyond this guide.
Summary
- A BitChat mesh packet uses a fixed-size header of 14 bytes (v1) or 16 bytes (v2) followed by optional variable sections gated by flag bits.
- The
BitchatPacketSwift struct inBitchatPacket.swiftmodels the logical fields, whileBinaryProtocolhandles the binary encoding and decoding. - Version 2 adds a bigger 4-byte payload length and an optional route hop list, and the
isRSRflag indicates reliable store-and-forward semantics. - All multi-byte fields use big-endian (network) byte order, making the format portable across architectures.
Frequently Asked Questions
How large is the BitChat mesh packet header?
The header is 14 bytes for version 1 and 16 bytes for version 2, accounting for the widened payload-length field. It contains version, type, TTL, timestamp, flags, and the payload length value.
What is the isRSR flag used for?
The isRSR flag marks a packet as requiring the mesh to perform reliable store-and-forward handling, which means that the intermediate nodes may store the packet and deliver it later if the recipient is offline at the moment of arrival. This is a designed for delayed, guaranteed-style message delivery.
When is a compression field present in the wire format?
The Original Payload Size field is present only when the isCompressed flag is set. When compression is used, the payload is the compressed data, and the original-size field tells the decoder the expected uncompressed length. The size itself is 2 bytes in v1 and 4 bytes in v2.
Does a BitChat packet always include a signature?
No, a signature is optional. The 64-byte signature section appears only when the packet has the hasSignature flag set, which is the case for authenticated messages. If no cryptographic proof is provided, the signature bytes are omitted to keep short message formats compact.
Where is the routing hop list stored in the packet?
The route list is stored as a variable-length section that comes after the recipient field. It starts with a 1-byte hop count, followed by the hop node IDs (each 8 bytes). Only version 2 packs the route section at all, and only when the hasRoute flag is set by the sender.
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 →