Skip to content

Documentation gaps and ambiguities found while building a JS interop client #4

Closed

Description

@thatSFguy

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=2 or =3 ever be emitted?
  • How does this relate to the IFAC_* machinery in RNS/Reticulum.py / RNS/Packet.py upstream?

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=0 always 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

  1. added a commit that references this issue on May 7, 2026
  2. thatSFguy commented on May 7, 2026

    @thatSFguy
    OwnerAuthor

    Thanks — useful feedback, all seven items got triaged. Spec edits in 8c4d550.

    # Item Disposition
    1 §11 flag byte bit 7 Addressed. Note: the flag byte is in §2.1, not §11 (§11 is REQUEST/RESPONSE / NomadNet). §2.1 now states header_type is 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.
    2 §9.7 re-announce interval Addressed. §9.7 already stated 5–15 min; expanded with a deployment-tiered RECOMMENDED table (LoRa 5–10 min / always-on relay 15–30 min / mobile 5–10 min while active) plus AVOID <60 s and AVOID >30 min thresholds with rationale. There's no upstream-mandated default — RNS/Reticulum.py:745 is 6 h discovery, RNS/Transport.py:162 is 2 h mgmt, neither applies to end-user destinations.
    3 §8.3 RNode air-frame truncated Not reproducible. §8.3 is fully present in 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".
    4 §10.1 Resource activation truncated Not reproducible. §10.1 lists three triggers (DIRECT-with-RESOURCE auto-set, NomadNet page fulfillment, rncp file transfer) and continues into §10.2. The byte ceiling you asked about is in §10's intro paragraph (line 2013): ~360 bytes (LINK_PACKET_MAX_CONTENT) for link-direct, and §10.2 step 6 gives the per-part SDU as link.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 matches ENCRYPTED_MDU after 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.
    5 §5.6 canonical msgpack Addressed. New §5.6.1 names umsgpack (RNS/vendor/umsgpack.py) as the canonical encoder, with a per-type rules table: str/bin minimum-width family, integers as smallest signed envelope that fits (per umsgpack _pack_integer), floats always float64 (9 bytes incl. type byte), maps as insertion-order. The integer-width pitfall you flagged (uint32 vs float64 for 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.
    6 §4.5 ratchet emission Partially addressed; rest already there. §7.3 already covers cadence in detail: §7.3.1 documents 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_announce parses 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.
    7 Sender clock semantics Already there. §9.6 covers exactly this: receivers SHOULD treat any LXMF timestamp before 2020-01-01 (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 in 038e394 with deterministic announce / opportunistic-LXMF / Link-handshake JSONs and matching tools/regen_*.py scripts. 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 in test-vectors/README.md.

    Closing this issue; re-open or file a follow-up if anything in the triage misses the mark.

  3. thatSFguy commented on May 7, 2026

    @thatSFguy
    OwnerAuthor

    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:

    "some downstream implementers have understood bit 7 as reserved for an IFAC indicator and bit 6 as the 1-bit header_type."

    That understanding is correct. My commit 8c4d550 added a normative paragraph telling implementations that bit 7 is part of header_type and "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 as header_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) >> 6 parses header_type as a 1-bit field at position 6.
    • RNS/Transport.py:1003 — bytes([raw[0] | 0x80, raw[1]]) sets the IFAC flag at bit 7 in Transport.transmit when an interface has an ifac_identity attached. The IFAC field is then inserted between byte 2 (hops) and the address field.

    Fix

    Commit 0c2021e on master:

    • §2.1 flag-byte layout corrected to include ifac_flag at bit 7.
    • Upstream parse masks (Packet.py:246-250) now quoted inline so a future reader can sanity-check the spec against the source in seconds.
    • The IFAC setter from Transport.py:993-1024 is quoted to show the wire-level placement of the IFAC field.
    • A ⚠️ Spec correction callout names commit 8c4d550 explicitly so anyone who pulled between Nov 4–6 knows to re-read.
    • tools/verify_packet_header.py gains verify_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 parsed header_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 0c2021e instead.

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions