Repository navigation
Documentation gaps and ambiguities found while building a JS interop client #4
Description
Hi — thanks for putting this spec together. While building a browser-based Reticulum/LXMF client (https://github.com/thatSFguy/reticulum-lora-webclient) against stock Sideband and other peers, I cross-checked our implementation notes against SPEC.md and found a handful of places where the spec is silent, truncated, or potentially ambiguous. Filing as a single feedback issue rather than separate ones since they share a theme — happy to split if you'd prefer.
1. §11 — flag byte bit 7 interpretation
§11 describes the flag byte as bit 7-6 : header_type (0=HEADER_1, 1=HEADER_2). Because only values 0 and 1 are defined, bit 7 is always 0 on current wire, but some downstream implementers have understood bit 7 as reserved for an IFAC indicator and bit 6 as the 1-bit header_type.
Could the spec state explicitly which is normative? Specifically:
- Is bit 7 reserved for future use, or is it part of
header_type? - Will values
header_type=2or=3ever be emitted? - How does this relate to the
IFAC_*machinery inRNS/Reticulum.py/RNS/Packet.pyupstream?
Implementers without an authoritative answer will diverge silently the moment a non-zero high bit appears.
2. §9.7 — periodic re-announce interval
The spec correctly flags periodic re-announcement as non-optional (relays' identity caches expire and silently drop LRPROOFs for unknown senders), but gives no interval guidance. In our testing:
- A one-shot announce-on-connect leaves most relays unaware after a few minutes.
- Sideband appears to use ~30 min (visible in its UI defaults).
- Our client re-announces every 5 min while the radio is on.
A SHOULD-level recommended interval, or at least guidance on what constitutes "too aggressive" vs "too sparse," would help.
3. §8.3 — RNode air-frame header section appears truncated
The section header RNode air-frame header and split-packet protocol exists but the body wasn't visible in our fetch. For implementers writing KISS-side hosts (Web Bluetooth, Web Serial), it would be useful to state explicitly:
- The 1-byte proprietary header (upper nibble = sequence, lower nibble = flags incl.
FLAG_SPLIT=0x01) is added/stripped by RNode firmware on the KISS boundary, so KISS hosts never see it on RX or need to add it on TX. - Implementers driving a SX1262 directly (e.g. RadioLib-based repeaters) DO need to handle it.
Source: markqvist/RNode_Firmware (and our fork thatSFguy/reticulum-rnode) — Kiss.cpp::send_rx_packet, Radio.cpp::read_pending / transmit.
4. §10.1 — Resource activation threshold appears truncated
The visible text "When Resource runs" suggests a missing rule for when Resource framing replaces single-packet delivery. Concretely:
- What is the practical max plaintext size for opportunistic single-packet LXMF delivery (we observe ~250–300 bytes of
content)? - For Link-delivered LXMF, what is the threshold above which Resource framing is required (we observe failures around ~415 bytes)?
- Is the threshold MTU-derived or a fixed constant?
Without this, implementers end up reverse-engineering the cutoff from packet loss patterns.
5. §5.6 — canonical msgpack encoder
The stamp-stripping rule allows two paths (raw-as-received vs decode + re-encode first 4 elements). In practice, @msgpack/msgpack and umsgpack are byte-compatible most of the time but can disagree on numeric width (e.g. uint32 vs float64 for an exactly-integer timestamp).
Could the spec name a canonical encoder or specify the integer/float disambiguation rule the signer is expected to use? Receivers can fall back, but if signers diverge the fallback path becomes load-bearing.
6. §4.5 — ratchet emission, not just parsing
The spec covers receiving and signing-over-when-present cleanly. It would help to add a parallel emitter-side section:
- When SHOULD a sender include a ratchet pub vs. omit it?
- Rotation cadence — per-announce, per-N-announces, time-based?
- Backward-compat: are there peers that strict-reject ratchet-less announces, or is
context_flag=0always accepted?
We currently emit ratchet-less announces and have not seen a peer reject them, but this is anecdote, not a normative statement.
7. Not currently in spec — sender clock semantics
Embedded LoRa nodes without an RTC return seconds-since-boot as their LXMF timestamp. Receivers that sort by stored timestamp end up with these messages anchored at 1970-01-01 + uptime. A short note recommending "receivers SHOULD prefer insertion-order or local-receive time when sender timestamp resolves to a pre-2020 wall-clock value" would prevent confused chronology in clients across the ecosystem.
If any of these are answered elsewhere in the repo (flows/, test-vectors/, or planned per-layer split), happy to be redirected. And if a PR adding any of this content would be welcome, let me know — I have working byte-level verifiers in tools/ of the linked repo that could be repurposed as test vectors.
Activity
Address feedback issue #4 — clarify header_type/IFAC, canonical msgpa…
thatSFguy commentedon May 7, 2026
Thanks — useful feedback, all seven items got triaged. Spec edits in
8c4d550.header_typeis a 2-bit field with values 2/3 RESERVED, bit 7 always reads 0 on RNS 1.x, and bit 7 MUST NOT be repurposed as an IFAC indicator. IFAC is a separate trailing field (RNS/Reticulum.py:151-154), never sharing space with the flag byte.RNS/Reticulum.py:745is 6 h discovery,RNS/Transport.py:162is 2 h mgmt, neither applies to end-user destinations.SPEC.md(lines 1628–1717): header layout, TX C-snippet from RNode_Firmware.ino:716–742, RX state-machine table, reassembly-timeout discussion (incl. the 500 ms repeater defense), sequence-collision airtime ceiling, source map. Likely a stale browser cache / partial fetch on your end — pull master and re-check. The KISS-boundary claim you raised is already covered explicitly under "TX logic".~360 bytes (LINK_PACKET_MAX_CONTENT)for link-direct, and §10.2 step 6 gives the per-part SDU aslink.mtu - HEADER_MAXSIZE - IFAC_MIN_SIZE≈ 261 bytes at default MTU=500. For opportunistic single-packet LXMF (§5.1), your observed ~250–300 byte ceiling matchesENCRYPTED_MDUafter Token + LXMF framing — I'll consider an explicit single-packet ceiling note for §5 if more implementers ask, but the constants are documented where they're consumed.umsgpack(RNS/vendor/umsgpack.py) as the canonical encoder, with a per-type rules table:str/binminimum-width family, integers as smallest signed envelope that fits (per umsgpack_pack_integer), floats alwaysfloat64(9 bytes incl. type byte), maps as insertion-order. The integer-width pitfall you flagged (uint32vsfloat64for an integer-valued timestamp) is called out specifically. Senders that follow these rules keep the path-1 (raw) verification on the hot path and the path-2 (decode + re-encode) fallback as defense in depth.RATCHET_INTERVAL = 30*60(time-based, not per-announce), §7.3.3 confirms per-announce rotation is ALSO interop-correct, §7.4 covers the ring. New §7.3.5 closes the explicit gap you raised: ratchet-less announces (context_flag=0) are accepted by every RNS 1.x receiver —validate_announceparses both layouts unconditionally; the trade-off is forward-secrecy only. So your "currently emit ratchet-less, no peer rejected" anecdote is now a normative statement.1577836800) as "no clock" and substitute local receive time. Likely missed because the heading is "Clockless sender timestamps" rather than something with "RTC" in it.The follow-up offer to repurpose your verifiers as test vectors is appreciated —
test-vectors/was just bootstrapped in038e394with deterministic announce / opportunistic-LXMF / Link-handshake JSONs and matchingtools/regen_*.pyscripts. If your byte-level verifiers cover anything beyond what those four files already pin down (Resource segmentation? NomadNet REQUEST/RESPONSE? RNode air-frame split frames?), a PR adding a vector + regenerator under that scheme would be very welcome. The convention is documented intest-vectors/README.md.Closing this issue; re-open or file a follow-up if anything in the triage misses the mark.
Correct §2.1 flag byte: bit 7 is IFAC, not part of header_type
thatSFguy commentedon May 7, 2026
Reopening to retract part of my earlier triage. Item #1 was decided wrong — your read was correct, mine wasn't. Apologies for the confidence.
What was wrong
You wrote:
That understanding is correct. My commit
8c4d550added a normative paragraph telling implementations that bit 7 is part ofheader_typeand "MUST NOT" be used as an IFAC indicator. That paragraph contradicted the upstream reference, the official manual, and your reading. Anyone who applied it would have mis-parsed every IFAC-protected packet asheader_type ∈ {2, 3}and dropped them.How I confirmed
The official Reticulum manual §4.6.3 (Release 1.0.4, Nov 2025) documents byte 1 of the header as:
ifac_flag(7) | header_type(6) | context_flag(5) | propagation_type(4) | destination_type(3-2) | packet_type(1-0).Upstream RNS 1.2.0 confirms it on both producer and parser sides:
RNS/Packet.py:246—(self.flags & 0b01000000) >> 6parsesheader_typeas a 1-bit field at position 6.RNS/Transport.py:1003—bytes([raw[0] | 0x80, raw[1]])sets the IFAC flag at bit 7 inTransport.transmitwhen an interface has anifac_identityattached. The IFAC field is then inserted between byte 2 (hops) and the address field.Fix
Commit
0c2021eon master:ifac_flagat bit 7.Packet.py:246-250) now quoted inline so a future reader can sanity-check the spec against the source in seconds.Transport.py:993-1024is quoted to show the wire-level placement of the IFAC field.⚠️ Spec correctioncallout names commit8c4d550explicitly so anyone who pulled between Nov 4–6 knows to re-read.tools/verify_packet_header.pygainsverify_ifac_bit_position()which asserts (a) the header_type parse mask is bit 6 only, (b) the IFAC mask 0x80 is disjoint from header_type, (c) setting bit 7 doesn't leak into parsedheader_type. Run against RNS 1.2.0:ALL PASS.Items #2–#7 in your original triage stand as-replied. Item #1's previous "Addressed" line is now wrong; please rely on
0c2021einstead.Closing again — but thanks for the catch. The whole point of the spec is to be checkable against reality, and this is exactly that mechanism working.