SPEC.md: collapsible ToC + collapse §11.6 NomadNet specifics

Adds a per-section table of contents at the top of the doc, wrapped
in <details> so it's collapsed by default (the spec body is visible
as soon as the file opens; click "Contents" to navigate). Every H2
and H3 heading is linked, including the unnumbered §14 failure-mode
categories.

Also wraps §11.6 (NomadNet specifics) in <details> — it's already
flagged "informational, not normative" and is the longest H3 sub-tree
in the document. Readers implementing only the §11 wire layer can
skim past it; readers implementing a NomadNet client one click away.

tools/_gen_toc.py regenerates the ToC by re-extracting headings.
Run it after adding/removing/renaming any H2 or H3.

Picked this over the per-layer-file split previously listed in todo
because the split would have broken ~37 cross-references (flow docs,
verifier docstrings, agent.md, README) for marginal reader benefit
at the document's current size (~3300 lines).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rob 2026-05-04 22:05:17 -04:00
commit 68afe192e5
3 changed files with 236 additions and 6 deletions

155
SPEC.md
View file

@ -6,6 +6,156 @@ A byte-level reference for implementing Reticulum-compatible clients. This docum
Source citations refer to the standard `pip install rns lxmf` install layout (`RNS/`, `LXMF/`).
<details>
<summary><b>Contents</b> — click to expand the per-section table of contents (regenerate with <code>python tools/_gen_toc.py</code>)</summary>
<!-- TOC: regenerate via `python tools/_gen_toc.py` -->
- [1. Identity and destination hashes](#1-identity-and-destination-hashes)
- [1.1 Identity composition](#11-identity-composition)
- [1.2 Destination hash](#12-destination-hash)
- [1.3 Private key on-disk format](#13-private-key-on-disk-format)
- [1.4 GROUP destinations (symmetric-key alternative to SINGLE)](#14-group-destinations-symmetric-key-alternative-to-single)
- [2. Packet header](#2-packet-header)
- [2.1 Flag byte layout](#21-flag-byte-layout)
- [2.2 Two header forms](#22-two-header-forms)
- [2.3 Originator HEADER_1 → HEADER_2 conversion](#23-originator-header_1-header_2-conversion)
- [2.4 Hop count](#24-hop-count)
- [2.5 Context byte](#25-context-byte)
- [2.6 Source](#26-source)
- [3. Token cryptography (modified Fernet)](#3-token-cryptography-modified-fernet)
- [3.1 Wire format](#31-wire-format)
- [3.2 Encrypt steps (opportunistic)](#32-encrypt-steps-opportunistic)
- [3.3 Decrypt steps](#33-decrypt-steps)
- [3.4 Source](#34-source)
- [4. Announce wire format](#4-announce-wire-format)
- [4.1 Packet body](#41-packet-body)
- [4.2 Signed data](#42-signed-data)
- [4.3 `app_data` format for LXMF delivery destinations](#43-app_data-format-for-lxmf-delivery-destinations)
- [4.4 Announce filtering by `name_hash`](#44-announce-filtering-by-name_hash)
- [4.5 Announce validation rules (receive side)](#45-announce-validation-rules-receive-side)
- [5. LXMF wire format](#5-lxmf-wire-format)
- [5.1 Opportunistic delivery (single Reticulum DATA packet)](#51-opportunistic-delivery-single-reticulum-data-packet)
- [5.2 Direct delivery (over an established Reticulum Link)](#52-direct-delivery-over-an-established-reticulum-link)
- [5.3 `msgpack_payload`](#53-msgpack_payload)
- [5.4 Source/destination semantics](#54-sourcedestination-semantics)
- [5.5 Signed data](#55-signed-data)
- [5.6 Signature verification — msgpack variant tolerance](#56-signature-verification-msgpack-variant-tolerance)
- [5.7 LXMF stamps and tickets (anti-spam)](#57-lxmf-stamps-and-tickets-anti-spam)
- [5.8 Propagation node protocol (offline message store-and-forward)](#58-propagation-node-protocol-offline-message-store-and-forward)
- [5.9 Source](#59-source)
- [6. Reticulum Link protocol](#6-reticulum-link-protocol)
- [6.1 LINKREQUEST (initiator → responder)](#61-linkrequest-initiator-responder)
- [6.2 LRPROOF (responder → initiator)](#62-lrproof-responder-initiator)
- [6.3 link_id derivation](#63-link_id-derivation)
- [6.4 Session key derivation](#64-session-key-derivation)
- [6.5 Packet receipts (regular `PROOF` packets)](#65-packet-receipts-regular-proof-packets)
- [6.6 MTU and mode signalling (3-byte trailer on LINKREQUEST and LRPROOF)](#66-mtu-and-mode-signalling-3-byte-trailer-on-linkrequest-and-lrproof)
- [6.7 KEEPALIVE and link teardown](#67-keepalive-and-link-teardown)
- [6.8 Channel mode (`CHANNEL = 0x0E`)](#68-channel-mode-channel-0x0e)
- [6.9 Source](#69-source)
- [7. Transport behavior — the parts that bite](#7-transport-behavior-the-parts-that-bite)
- [7.1 Path requests: peers send `path?` before opportunistic LXMF when no path is known](#71-path-requests-peers-send-path-before-opportunistic-lxmf-when-no-path-is-known)
- [7.2 Responding to path requests](#72-responding-to-path-requests)
- [7.3 Ratchet rotation (forward-secrecy hygiene, not dedup)](#73-ratchet-rotation-forward-secrecy-hygiene-not-dedup)
- [7.4 Ratchet ring (inbound decrypt tolerance)](#74-ratchet-ring-inbound-decrypt-tolerance)
- [7.5 Periodic re-announce](#75-periodic-re-announce)
- [7.6 `TCPServerInterface.OUT` is True by default in practice](#76-tcpserverinterfaceout-is-true-by-default-in-practice)
- [7.7 Source](#77-source)
- [8. Transport framing](#8-transport-framing)
- [8.1 KISS (BLE / serial / RNode link)](#81-kiss-ble-serial-rnode-link)
- [8.2 HDLC (TCP / `rnsd TCPServerInterface`)](#82-hdlc-tcp-rnsd-tcpserverinterface)
- [8.3 RNode air-frame header and split-packet protocol](#83-rnode-air-frame-header-and-split-packet-protocol)
- [8.4 RNode KISS configuration handshake](#84-rnode-kiss-configuration-handshake)
- [8.5 RNode CSMA / airtime accounting](#85-rnode-csma-airtime-accounting)
- [8.6 AutoInterface multicast discovery (LAN auto-detect)](#86-autointerface-multicast-discovery-lan-auto-detect)
- [9. Implementation gotchas](#9-implementation-gotchas)
- [9.1 LXMF `source_hash` is the destination hash, not the identity hash](#91-lxmf-source_hash-is-the-destination-hash-not-the-identity-hash)
- [9.2 Web Crypto and JCA AES-CBC auto-pad PKCS#7 — do not pad manually](#92-web-crypto-and-jca-aes-cbc-auto-pad-pkcs7-do-not-pad-manually)
- [9.3 RNS bundles `umsgpack` — encode display names as `bytes`, not `str`](#93-rns-bundles-umsgpack-encode-display-names-as-bytes-not-str)
- [9.4 Display name preservation across re-announces](#94-display-name-preservation-across-re-announces)
- [9.5 Self-announce echo](#95-self-announce-echo)
- [9.6 Clockless sender timestamps](#96-clockless-sender-timestamps)
- [9.7 Periodic re-announce is non-optional](#97-periodic-re-announce-is-non-optional)
- [9.8 The destination hash uses the bare app-name string](#98-the-destination-hash-uses-the-bare-app-name-string)
- [9.9 Diagnostic: rx-log every inbound packet at the engine entry](#99-diagnostic-rx-log-every-inbound-packet-at-the-engine-entry)
- [9.10 microReticulum `random_hash` lacks the timestamp half](#910-microreticulum-random_hash-lacks-the-timestamp-half)
- [10. Resource fragmentation protocol](#10-resource-fragmentation-protocol)
- [10.1 When Resource runs](#101-when-resource-runs)
- [10.2 Initiator-side preparation](#102-initiator-side-preparation)
- [10.3 Wire packet contexts used during a Resource transfer](#103-wire-packet-contexts-used-during-a-resource-transfer)
- [10.4 RESOURCE_ADV — the advertisement](#104-resource_adv-the-advertisement)
- [10.5 RESOURCE_REQ — receiver requests parts](#105-resource_req-receiver-requests-parts)
- [10.6 RESOURCE part packets](#106-resource-part-packets)
- [10.7 RESOURCE_HMU — hashmap update](#107-resource_hmu-hashmap-update)
- [10.8 RESOURCE_PRF — final proof](#108-resource_prf-final-proof)
- [10.9 RESOURCE_ICL / RESOURCE_RCL — cancellation](#109-resource_icl-resource_rcl-cancellation)
- [10.10 Sliding window and rate adaptation](#1010-sliding-window-and-rate-adaptation)
- [10.11 Multi-segment resources](#1011-multi-segment-resources)
- [10.12 Compression and encryption layering](#1012-compression-and-encryption-layering)
- [10.13 Source map for §10](#1013-source-map-for-10)
- [11. REQUEST/RESPONSE protocol (NomadNet pages, propagation `/get`, custom RPC)](#11-requestresponse-protocol-nomadnet-pages-propagation-get-custom-rpc)
- [11.1 Wire form — REQUEST (initiator → server)](#111-wire-form-request-initiator-server)
- [11.2 Wire form — RESPONSE (server → initiator)](#112-wire-form-response-server-initiator)
- [11.3 Path hash collision avoidance](#113-path-hash-collision-avoidance)
- [11.4 Authorization (`allow` modes)](#114-authorization-allow-modes)
- [11.5 RequestReceipt — initiator-side state machine](#115-requestreceipt-initiator-side-state-machine)
- [11.6 NomadNet specifics (informational, not normative)](#116-nomadnet-specifics-informational-not-normative)
- [11.7 Source map](#117-source-map)
- [12. Transport-relay behaviour](#12-transport-relay-behaviour)
- [12.1 The `transport_enabled` toggle](#121-the-transport_enabled-toggle)
- [12.2 DATA forwarding rules](#122-data-forwarding-rules)
- [12.3 ANNOUNCE rebroadcasting](#123-announce-rebroadcasting)
- [12.4 Path table management](#124-path-table-management)
- [12.5 Reverse-table link transport](#125-reverse-table-link-transport)
- [12.6 Tunnels and shared-instance protocol](#126-tunnels-and-shared-instance-protocol)
- [12.7 Source map for §12](#127-source-map-for-12)
- [13. Threading and concurrency model](#13-threading-and-concurrency-model)
- [13.1 Long-running threads](#131-long-running-threads)
- [13.2 Lock inventory](#132-lock-inventory)
- [13.3 Callback-thread guarantees (and lack thereof)](#133-callback-thread-guarantees-and-lack-thereof)
- [13.4 Implementation-private constants](#134-implementation-private-constants)
- [13.5 Source map](#135-source-map)
- [14. Failure modes — symptom → root cause](#14-failure-modes-symptom-root-cause)
- [Identity / announce](#identity-announce)
- [Token crypto / opportunistic LXMF](#token-crypto-opportunistic-lxmf)
- [Link establishment / proof receipts](#link-establishment-proof-receipts)
- [Resource transfers (large bodies)](#resource-transfers-large-bodies)
- [Path discovery](#path-discovery)
- [Transport / framing](#transport-framing)
- [LXMF specifics](#lxmf-specifics)
- [Concurrency](#concurrency)
- [When all else fails](#when-all-else-fails)
- [15. Time and clock requirements](#15-time-and-clock-requirements)
- [15.1 Three clock kinds](#151-three-clock-kinds)
- [15.2 Required: monotonic seconds (every implementation)](#152-required-monotonic-seconds-every-implementation)
- [15.3 Recommended: monotonic-with-no-skew across announces (timestamp encoding)](#153-recommended-monotonic-with-no-skew-across-announces-timestamp-encoding)
- [15.4 Recommended: wall time (LXMF-level)](#154-recommended-wall-time-lxmf-level)
- [15.5 Optional: high-resolution monotonic for diagnostics](#155-optional-high-resolution-monotonic-for-diagnostics)
- [15.6 What fails on a no-RTC, no-NTP-sync device](#156-what-fails-on-a-no-rtc-no-ntp-sync-device)
- [15.7 Source map](#157-source-map)
- [16. Bounded-state inventory (memory limits at a glance)](#16-bounded-state-inventory-memory-limits-at-a-glance)
- [16.1 Per-node state caps](#161-per-node-state-caps)
- [16.2 Per-interface state caps](#162-per-interface-state-caps)
- [16.3 Per-destination state caps](#163-per-destination-state-caps)
- [16.4 Per-Link state caps](#164-per-link-state-caps)
- [16.5 Per-Resource state caps](#165-per-resource-state-caps)
- [16.6 Identity/cryptography caches](#166-identitycryptography-caches)
- [16.7 LXMF-level caps](#167-lxmf-level-caps)
- [16.8 Channel state caps](#168-channel-state-caps)
- [16.9 What this means for embedded targets](#169-what-this-means-for-embedded-targets)
- [17. Implementation taxonomy: who needs which sections](#17-implementation-taxonomy-who-needs-which-sections)
- [17.1 The three categories](#171-the-three-categories)
- [17.2 Section relevance by category](#172-section-relevance-by-category)
- [17.3 Worked example: §2.3 originator HEADER_1→HEADER_2 conversion](#173-worked-example-23-originator-header_1header_2-conversion)
- [17.4 Pragmatic implication](#174-pragmatic-implication)
- [18. Test vectors](#18-test-vectors)
- [19. Source map](#19-source-map)
<!-- /TOC -->
</details>
---
## 1. Identity and destination hashes
@ -2305,6 +2455,9 @@ Default timeout is `link.rtt × link.traffic_timeout_factor + Resource.RESPONSE_
### 11.6 NomadNet specifics (informational, not normative)
<details>
<summary>Click to expand — NomadNet-layer conventions on top of §11 (form data env vars, link target syntax, micron page headers, <code>/file/</code> downloads, ALLOW_LIST, partials). Skip if you're not implementing a NomadNet client; the §11 wire form is the protocol layer.</summary>
NomadNet pages are served over this protocol with these conventions. Source-of-truth for all of these is upstream `markqvist/NomadNet`: `nomadnet/Node.py` (server) and `nomadnet/ui/textui/Browser.py` (client).
#### 11.6.1 Paths and the `nomadnetwork.node` aspect
@ -2440,6 +2593,8 @@ Implementation reference: `Browser.py:493-606` (`__load_partial`, `start_partial
None of these are wire-spec — they're caller conventions layered on top of §11. A Reticulum client that can't render micron markup or doesn't implement the form/cache/partial conventions can still fetch pages and display the raw bytes; the protocol layer doesn't care about content.
</details>
### 11.7 Source map
| File | What |

15
todo.md
View file

@ -402,12 +402,15 @@ order: top three save the most debugging hours.
## Spec polishing (lower priority)
- [ ] **Split `SPEC.md` into per-layer files** as the document grows
past ~2300 lines. Suggested layout per `README.md`:
`00-overview.md`, `01-packet-header.md`, `02-identity.md`,
`03-announce.md`, `04-token-crypto.md`, `05-lxmf.md`,
`06-link.md`, `07-resource.md`, `08-transport.md`,
`09-paths-and-discovery.md`, `10-implementation-gotchas.md`.
- [x] **Navigation polish for `SPEC.md`** — at ~3300 lines, splitting
into per-layer files would have broken ~37 cross-references
(flow docs, verifier docstrings, agent.md, README) for
relatively little reader benefit. Picked the lighter polish
instead: a collapsible Table of Contents at the top of the
doc with anchor links to every H2 + H3, plus a `<details>`
wrap on §11.6 (NomadNet specifics — informational/non-normative,
and the longest H3 sub-tree in the document). Helper script at
`tools/_gen_toc.py` regenerates the ToC if headings change.
- [x] **Add a "last-verified-against-rns" line** to SPEC.md
frontmatter (per `agent.md` §7). Done — `RNS 1.2.0 / LXMF

72
tools/_gen_toc.py Normal file
View file

@ -0,0 +1,72 @@
"""
One-shot helper that builds a navigation Table of Contents for SPEC.md
by extracting every H2 and H3 heading and computing the GitHub anchor
for each. The output is printed to stdout. To regenerate the ToC after
adding/removing/renaming headings:
python tools/_gen_toc.py > /tmp/toc.md
# paste contents into SPEC.md between the <!-- TOC --> markers
This is a maintenance helper, not a verifier; not listed in
`tools/README.md`. Delete if it stops being useful.
"""
from __future__ import annotations
import os
import re
import sys
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SPEC_PATH = os.path.join(REPO_ROOT, "SPEC.md")
def slugify(s: str) -> str:
s = s.lower()
s = s.replace("`", "")
# Drop everything except word chars (letters/digits/_), whitespace, and hyphen
s = re.sub(r"[^\w\s-]", "", s, flags=re.UNICODE)
s = re.sub(r"\s+", "-", s)
s = re.sub(r"-+", "-", s)
return s.strip("-")
def main() -> int:
with open(SPEC_PATH, "r", encoding="utf-8") as f:
lines = f.readlines()
in_fence = False
entries = []
for line in lines:
stripped = line.rstrip("\n")
if stripped.startswith("```"):
in_fence = not in_fence
continue
if in_fence:
continue
m2 = re.match(r"^## (.+?)\s*$", stripped)
m3 = re.match(r"^### (.+?)\s*$", stripped)
if m2:
title = m2.group(1)
entries.append(("h2", title, slugify(title)))
elif m3:
title = m3.group(1)
entries.append(("h3", title, slugify(title)))
out = sys.stdout
out.reconfigure(encoding="utf-8") if hasattr(out, "reconfigure") else None
out.write("<!-- TOC: regenerate via `python tools/_gen_toc.py` -->\n")
out.write("## Contents\n\n")
for kind, title, slug in entries:
if kind == "h2":
out.write(f"- [{title}](#{slug})\n")
else:
out.write(f" - [{title}](#{slug})\n")
out.write("<!-- /TOC -->\n")
return 0
if __name__ == "__main__":
sys.exit(main())