From fd22df6639d751880dad07eff940b1860c2ee1e7 Mon Sep 17 00:00:00 2001 From: Yashodhan Singh Date: Sat, 1 Aug 2026 21:38:31 +0530 Subject: [PATCH] spec: add wire-format chapter (v0.1.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drafts spec/01-wire-format.md: packet header (v1/v2), flags, variable sections, signing, padding, message types, and the two TLV framings. Trims WHITEPAPER.md §4.1 to a cross-link now that the byte-exact detail lives in the spec chapter. --- WHITEPAPER.md | 4 +- spec/01-wire-format.md | 141 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 spec/01-wire-format.md diff --git a/WHITEPAPER.md b/WHITEPAPER.md index 13fefede..550d5777 100644 --- a/WHITEPAPER.md +++ b/WHITEPAPER.md @@ -46,9 +46,7 @@ Signed announcements additionally carry the nickname, the Noise static public ke ### 4.1 Packet Format -A compact binary header (version, type, TTL, timestamp, flags) is followed by an 8-byte sender ID, an optional 8-byte recipient ID, the payload, and an optional Ed25519 signature. Version 2 packets may carry an explicit source route. Signatures exclude the TTL byte so relays can decrement it without invalidating them. - -Only `noiseEncrypted` and `noiseHandshake` packets are padded, toward 256/512/1024/2048-byte buckets; every other type — public messages, announcements, board posts, group messages, fragments, files, and voice frames — goes out at its natural length. Padding is PKCS#7-style with pad bytes equal to the pad length, and because that length must fit one byte, a frame needing more than 255 bytes to reach its bucket is emitted unpadded. Payload length is therefore observable for most traffic. +The packet header, byte offsets, flags, TLV encodings, and padding scheme are specified byte-exactly in [Wire Format](spec/01-wire-format.md). ### 4.2 Flood Control diff --git a/spec/01-wire-format.md b/spec/01-wire-format.md new file mode 100644 index 00000000..43ce9da0 --- /dev/null +++ b/spec/01-wire-format.md @@ -0,0 +1,141 @@ +# Wire Format + +This chapter defines the byte-level encoding of a bitchat packet: the fixed header, the variable sections that follow it, padding, and the two general-purpose TLV (type-length-value) framings used elsewhere in this specification. Application-layer payload catalogs (which TLV types exist and what their values mean) are defined in the chapters that own them, not here; this chapter defines only the grammar those catalogs are written in. + +## 1. Packet Versions + +A packet carries one of two version numbers, `1` or `2`, as the first byte of its header. The two versions share the same field order and differ only in: + +- the width of the `payloadLength` field (2 bytes for v1, 4 bytes for v2), and +- the availability of the optional source-route section, which v1 packets MUST NOT carry. + +A decoder MUST reject a packet whose version byte is neither `1` nor `2`. + +## 2. Header Layout + +All multi-byte integer fields, in the header and everywhere else in this chapter, are big-endian (network byte order) unless stated otherwise. + +``` ++--------+------+-----+------------------------+-------+------------------+ +|Version | Type | TTL | Timestamp | Flags | PayloadLength | +|1 byte |1 byte|1byte| 8 bytes |1 byte | 2 or 4 bytes | ++--------+------+-----+------------------------+-------+------------------+ + offset 0 1 2 3 11 12 +``` + +| Offset | Length (bytes) | Field | Description | +|---|---|---|---| +| 0 | 1 | `version` | `1` or `2`. Selects the header size and `payloadLength` width. | +| 1 | 1 | `type` | The message type. See [Message Types](#7-message-types). | +| 2 | 1 | `ttl` | Hop-count budget. Decremented by each relay; a packet MUST NOT be relayed once its `ttl` reaches `0`. | +| 3 | 8 | `timestamp` | Milliseconds since the Unix epoch. | +| 11 | 1 | `flags` | Bitfield. See [Flags](#3-flags). | +| 12 | 2 (v1) / 4 (v2) | `payloadLength` | Length, in bytes, of the `payload` section only (see [Payload and Compression](#43-payload-and-compression)). Excludes the source-route section. | + +The header is therefore **14 bytes for v1** and **16 bytes for v2** — the only difference is the width of `payloadLength`, not an added field. A v1 packet's `payloadLength` is a 16-bit field, so its payload section is bounded to 65,535 bytes; a v2 packet's 32-bit `payloadLength` raises that ceiling, subject to whatever transport-level limits the carrying link imposes (see the BLE Transport chapter). + +## 3. Flags + +The `flags` byte is a bitfield, bit 0 the least significant: + +| Bit | Value | Name | Meaning | +|---|---|---|---| +| 0 | 0x01 | `hasRecipient` | The 8-byte `recipientID` section is present. | +| 1 | 0x02 | `hasSignature` | The 64-byte `signature` section is present. | +| 2 | 0x04 | `isCompressed` | The `payload` section is compressed; see [Payload and Compression](#43-payload-and-compression). | +| 3 | 0x08 | `hasRoute` | The source-route section is present. MUST NOT be set on a v1 packet. | +| 4 | 0x10 | `isRSR` | Marks the packet as a solicited response to a prior sync request. This bit is excluded from the signed frame (see [Signing](#5-signing)) because it is set after signing and MAY change during relay. Its consumption is defined in the Store and Forward chapter. | +| 5–7 | 0x20–0x80 | reserved | MUST be `0` on encode. A decoder MUST ignore reserved bits rather than reject the packet, to allow future extension. | + +## 4. Variable Sections + +Following the header, sections appear in this fixed order. Each is present only under the condition given; absent sections contribute no bytes. + +| Section | Size | Present when | +|---|---|---| +| `senderID` | 8 bytes, fixed | always | +| `recipientID` | 8 bytes, fixed | `hasRecipient` | +| route | 1-byte hop count + 8 bytes/hop | `hasRoute` (v2 only) | +| `payload` | `payloadLength` bytes (optionally prefixed by a 2/4-byte original-size field; see below) | always | +| `signature` | 64 bytes, fixed | `hasSignature` | + +### 4.1 Sender ID and Recipient ID + +`senderID` and `recipientID` are each 8-byte `peer ID` values (see the glossary in [`README.md`](README.md)). `recipientID` is present only when `hasRecipient` is set; its absence marks the packet as a broadcast rather than a directed send. + +### 4.2 Source Route + +A v2 packet MAY carry an explicit `source route`: a 1-byte hop count `N`, followed by `N` 8-byte peer IDs, in traversal order. This section is present only when `hasRoute` is set, and its bytes are **not** counted in `payloadLength`. `N` MUST NOT exceed 255 (it is bounded by the 1-byte count prefix). + +### 4.3 Payload and Compression + +The `payload` section is `payloadLength` bytes. When `isCompressed` is set, the first bytes of the payload section are an original-size preamble — 2 bytes for a v1 packet, 4 bytes for a v2 packet, big-endian — giving the decompressed size, followed by the compressed bytes; both the preamble and the compressed bytes are counted in `payloadLength`. When `isCompressed` is not set, the payload section is the payload bytes verbatim. + +The interpretation of the (decompressed) payload bytes depends on `type`; see the Payloads, Noise, and Store and Forward chapters for the payload encodings each message type carries. + +### 4.4 Signature + +When `hasSignature` is set, a 64-byte Ed25519 signature follows the payload section. See [Signing](#5-signing) for what is signed. + +## 5. Signing + +The signature, when present, is computed over the packet's encoded bytes with two substitutions: the `signature` section itself is omitted, and `ttl` is fixed to `0` regardless of the packet's actual TTL. `ttl` is excluded because a relay decrementing it in place would otherwise invalidate every signed packet it forwards. `isRSR` is likewise excluded, being set after the packet is signed. + +A verifier MUST reconstruct the same fixed-TTL, signature-omitted, RSR-omitted frame before checking a signature against it. + +## 6. Padding + +Only `noiseHandshake` and `noiseEncrypted` packets are padded; every other message type is encoded at its natural length. Padding is applied to the full encoded frame (header through payload, before the signature section) and is PKCS#7-style: the pad length is appended as that many bytes, each byte equal to the pad length itself. + +Padding targets the smallest of the block sizes `256`, `512`, `1024`, `2048` bytes that the frame (plus a 16-byte allowance for encryption overhead) fits into. Because the pad length must fit in a single byte, a frame that would need more than 255 bytes of padding to reach its target block is emitted **unpadded** instead of padded to a smaller-than-optimal bucket. A decoder MUST attempt to decode a frame as unpadded first, and only on failure retry after stripping trailing PKCS#7 padding. + +## 7. Message Types + +The `type` byte selects both the message's purpose and, indirectly, the shape of its payload: + +| Value | Name | +|---|---| +| 0x01 | `announce` | +| 0x02 | `message` | +| 0x03 | `leave` | +| 0x04 | `courierEnvelope` | +| 0x10 | `noiseHandshake` | +| 0x11 | `noiseEncrypted` | +| 0x20 | `fragment` | +| 0x21 | `requestSync` | +| 0x22 | `fileTransfer` | +| 0x23 | `boardPost` | +| 0x24 | `prekeyBundle` | +| 0x25 | `groupMessage` | +| 0x26 | `ping` | +| 0x27 | `pong` | +| 0x28 | `nostrCarrier` | +| 0x29 | `voiceFrame` | + +Each type's payload encoding is defined in the chapter that owns it (Payloads, Noise, Store and Forward, BLE Transport, or Nostr Bridge). A decoder MUST skip — not reject the enclosing packet for — a `type` value it does not recognize, to allow forward-compatible extension. + +## 8. TLV Encodings + +Two distinct TLV (type-length-value) byte framings are used across this specification. They are structurally different — most notably in the width of the length field — so this chapter names them distinctly rather than describing one universal "TLV format." Both use unknown-type-skip decoding: a decoder MUST skip a TLV entry whose type it does not recognize (using the entry's length to find the next one) rather than rejecting the payload that contains it. + +### 8.1 TLV-8 + +``` ++------+--------+-------------------+ +| Type | Length | Value | +|1 byte|1 byte | Length bytes | ++------+--------+-------------------+ +``` + +`Length` is the number of bytes in `Value`, as an unsigned 8-bit integer — a single TLV-8 entry's value is therefore at most 255 bytes. This framing is used by the `announce` payload and by gossip neighbor lists; see the Payloads chapter for the type catalog. + +### 8.2 TLV-16 + +``` ++------+-----------------+-------------------+ +| Type | Length | Value | +|1 byte| 2 bytes (BE) | Length bytes | ++------+-----------------+-------------------+ +``` + +`Length` is the number of bytes in `Value`, as a big-endian unsigned 16-bit integer. This framing is used by `prekeyBundle` payloads (see the Noise chapter) and `courierEnvelope` payloads (see the Store and Forward chapter).