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 headerDescription section of BinaryProtocol.swift at 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 a BitchatPacket and 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 a BitchatPacket, 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 BitchatPacket Swift struct in BitchatPacket.swift models the logical fields, while BinaryProtocol handles the binary encoding and decoding.
  • Version 2 adds a bigger 4-byte payload length and an optional route hop list, and the isRSR flag 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:

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 →