Upstream RNS enforces two requirements in code that SPEC.md left implicit;
both caused silent message loss in a clean-room Go LXMF service against
upstream Python rns 1.2.4 / lxmf 0.9.7.
§6.4.2 LRRTT — initiator's link-activation packet
- HEADER_1, DATA, dest_type=LINK (0x03), ctx=0xfe; body is
`umsgpack.packb(rtt_seconds)` encrypted with the link's session keys.
- The responder transitions HANDSHAKE→ACTIVE only on LRRTT receipt
(Link.py:534-553), which is also what fires the link_established
callback. LXMF's set_resource_strategy(ACCEPT_APP) is installed
from that callback; without it, every RESOURCE_ADV the initiator
sends hits the silent ACCEPT_NONE branch at Link.py:1087.
§6.4.3 Header type for post-handshake DATA and Resource
- Link-addressed packets are routed via link_table, which forwards
header bytes verbatim (Transport.py:1587-1622). HEADER_2 with a
relay's transport_id therefore arrives at the destination intact
and is dropped by packet_filter (Transport.py:1283-1285) as
"for another transport instance".
- Mandates HEADER_1 with no transport_id for all post-handshake
link DATA / Resource / control packets regardless of hop count.
- Asymmetry with LINKREQUEST (which IS path_table-routed and so
HEADER_2-eligible) is spelled out.
Companion changes:
- §6.4 renamed to "Session keys and link activation"; existing
HKDF content moved into §6.4.1.
- §2.5 LRRTT context-byte entry points at §6.4.2.
- §12.5.2 (Link DATA forwarding) cross-references §6.4.3.
- §14 failure-modes table: two new entries for the silent-drop
chains documented above.
- flows/send-link-lxmf.md step 4 strengthened (LRRTT is mandatory,
not informational); step 6 corrected (Transport.outbound does NOT
apply HEADER_1→HEADER_2 for link DATA — that conversion is
path_table-keyed, link DATA is link_table-keyed).
- test-vectors/links.json extended with an LRRTT entry: pinned
rtt_seconds=0.05 + pinned 16-byte IV produces deterministic
wire bytes for the encrypted body.
- tools/regen_links.py drives the LRRTT generation with an
os.urandom patch for the Token IV.
- tools/verify_link_lrrtt.py (new) locks the wire claims:
HEADER_1, ctx=0xfe, dest=link_id, body decrypts under
derived_key to msgpack float64 matching rtt_seconds.
Citations all verified against installed RNS 1.2.4 / LXMF 0.9.7.
All 14 verifiers PASS.
|
||
|---|---|---|
| .claude/skills/rns-update | ||
| flows | ||
| test-vectors | ||
| tools | ||
| .gitignore | ||
| agent.md | ||
| LICENSE | ||
| README.md | ||
| SPEC.md | ||
| todo.md | ||
Reticulum Specifications
Byte-level interoperability specifications for the Reticulum Network Stack and LXMF — the parts that aren't in the upstream manuals but are needed to build a working client from scratch.
Upstream Reticulum has excellent operator-facing documentation (config, deployment, design philosophy). What's missing — and what every alternative implementation has had to reverse-engineer from the Python source — is an authoritative wire-level spec: header bit layouts, msgpack field types, signature input formats, the exact behavior of Transport.outbound, and the long list of "would never guess from reading the manual" gotchas that cost hours of debugging each.
This repo collects those findings in one place. The hope is that future client authors (Kotlin, Swift, Rust, Go, embedded C — pick your stack) can read this instead of re-deriving everything from RNS/Transport.py.
Status
Early days, contributions welcome. Current content was bootstrapped from the working notes of two reverse-engineering efforts:
- The web-based Reticulum client at
reticulum-lora-webclient - The native Android client at
reticulum-mobile-app
Each finding is grounded in upstream source citations (file + line) so it can be re-verified as RNS evolves.
What's here
SPEC.md— the single combined spec document, organized by protocol layerflows/— chronological end-to-end narratives (e.g. "send a message"), cross-referencing SPEC.md sectionstools/— self-contained Python verifier scripts that test SPEC.md claims against upstream RNS / LXMF. Pinned viatools/requirements.txtto the upstream versions the scripts were last re-verified againsttest-vectors/— known-good byte sequences each implementation should be able to round-trip (intent: grow into a compliance suite)
As content grows, SPEC.md will be split into per-layer files (packet header, identity, announce, token-crypto, LXMF, link, resource, transport).
Spec corrections
Errata that may invalidate code built against an earlier revision of SPEC.md. Newest first. Feature additions and ordinary edits live in git log — this section is reserved for cases where the spec said one thing, that turned out to be wrong, and an implementer who pulled the bad version needs to fix their code.
- 2026-05-06 — §2.1 flag byte: bit 7 is the IFAC flag, not part of
header_type. Bad text introduced in8c4d550, corrected in0c2021e; on master from 2026-05-04 to 2026-05-06. The corrected layout isifac_flag(bit 7) | header_type(bit 6) | context_flag(5) | transport_type(4) | destination_type(3-2) | packet_type(1-0), matching the official manual §4.6.3 and upstreamRNS/Packet.py:246(parse mask0b01000000 >> 6) /RNS/Transport.py:1003(IFAC setterraw[0] | 0x80). Implementers who consumed the bad version will mis-parse every IFAC-protected packet asheader_type ∈ {2, 3}and drop it. Surfaced by issue #4 item #1.
Scope
In scope:
- Wire formats: byte layouts, field encodings, framing
- Signing inputs and what's hashed where
- Cross-cutting behaviors required for interop (path requests, ratchet rotation, retransmit semantics)
- "Gotchas" — things upstream code does that aren't obvious from the manual or RFC-style sketches
- Test vectors that any implementation must be able to round-trip
Out of scope:
- Operator/user documentation — see the official manual
- API design choices for any specific implementation
- Networking layer config (interfaces, transport modes) — already well documented
Source citations
Where a finding cites upstream Python code, the path is relative to a standard pip install rns lxmf installation, e.g. RNS/Transport.py, LXMF/LXMF.py. Where the bundled umsgpack is referenced, the path is RNS/vendor/umsgpack.py.
When upstream code changes such that a citation no longer matches, file an issue or PR — the goal is to track the de-facto wire spec as it actually behaves, not as it was at any single snapshot.
Contributing
If you've debugged a Reticulum interop problem and the answer wasn't in the upstream docs, please add it. Format:
### N.M Short description of the finding
**Symptom:** what you observed that prompted the investigation.
**What's happening:** the actual mechanism, ideally with upstream source citation (file + line).
**Implication / fix:** what an implementation must do to interop.
**Source:** upstream file paths and approximate line numbers.
Add a worked test vector to test-vectors/ if the finding is byte-level.
License
CC BY 4.0 — use freely, attribution appreciated.