Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/permissionlesstech/bitchat/llms.txt

Use this file to discover all available pages before exploring further.

All BLE mesh communication in Bitchat uses a compact binary packet format optimized for Bluetooth LE’s constrained bandwidth and MTU limitations. The format is designed around network byte order (big-endian) multi-byte values, variable-length fields with explicit length prefixes, and an optional Ed25519 signature that covers the full packet except the TTL byte. Two versions of the format exist: V1 (legacy, 2-byte payload length) and V2 (current, 4-byte payload length with optional source route).

V1 Packet Structure

V1 is the legacy format. The fixed header is 14 bytes; the payload length field is 2 bytes, limiting payloads to 65 535 bytes before fragmentation.
+-------------------+---------------------------------------------------------+
| Fixed Header (14) | Variable Sections                                       |
+-------------------+----------+-------------+------------------+-------------+
| Ver: 1 (1B)       | SenderID | RecipientID | Payload          | Signature   |
| Type, TTL, etc.   | (8B)     | (8B)        | (Length in Head) | (64B)       |
| Len: 2 Bytes      |          | (Optional)  |                  | (Optional)  |
+-------------------+----------+-------------+------------------+-------------+

V2 Packet Structure

V2 extends the header to 16 bytes by widening the payload length field to 4 bytes (UInt32). It also adds an optional SOURCE ROUTE section between the recipient ID and the payload, enabled by the HAS_ROUTE (0x08) flag. The source route may only be present on packets that also carry a RecipientID (HAS_RECIPIENT flag set).
+-------------------+-----------------------------------------------------------------------------+
| Fixed Header (16) | Variable Sections                                                           |
+-------------------+----------+-------------+-----------------------+------------------+-------------+
| Ver: 2 (1B)       | SenderID | RecipientID | SOURCE ROUTE          | Payload          | Signature   |
| Type, TTL, etc.   | (8B)     | (8B)        | (Variable)            | (Length in Head) | (64B)       |
| Len: 4 Bytes      |          | (Required*) | Only if HAS_ROUTE=1   |                  | (Optional)  |
+-------------------+----------+-------------+-----------------------+------------------+-------------+
* A RecipientID is required whenever HAS_ROUTE is set.

Fixed Header Fields

FieldV1 SizeV2 SizeDescription
Version1 byte1 byte0x01 (V1) or 0x02 (V2)
Type1 byte1 bytePacket type identifier (see table below)
TTL1 byte1 byteRemaining relay hops; decremented by each relay. Excluded from signature scope so relays can decrement without invalidating it
Timestamp8 bytes8 bytesMilliseconds since Unix epoch (big-endian UInt64)
Flags1 byte1 byteBitfield: HAS_SENDER (0x01), HAS_RECIPIENT (0x02), HAS_SIGNATURE (0x04), HAS_ROUTE (0x08)
Payload Length2 bytes4 bytesLength of the payload in bytes. In V2 this is a UInt32 and excludes the route section, IDs, and signature
Total Header Size14 bytes16 bytesV2 is 2 bytes larger due to the wider length field

Packet Types

Bitchat defines the following packet types on the mesh. The first byte of the fixed header identifies the type.
TypeRaw ValueDescription
noiseEncrypted0x11A Noise-session-encrypted envelope. All private payloads — private messages, delivery acknowledgments, read receipts, voice frames, private file transfers — ride inside this type. Intermediate relays see only opaque ciphertext.
noiseHandshake0x10Noise XX handshake message. Establishes a mutually-authenticated, forward-secret session between two peers.
message0x02Signed public broadcast message (plain text, visible to everyone in the mesh).
groupMessage0x25Group-encrypted broadcast (cleartext group ID, ChaChaPoly body). Opaque ciphertext to non-members; carried and served like a public message for history sync.
TypeRaw ValueDescription
announce0x01Signed peer-presence announcement. Carries nickname, Noise static public key, Ed25519 signing key, capability flags, and up to 10 direct-neighbor IDs. Propagates multi-hop.
leave0x03Peer departure notification. Signals that a peer is intentionally disconnecting.
ping / pong0x26 / 0x27Directed echo request and reply (nonce + origin TTL). Never replayed via gossip sync.
TypeRaw ValueDescription
fragment0x20A single fragment of a larger packet that exceeded the link MTU. Contains an 8-byte fragment ID, 2-byte index, 2-byte total, 1-byte original type, and fragment data. Always uses full fanout (never subset).
fileTransfer0x22A public file or image transfer over the mesh. Encoded as a BitchatFilePacket TLV payload containing filename, file size, MIME type, and content. Private file transfers ride inside noiseEncrypted.
voiceFrame0x29Push-to-talk live voice burst. Carried as a public broadcast; stale audio frames are dropped at the receiver. Never replayed via gossip sync. Private voice bursts ride inside noiseEncrypted.
TypeRaw ValueDescription
boardPost0x23Signed geohash bulletin board post or tombstone. Long-lived (synced until the post’s own expiry) with a separate gossip sync capacity and cadence.
requestSync0x21Gossip sync initiation. Carries a compact GCS filter encoding what the sender already has, so the peer can return only missing packets. TTL is always 0 (local only).
TypeRaw ValueDescription
courierEnvelope0x04A sealed (Noise X) store-and-forward envelope addressed by a 16-byte rotating recipient tag. Directed traffic; never relayed via gossip sync. See Store & Forward.
prekeyBundle0x24A signed prekey bundle published for offline key agreement. Gossiped like board posts; one per owner, newer replaces older.
nostrCarrier0x28Gateway carrier for Nostr bridging. A complete signed Nostr event ferried between a mesh-only peer and an internet gateway peer. Never replayed via gossip sync.

Noise Payload Types

The noiseEncrypted packet type is a container. After decryption, the first byte of the plaintext indicates the inner payload type:
Inner TypeRaw ValueDescription
privateMessage0x01Private chat message between two peers
readReceipt0x02Message was read by the recipient
delivered0x03Message was delivered
groupInvite0x06Creator-signed group state (invite or initial roster)
groupKeyUpdate0x07Creator-signed group state (key rotation or roster update)
voiceFrame0x08Live voice burst frame (private push-to-talk)
privateFile0x20Private file transfer (BitchatFilePacket encoded inside Noise)
authenticatedPeerState0x21Versioned peer capability state, authenticated by the Noise session
verifyChallenge0x10QR-based out-of-band verification challenge
verifyResponse0x11QR-based out-of-band verification response
vouch0x12Batch of transitive-verification vouch attestations

Padding

Only noiseEncrypted and noiseHandshake packets are padded. Every other type — public messages, announcements, board posts, group messages, fragments, files, and voice frames — is transmitted at its natural length, making payload length observable to any passive BLE listener. Padding uses PKCS#7-style byte filling: each padding byte equals the total pad length. Frames are padded toward the next bucket:
Payload bucketTarget size
Small256 bytes
Medium512 bytes
Large1 024 bytes
Extra-large2 048 bytes
Because the pad length must fit in a single byte, the maximum padding is 255 bytes. A frame that would need more than 255 bytes of padding to reach its bucket is emitted unpadded. This is a known gap; closing it is planned future work.
Non-Noise packet types are not padded. Payload length is therefore observable for public messages, fragments, file transfers, announces, and voice frames. Traffic analysis based on payload size is not prevented for these types.

Signatures

Packets carrying an Ed25519 signature include it as the final 64 bytes of the wire frame (when HAS_SIGNATURE is set in the flags field). The signature is computed over the entire packet structure — fixed header, sender ID, recipient ID, source route (if present), and payload — with the TTL byte temporarily set to zero and the signature bytes removed before signing. Setting TTL to zero during signing is what allows intermediate relays to decrement the TTL without invalidating the signature: the signature was never over the original TTL value. Tampering with any other field (including the route list in a V2 source-routed packet) invalidates the signature and causes the receiving peer to drop the packet.

Source Routing (V2)

Source routing is a V2-only feature controlled by the HAS_ROUTE (0x08) flag in the flags byte. When set, a SOURCE ROUTE section appears immediately after the RecipientID:
[Count: 1 byte] [HopID-1: 8 bytes] [HopID-2: 8 bytes] ... [HopID-N: 8 bytes]
The Count byte gives the number of intermediate hops (not counting sender or recipient, which are already in the fixed header). A relay finds its own peer ID at index i in the hop list and forwards to the peer at index i+1; if it is the last hop, it forwards directly to the RecipientID. If the next hop is unreachable, the relay falls back to flooding.
The HAS_ROUTE flag is only valid when version >= 2. A V1 relay receiving a packet with this flag set must ignore it. iOS only originates source-routed packets when every node on the intended path has been observed speaking V2, ensuring that V1 peers never receive a frame they cannot decode.
For full details on route origination policy, failure fallback, and topology discovery, see the BLE Mesh reference and the SOURCE_ROUTING.md design document in the repository.

Build docs developers (and LLMs) love