spec: scope the capabilities decode rule and state the unknown-TLV rule

§5.1 claimed one permissive decode rule ("accepts any length, keeps the
low 64 bits") covered both the standalone announce TLV and the copy
inside authenticatedPeerState. Only the announce form works that way;
the session-bound form is bounded at 8 bytes by §5's own table, and a
wider field fails the whole payload rather than being truncated. Split
the two, and mark §2's length bound as the encoder constraint it is.

§5 also said nothing about a TLV type a decoder does not recognize,
while listing three explicit MUST-rejects beside it — read alone, that
invites a decoder that rejects the first field any later revision adds.
State the rule: skip an unrecognized type that is well-framed, reject a
malformed type that is recognized, reject any entry whose length
overruns the payload.

Record the limit this exposes as a known gap: the payload can gain a
field but cannot widen one, and rejection discards every TLV rather
than the offending field.

Reported by Chessing234 in review of #1639.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yashodhan Singh 2026-08-20 22:45:24 +05:30
parent 286efb0f38
commit acfba30466
2 changed files with 15 additions and 4 deletions

View File

@ -18,7 +18,7 @@ An outer payload is the `payload` section of a plaintext `BitchatPacket` — its
| 0x02 | `noisePublicKey` | 32 | The device's Noise `static key`. |
| 0x03 | `signingPublicKey` | 32 | The device's Ed25519 `signing key`. |
| 0x04 | `directNeighbors` | multiple of 8, optional | Up to 10 direct-neighbor `peer ID`s, concatenated. |
| 0x05 | `capabilities` | 1–8, optional | `PeerCapabilities` bitfield ([§5.1](#51-peercapabilities-bitfield)). |
| 0x05 | `capabilities` | 1–8, optional | `PeerCapabilities` bitfield ([§5.1](#51-peercapabilities-bitfield)). The length bound constrains encoders; a decoder MUST accept any length here, per [§5.1](#51-peercapabilities-bitfield). |
| 0x06 | `bridgeGeohash` | ≤12, optional, UTF-8 | Coarse geohash cell this peer bridges to Nostr, present only when advertising the `bridge` capability. Full encoding is defined in the Nostr Bridge chapter. |
`nickname`, `noisePublicKey`, and `signingPublicKey` are REQUIRED; a decoder MUST reject an `announce` payload missing any of them. `directNeighbors`, `capabilities`, and `bridgeGeohash` are each OPTIONAL and MAY be absent, including from clients that predate them.
@ -64,9 +64,18 @@ An outer payload is the `payload` section of a plaintext `BitchatPacket` — its
`version` MUST be `0x01`; a decoder MUST reject any other value. A decoder MUST reject a payload with a duplicate TLV entry, and MUST reject a `capabilities` TLV that is not the minimal (trailing-zero-byte-stripped) encoding described in [§5.1](#51-peercapabilities-bitfield).
A decoder MUST skip a TLV whose type it does not recognize, provided that entry's `length` lies wholly within the payload; it MUST reject the payload on any TLV whose `length` overruns the payload, recognized or not. A decoder MUST reject the whole payload — not skip the entry — on a malformed TLV of a type it does recognize. Skipping unrecognized types is what lets a later revision add a field to this payload without breaking peers that predate it; rejecting a malformed known type keeps a field a peer did send from being read as a field it did not.
### 5.1 `PeerCapabilities` Bitfield
`PeerCapabilities` is a little-endian bitfield, encoded as the fewest whole bytes needed to represent its highest set bit (at least 1 byte when non-empty; an encoder omits trailing all-zero bytes). A decoder accepts any length and keeps only the low 64 bits, so an unrecognized high bit set by a newer client round-trips without corrupting bits the decoder does understand. This encoding is used both standalone (`announce`'s `capabilities` TLV, [§2](#2-presence-announce)) and inside `authenticatedPeerState` ([§5](#5-peer-state-and-capabilities)).
`PeerCapabilities` is a little-endian bitfield, encoded as the fewest whole bytes needed to represent its highest set bit (at least 1 byte when non-empty; an encoder omits trailing all-zero bytes). An encoder MUST NOT emit more than 8 bytes.
This encoding is used both standalone (`announce`'s `capabilities` TLV, [§2](#2-presence-announce)) and inside `authenticatedPeerState` ([§5](#5-peer-state-and-capabilities)). Decoding differs between the two, and the rules are not interchangeable:
- **Standalone**, in `announce`: a decoder MUST accept any length, keeping the low 64 bits and ignoring any bytes beyond them. Neither a field wider than 8 bytes nor a non-minimal encoding invalidates the `announce`.
- **Inside `authenticatedPeerState`**: a decoder MUST reject the whole payload on a `capabilities` TLV that is empty, longer than 8 bytes, or not minimally encoded, per that section's table and rules.
Within the 8-byte field the two forms behave identically: an unrecognized set bit in 11–63 decodes, is preserved, and re-encodes minimally, so a bit assigned by a later revision round-trips without corrupting bits the decoder does understand. The strict form's rejection reaches only a field wider than the 64-bit bitfield.
| Bit | Name | Meaning |
|---|---|---|

View File

@ -60,7 +60,8 @@ Chapter 6's items are split by the REQUIRED/capability-gated boundary that chapt
- [ ] `message (0x02)` (public) and the `leave (0x03)` and `delivered`/`readReceipt` inner payloads carry raw content directly, with no TLV framing ([§3.1](04-payloads.md#31-public-message), [§3.3](04-payloads.md#33-leave), [§4](04-payloads.md#4-delivery-and-read-acknowledgement)).
- [ ] `privateMessage (0x01, inner)` is TLV-8 with `messageID` and `content` ([§3.2](04-payloads.md#32-private-message)).
- [ ] `authenticatedPeerState (0x21)` is a 1-byte version + TLV-8; a decoder MUST reject any `version` other than `0x01`, MUST reject duplicate TLV entries, and MUST reject a non-minimal `capabilities` encoding ([§5](04-payloads.md#5-peer-state-and-capabilities)).
- [ ] `PeerCapabilities` is little-endian and minimal-byte-length (trailing zero bytes stripped); a decoder keeps only the low 64 bits so unknown high bits round-trip ([§5.1](04-payloads.md#51-peercapabilities-bitfield)).
- [ ] An `authenticatedPeerState` decoder MUST skip an unrecognized TLV type whose `length` lies within the payload, MUST reject the payload on any TLV whose `length` overruns it, and MUST reject the whole payload — not skip the entry — on a malformed TLV of a recognized type ([§5](04-payloads.md#5-peer-state-and-capabilities)).
- [ ] `PeerCapabilities` is little-endian and minimal-byte-length (trailing zero bytes stripped) and an encoder MUST NOT emit more than 8 bytes; a standalone `announce` decoder MUST accept any length, keeping the low 64 bits, while an `authenticatedPeerState` decoder MUST reject an empty, over-8-byte, or non-minimal field ([§5.1](04-payloads.md#51-peercapabilities-bitfield)).
- [ ] The full `PeerCapabilities` bit table MUST be supported for dispatch, bit 10 (`nonDestructiveNoiseReplacement`) MUST never be advertised by a conforming encoder, and bits 11-63 MUST be `0` on encode while a decoder MUST preserve (not reject on) an unrecognized set bit ([§5.1](04-payloads.md#51-peercapabilities-bitfield)).
- [ ] `boardPost (0x23)` is TLV-16; a decoder MUST reject an absent/unrecognized `kind` or a payload missing a field its `kind` requires, and `expiresAt` MUST NOT exceed `createdAt` + 7 days ([§6](04-payloads.md#6-board-posts)).
- [ ] Board post signatures open with a 1-byte length + context string (`bitchat-board-v1` post / `bitchat-board-del-v1` tombstone), not the bare context string, followed by the defined field concatenation ([§6.1](04-payloads.md#61-signing)).
@ -124,8 +125,9 @@ No equivalent vector file exists yet for the `X` pattern, courier envelopes, or
## 8. Known Gaps (Non-Normative)
The following capability bits and concepts are used normatively elsewhere in this specification but have no defined wire mechanism of their own. They are not checklist items — there is nothing to check yet — and are noted here so an implementer does not mistake the absence of a checklist entry for the absence of the concept.
The following capability bits, concepts, and structural limits are used or implied normatively elsewhere in this specification but have no defined wire mechanism of their own, or no defined remedy. They are not checklist items — there is nothing to check yet — and are noted here so an implementer does not mistake the absence of a checklist entry for the absence of the concept.
- **`wifiBulk`** (`PeerCapabilities` bit 1, [§5.1](04-payloads.md#51-peercapabilities-bitfield) of Payloads) is defined only as "peer supports bulk transfer over a local Wi-Fi side channel." No chapter specifies a wire format, discovery mechanism, or session-establishment procedure for this side channel.
- **`favorite` / `mutual favorite`** gates courier deposit trust tiers ([§3.3](05-store-and-forward.md#33-deposit-policy-and-trust-tiers) of Store and Forward), Nostr private-message reachability, and relay-identity separation ([§2.4](06-nostr-bridge.md#24-sending-and-reachability), [§7.1](06-nostr-bridge.md#71-rendezvous-cell-and-events) of Nostr Bridge), but no chapter specifies how a peer proposes, signals, exchanges, or verifies favorite status on the wire. Only the [glossary](README.md#glossary) defines the term.
- **`privateMediaReceipts`** (`PeerCapabilities` bit 9, [§5.1](04-payloads.md#51-peercapabilities-bitfield) of Payloads) implies delivery/read acknowledgement for private media, but neither `privateFile`/`fileTransfer` ([§8](04-payloads.md#8-files) of Payloads) nor `voiceFrame` ([§9](04-payloads.md#9-voice) of Payloads) defines a `messageID`-equivalent field for a `delivered`/`readReceipt` payload to reference.
- **`authenticatedPeerState` can gain a field but cannot widen one.** [§5](04-payloads.md#5-peer-state-and-capabilities) of Payloads makes adding a TLV type forward-compatible — a decoder skips what it does not recognize — but offers no path for growing a field it already defines. A revision assigning `PeerCapabilities` bit 64 would need a ninth byte, which the same section requires a decoder to reject; and because rejection discards the payload whole rather than the single field, the peer's `signingPublicKey` and every other TLV it authenticated inside the session go with it. Nothing in this version can trigger it — [§5.1](04-payloads.md#51-peercapabilities-bitfield) reserves bits only through 63 — so this is a limit on how this payload may be revised, not a defect in it. Whether a future revision widens the field by degrading per-TLV instead of per-payload, or by assigning a second capabilities type, is left open.