spec: add wire-format chapter (v0.1.0)

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.
This commit is contained in:
Yashodhan Singh 2026-08-01 21:38:31 +05:30
parent c83547b0d1
commit fd22df6639
2 changed files with 142 additions and 3 deletions

View File

@ -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

141
spec/01-wire-format.md Normal file
View File

@ -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. |
| 57 | 0x200x80 | 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).