Commit graph

15 commits

Author SHA1 Message Date
Rob
d27f01946e Add §1.4 GROUP destinations (Tier 2 #4)
GROUP destinations use a pre-shared 64-byte symmetric key (32B
signing + 32B encryption split) with the same Token wire format as
Link-derived encryption — iv(16) || aes_ciphertext || hmac(32), no
eph_pub prefix. dest_hash recipe matches SINGLE with identity
optional. Spec covers key gen via Token.generate_key, wire form,
dest_hash variants, on-disk format (raw bytes, no header), and a
why-rarely-used note (no forward secrecy, key distribution
unsolved at the protocol layer).

Most LXMF interop clients can ignore GROUPs entirely but should
still recognize the 0x01 type byte to gracefully reject inbound
packets they can't decrypt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:03:31 -04:00
Rob
7931eb1d8c Add §8.4 RNode KISS handshake + §8.5 airtime caps (Tier 2 #3+#5)
§8.4: full bring-up sequence for a host driving an RNode over KISS.
Five sub-sections — command-byte inventory (28 commands cited from
RNode_Firmware/Framing.h:24-95), 12-step bring-up recipe, the
CMD_DETECT/DETECT_REQ(0x73)/DETECT_RESP(0x46) handshake with
asynchronous CMD_FW_VERSION/CMD_PLATFORM/CMD_MCU replies,
4-byte big-endian numerics for FREQUENCY/BANDWIDTH (subject to KISS
escape), and the RX sidecar format (RSSI = byte − 157,
SNR = signed Q6.2 / 4 dB).

§8.5: airtime caps via CMD_ST_ALOCK (0x0B) and CMD_LT_ALOCK (0x0C),
encoded as 2-byte big-endian uint16 of (limit_percent × 100).
Reticulum.ANNOUNCE_CAP = 2.0 is the upstream default. Pre-TX
carrier-sense is firmware-private — host-side clients driving an
RNode don't implement their own LBT, but native LoRa clients need
the airtime_bins algorithm from RNode_Firmware.ino:683-712.

Closes Tier 2 #3 and #5 (CSMA/airtime). Tier 2: 4 of 8 done.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:01:52 -04:00
Rob
22ee7636ef Add §6.7 KEEPALIVE / link teardown (Tier 2 #1+#2)
Documents the link control plane that's required for any client
that wants links to survive idle periods. Five sub-sections:

  §6.7.1  KEEPALIVE wire form: context = 0xFA, initiator-originated
          0xFF ping body → responder 0xFE pong reply body, both
          Token-encrypted by the link session key. Cadence formula
          RTT × (KEEPALIVE_MAX/KEEPALIVE_MAX_RTT) = RTT × 205.7,
          clamped to [5s, 360s]. Initial value is 360s before RTT
          is measured by validate_proof.

  §6.7.2  STALE → CLOSED transition. Watchdog moves link to STALE
          when last_inbound + 2*keepalive elapses, then on next
          watchdog pass emits LINKCLOSE and goes to CLOSED.
          teardown_reason = TIMEOUT.

  §6.7.3  LINKCLOSE wire form: context = 0xFC, body = 16-byte
          link_id Token-encrypted. Receiver MUST verify
          plaintext == link_id before accepting the close. After
          accepting, link.shared_key/derived_key zeroed for forward
          secrecy.

  §6.7.4  Teardown reason codes: TIMEOUT(0x01), INITIATOR_CLOSED
          (0x02), DESTINATION_CLOSED(0x03). Local-state values, not
          on the wire.

  §6.7.5  Six-step minimum-receiver-responsibility recipe.

Also marks Tier 2 implicit/explicit proof item done — already
covered as part of §6.5's Tier 1 #3 expansion.

Old §6.7 "Source" renumbered to §6.8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 11:59:13 -04:00
Rob
537b1e8182 Fix and expand §1.3 — on-disk identity format (real spec bug!)
Closes Tier 1 #6 and the entire Tier 1 sweep. Previous §1.3 said the
on-disk byte order was Ed25519_priv(32) || X25519_priv(32) ("opposite
of the public_key concatenation"). That was WRONG.

Verified empirically against RNS 1.2.0 by round-tripping the existing
test vectors through Identity.to_file and reading the bytes back:

    disk = X25519_priv(32) || Ed25519_priv(32)    # same as public_key

This matches Identity.get_private_key() at RNS/Identity.py:694-698:
   return self.prv_bytes + self.sig_prv_bytes
where prv_bytes is X25519 (line 679) and sig_prv_bytes is Ed25519
(line 682). It also matches load_private_key at line 706-717.

Implementations following the prior spec wording would have written
identity files that fail to load on upstream RNS — a real interop
break that would have been very hard to debug because the failure is
in keypair-loading, before any signature operation runs.

§1.3 rewritten and expanded:

  - Correct byte order with citation to upstream code.
  - 64-byte raw-blob format with explicit "no header / no version /
    no checksum / no encryption".
  - File-system facts: no chmod, expected to live in OS-protected
    storage, filename is caller-controlled.
  - from_bytes HAZARD note: feeding raw random bytes skips the
    `cryptography` library's keypair-generation invariants
    (X25519 RFC 7748 §5 scalar clamping etc).
  - Cross-implementation portability follows automatically because
    there's nothing in the file but the bytes.
  - ⚠️ Spec correction callout warning future readers about the
    previous wording so the bug history is on record.

tools/verify_destination_hash.py extended with a §1.3 to_file /
from_file round-trip section. For each test vector it now:
  - writes the identity via to_file
  - asserts the on-disk file is exactly 64 bytes
  - asserts disk[:32] hex == expected x25519_priv_hex
  - asserts disk[32:64] hex == expected ed25519_priv_hex
  - reloads via from_file and asserts identity_hash invariance

This is what would have caught the bug if it had been there from the
start. tools/README.md updated to reflect §1.3 coverage.

Cumulative Tier 1 status: 6 of 6 done. A from-scratch client built
from §1-§9 + §10 + §11 + flows/ can now interop with upstream
Reticulum / LXMF / RNode for identity, announce, opportunistic LXMF
DATA, Resource fragmentation, regular PROOF receipts, link
handshakes with MTU/mode signalling, path-? discovery, and
KISS/HDLC/RNode-air-frame framing. Tiers 2 and 3 remain open in the
todo for follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 11:54:54 -04:00
Rob
0bf03d924d Expand §7.2 + add path-discovery flow
Closes Tier 1 #5. The previous §7.2 was four bullet points naming the
"answer with an announce" rule but missing every wire detail —
implementation-time the SF mobile client got steps 4 (dedup) and 5
(local-destination check) wrong on its first cut and the bug only
surfaced as "I can message my own destination but no one else can
reply".

§7.2 is now six sub-sections:

  §7.2.1  Path-request packet parse rules. The handler's slice
          recipe with branching on payload length (32B = leaf form
          target||tag; 48B+ = transport form target||transport_id||
          tag); tag cap at 16B; tagless-request rejection.

  §7.2.2  Tag-based dedup via Transport.discovery_pr_tags. The
          unique_tag = dest_hash || tag construction, the 32000-
          entry cap, why missing this turns a leaf into a broadcast-
          storm amplifier on retransmits.

  §7.2.3  The five-way dispatch in Transport.path_request:
          local-destination / transit-knows-path / local-client-
          forward / discovery-recursive / drop. Branches 1 and 5
          are the only ones a leaf needs.

  §7.2.4  Path-response announce wire format. Body byte-identical
          to a regular announce (§4.1); only the outer packet
          context byte differs (NONE → PATH_RESPONSE 0x0B).
          PR_TAG_WINDOW=30s body-cache that serves identical wire
          bytes to racing relays so transit dedup converges.

  §7.2.5  Timing constants: PATH_REQUEST_GRACE = 0.4s, +
          PATH_REQUEST_RG = 1.5s for roaming-mode interfaces.
          Local-destination and local-client originator branches
          bypass the grace.

  §7.2.6  Minimum responsibility for a non-transport leaf — the
          six-step protocol-level recipe.

flows/path-discovery.md: 9-step chronology covering both
single-hop leaf-owns-target and two-hop transit-relay-knows-path
cases. Wire-byte ladder diagrams for both. Notes the ingress-limit
bypass for path-responses (Transport.py:1632-1639), the
receive_path_responses opt-in for handler dispatch
(Transport.py:1989-1991), and the timeout/escalation path through
LXMRouter.process_outbound's MAX_PATHLESS_TRIES retry counter.

flows/README.md status table updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 11:50:10 -04:00
Rob
dc0a1438e6 Add §6.6 for the 3-byte MTU/mode signalling field
Closes Tier 1 #4. Without this, a clean-room Link implementation that
either always emits the signalling slot or always omits it will fail
handshakes against the opposite-config peer because the LRPROOF
signed_data either includes or excludes the 3 bytes — and the
signature verifies against exactly one of those forms.

§6.6 covers six sub-sections:

  §6.6.1  Wire layout. 24-bit big-endian packed value: top 3 bits of
          byte 0 = mode, low 21 bits = mtu. Citations to encoder at
          RNS/Link.py:147-151 and decoders at :154+, :171+.

  §6.6.2  Mode field. 3 bits, values 0..7. Currently only
          MODE_AES256_CBC = 0x01 is in ENABLED_MODES; six others are
          reserved (AES-128, AES-256-GCM, OTP, four PQ slots).
          Sender-side signalling_bytes() raises on disabled modes;
          receiver-side mode_from_lr_packet returns the raw integer
          without validation. handshake() at line 353 enforces.

  §6.6.3  MTU field. 21 bits, max 2,097,151. Forward-looking width;
          real interfaces are way smaller. Initiator emits its
          next-hop HW_MTU; responder clamps to min(its-view,
          requested) by rewriting the LINKREQUEST data buffer in
          place at RNS/Transport.py:2042-2051 BEFORE
          Destination.receive runs, so the eventual LRPROOF carries
          the clamped value. The clamp also leaves link_id invariant
          because §6.3's hashable_part strips trailing signalling.

  §6.6.4  Presence detection — purely by body length. Lengths 64 vs
          67 for LINKREQUEST, 96 vs 99 for LRPROOF. No flag bit.

  §6.6.5  Signed_data inclusion rule (the interop break) — the LRPROOF
          signs over the signalling bytes when present. A peer that
          omits them when present (or includes them when absent)
          gets a signed_data mismatch and the link never establishes.

  §6.6.6  link_mtu_discovery = No config option. Disables emit on
          the initiator side; receivers don't need a parallel switch
          (length-dispatch handles it).

§6.1 and §6.2 inline references updated to point at §6.6 for the bit
layout instead of the previous "[signalling(3)]" placeholder. The
existing §6.6 "Source" entry renumbered to §6.7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 11:36:51 -04:00
Rob
fa014d21e6 Document microReticulum random_hash interop bug (§4.1 callout + §9.10)
Real interop bug found while checking what the
thatSFguy/reticulum-lora-repeater stack does with the random_hash
field. The repeater is a thin wrapper around attermann/microReticulum,
which emits 10 fully-random bytes for random_hash rather than the
upstream Python form of 5 random bytes + 5 bytes of big-endian uint40
unix_seconds. The Python form is preserved as a comment in
microReticulum src/Destination.cpp:270-272, with a "CBA TODO add in
time to random hash" next to the random-only implementation.

Effect: Python RNS receivers parse random_hash[5:10] as an emission
timestamp via Transport.timebase_from_random_blob (RNS/Transport.py:
3100-3101), and use it for path-table replacement decisions in the
equal-or-greater-hop branch (RNS/Transport.py:1721-1745). A
uniformly-random uint40 has median ~5.5e11 ≈ year 19403 AD, so
microReticulum announces look "far-future" to Python receivers and
permanently win replay-ordering comparisons until the path TTL
expires.

First-contact path-table population is unaffected — the bug only
surfaces on path replacement, which makes it a quiet failure mode
in mixed-vendor meshes (microReticulum repeater + Python rnsd).

Symmetry: microReticulum receivers don't consult the timestamp half,
so microReticulum-to-microReticulum traffic is unaffected. The
asymmetry is what makes the symptom show up only when a Python
relay is also in the mesh.

The repeater's pre_build.py aggressively patches FIVE other
microReticulum protocol bugs (ratchet announce parsing, identity
hash length 16→32, validate_announce/announce diagnostics, DATA/
PROOF forwarding for transport-mode, path-table write dedup) — but
not this one. Filed as an outreach todo to upstream the fix to
attermann/microReticulum.

  SPEC.md §4.1   — adds an UNVERIFIED callout naming the deviation,
                   citing the exact source location and explaining
                   the propagation path through Python's path-table
                   logic.
  SPEC.md §9.10  — gotcha entry making the bug findable from the
                   gotchas list, with a suggested clean-room
                   workaround (emit the timestamp half yourself,
                   even just seconds-since-boot).
  todo.md        — outreach entry to file an issue on
                   attermann/microReticulum proposing the fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 11:27:03 -04:00
Rob
ab66e4040f Expand §6.5 with full PROOF body wire spec (explicit vs implicit)
Closes Tier 1 #3. The previous §6.5 was one paragraph that named
"a PROOF packet" without specifying its body shape, signing input,
or explicit/implicit choice — exactly the level of vagueness that
caused the SF mobile client to ship the wrong proof shape on its
first cut.

New §6.5 has six sub-sections:

  §6.5.1  Two body formats:
            explicit = packet_hash(32) || signature(64) = 96B
            implicit =                    signature(64) = 64B
          Distinguished purely by length at the receiver per
          PacketReceipt.validate_proof (RNS/Packet.py:497-548).

  §6.5.2  Sender-side policy. Opportunistic DATA proofs default to
          the IMPLICIT form (Reticulum.__use_implicit_proof = True
          at RNS/Reticulum.py:259), only switching to explicit when
          the operator's config sets use_implicit_proof = No. Link
          DATA proofs are hardcoded explicit on both emit
          (Link.prove_packet at RNS/Link.py:383-394) and validate
          (validate_link_proof at RNS/Packet.py:449-494, with the
          implicit branch commented out).

  §6.5.3  Where the proof is addressed:
          opportunistic -> packet_hash[:16] as a synthetic
                           ProofDestination
          link          -> link.link_id

  §6.5.4  Wire summary with byte-position ladders for both forms.

  §6.5.5  Receiver tolerance: validators MUST accept both 64- and
          96-byte bodies for opportunistic DATA proofs since the
          upstream default differs from what most non-RNS clients
          assume.

  §6.5.6  Restates the Link-DATA mandatory-receipt rule with
          context-byte clarification.

Side fix: §2.5 contexts table description for LINKPROOF (0xFD)
corrected. The constant is defined upstream but NOT actually emitted
by either Identity.prove or Link.prove_packet — both build their
proof packets with packet_type = PROOF and context = NONE (0x00).
LINKPROOF (0xFD) is reserved but unused in RNS 1.2.0; the proof-ness
of a packet is conveyed by packet_type, not context.

todo.md gets a new "tools/verify_proof_packet.py" entry under the
runtime-verifier section to lock the explicit/implicit dispatch in
with a runtime test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 11:18:56 -04:00
Rob
95823ad840 Add §10 Resource fragmentation + send-resource flow
Closes Tier 1 #2. Without this, a client can't send any LXMF body
larger than LINK_PACKET_MAX_CONTENT ≈ 360 B, can't receive a NomadNet
page that doesn't fit in one MTU, and can't transfer files via rncp.

SPEC.md §10 (new): full Resource fragmentation protocol with citations
to RNS/Resource.py. 13 sub-sections covering preparation pipeline
(metadata prefix → optional bz2 → random_hash prefix → SHA-256 over
data||random_hash → link.encrypt of the WHOLE blob → part-split into
SDU-sized chunks → 4-byte map_hash hashmap with collision guard within
COLLISION_GUARD_SIZE = 2*WINDOW_MAX + HASHMAP_MAX_LEN), wire context
inventory (RESOURCE_ADV / RESOURCE / RESOURCE_REQ / RESOURCE_HMU /
RESOURCE_PRF / RESOURCE_ICL / RESOURCE_RCL), the msgpack dict for the
advertisement (t/d/n/h/r/o/i/l/q/f/m), the request payload format with
the hashmap_exhausted sentinel, the lazy-hashmap RESOURCE_HMU
continuation that lets large hashmaps avoid breaking small-MTU links,
the proof body
   resource_hash(32) || full_proof = SHA256(data||hash) (32)
returned in a PROOF-type packet, the sliding window dynamics
(WINDOW=4 → WINDOW_MAX_FAST=75 / WINDOW_MAX_VERY_SLOW=4 with rate
detection), multi-segment cutover at MAX_EFFICIENT_SIZE = 1 MiB - 1
with the lazy `__prepare_next_segment` pattern, and the
encryption-before-split layering that means a missing part can't be
decrypted in isolation.

flows/send-resource.md: 10-step chronology from RNS.Resource()
construction through advertise → req/parts loop → HMU continuation →
final RESOURCE_PRF → multi-segment fan-out, with a wire-byte ladder
diagram and a per-step source map.

Side fixes found while drafting:
  - SPEC.md §2.5 contexts table was wildly incomplete and had a real
    bug: KEEPALIVE was listed as 0xFD; upstream is 0xFA per
    RNS/Packet.py:87. 0xFD is actually LINKPROOF (the regular
    DATA-receipt context, §6.5). Replaced with the full upstream
    context inventory: NONE, RESOURCE_*, CACHE_REQUEST, REQUEST,
    RESPONSE, PATH_RESPONSE, COMMAND, COMMAND_STATUS, CHANNEL,
    KEEPALIVE, LINKIDENTIFY, LINKCLOSE, LINKPROOF, LRRTT, LRPROOF.
  - SPEC.md §6.5 reworded: "send back a PROOF packet (no context
    byte specifics)" → "send back a PROOF-type packet with
    context = LINKPROOF (0xFD)" for clarity.
  - The previously-numbered §10 "Test vectors" and §11 "Source map"
    are renumbered to §11 / §12 so the new Resource section lands in
    its correct protocol-stack position. agent.md §5 audit table
    updated accordingly.

flows/README.md status table updated; receive-resource.md added as
the next pending flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 11:08:40 -04:00
Rob
a1ec6ce7fd Add receive-announce flow + SPEC §4.5 validation rules
Closes the highest-priority Tier 1 gap. Without this, a from-scratch
client can't learn any peers exist; known_destinations stays empty and
every outbound message fails at recall(dest_hash).

SPEC.md §4.5 (new): announce validation rules with full citations to
RNS/Identity.py::validate_announce (line 496) and the dispatch path in
RNS/Transport.py:1623-2024. Covers the body parse with context_flag
branch, signed_data reconstruction (including the empty-bytes-not-absent
ratchet rule), Ed25519 signature verification, dest_hash recomputation,
public-key collision rejection, blackhole list, cache update order
(known_destinations -> known_ratchets -> path_table), PATH_RESPONSE
distinction, and the implementation-private SHOULD rules around
ingress rate limiting, random_blob history caps, and self-announce
filtering.

flows/receive-announce.md: chronological walk through 9 steps from
deframing to handler dispatch, with the cheap-pre-filter design
(signature-checked-then-counted) called out, the burst-active ingress
limiter explained against IC_BURST_FREQ_NEW=6Hz / IC_BURST_FREQ=35Hz,
the path-table decision tree, and the announce_handlers fan-out with
aspect_filter and PATH_RESPONSE filtering. Ends with a wire-byte
diagram and a per-step source map.

Two side fixes found while drafting:
  - SPEC.md §4.1 had random_hash described as "10 random bytes". It's
    actually random_hash = get_random_hash()[0:5] + int(time.time()).to_bytes(5, "big")
    per RNS/Destination.py:282. Transit relays parse the trailing 5
    bytes via timebase_from_random_blob (RNS/Transport.py:3100) for
    replay-ordering decisions.
  - SPEC.md §2.5 contexts table was missing PATH_RESPONSE = 0x0B
    (RNS/Packet.py:83).

flows/README.md status table updated; the priority-ordered todo list
also gets a few new entries spun off from the work
(send-announce, forward-announce, send-resource, path-discovery flows).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:56:11 -04:00
Rob
c18cff533c todo: spec gaps for a functional client, tiered
Captures the full Tier 1/2/3 list of missing protocol specification
needed for a from-scratch client to interoperate with upstream
Reticulum / LXMF / RNode-firmware.

Each item carries the source-citation hooks I gathered while answering
the question, so whoever picks the work up doesn't have to re-research
where the upstream code lives. Highlights:

  Tier 1 (barebones interop): receive-announce flow + §4.5 validation
  rules, Resource fragmentation §12, regular PROOF body §6.5 expansion,
  3-byte MTU/mode signalling field §6, path-response context 0x0B
  distinction, identity on-disk format §1.3 expansion.

  Tier 2 (useful in the wild): propagation node protocol, KEEPALIVE
  and link teardown §6, LXMF stamps + tickets, NomadNet page protocol
  §13, GROUP destinations, CSMA / airtime tracking, RNode KISS
  configuration handshake §8.5, implicit vs explicit proof mode.

  Tier 3 (transport / relay): DATA forwarding rules §7.7, ANNOUNCE
  rebroadcasting §4.6, path table management §7.8, tunnels and
  shared-instance protocol §7.9, reverse-table link transport §6.x.

Folds the previous "Document the Reticulum Resource fragmentation
protocol" and "Document the Propagation /get pull protocol" entries
from the lower polishing section into Tier 1 / Tier 2 respectively
so they're tracked at the right priority.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:47:23 -04:00
Rob
588dcc9982 Expand §8.3 with the full RNode air-frame split-packet protocol
The previous one-sentence §8.3 was wrong about scope: it said KISS hosts
treat the 1-byte header as opaque pass-through, which is misleading —
the byte lives between RNodes on the LoRa air-frame, not on the KISS
channel. Hosts (RNS, Sideband, etc.) never see it. Any alternative
implementation that talks LoRa to an RNode must construct/parse it
bit-exactly, or its TX is invisible and its RX mistakes the header for
the first payload byte.

New text covers:
  - Header byte layout: bit 7..4 random seq nibble, bit 0 FLAG_SPLIT,
    SEQ_UNSET=0xFF sentinel (Framing.h:105-108).
  - TX rules: header = random(256) & 0xF0 | (FLAG_SPLIT iff
    payload > 254). Both halves of a split share the same byte byte-
    for-byte. Split at 255 bytes total per LoRa frame; max reassembled
    payload 508. (RNode_Firmware.ino:716-742; Config.h:59-61.)
  - RX state machine: at most one buffered first-half keyed by seq
    nibble; four cases for inbound frames (RNode_Firmware.ino:359-446).
  - Reassembly timeout: upstream firmware has none (relies on
    subsequent traffic to evict). The clean-room repeater adds a 500ms
    defensive timeout (reticulum-lora-repeater/src/Radio.cpp:189-194)
    — implementation-private, not part of the wire spec.
  - Sequence-collision ceiling: 4 random bits = 1/16 collision per
    overlapping split-packet pair from the same sender. Don't burst.
  - Note that a "header rotates between transmissions" memory of this
    protocol is a fading recall of the per-TX random seq nibble — there
    is no retransmit-driven byte rotation or rechunk. LoRa TX is
    fire-and-forget; higher-layer retransmit just re-runs the TX path
    and gets a fresh random seq.

todo.md gets an entry for tools/verify_rnode_split.py to lock the
new §8.3 in with a runtime test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:34:18 -04:00
Rob
8480555320 Correct SPEC.md §6.2 LRPROOF body order and §6.3 link_id offsets
Two source-cited corrections found while drafting the link send flow:

§6.2 — the LRPROOF body is signature(64) || responder_X25519_pub(32) ||
[signalling], not link_id || responder_X25519_pub || signature ||
[signalling]. The link_id appears in the packet header (dest_hash
position) per RNS/Packet.py:182-184 when context==LRPROOF, not in the
body. The responder's long-term Ed25519 pub is also NOT on the wire —
both sides know it from a prior announce, and it is included only in the
signature input. Citations: RNS/Link.py:373 (signer), :376 (proof_data),
:417 (validator).

§6.3 — get_hashable_part offsets N are 2 for HEADER_1 and 18 for HEADER_2
(skip flags+hops, and additionally skip transport_id for HEADER_2),
producing the same hashable_part on both sides regardless of relay
conversion. Previously listed as 18/34, which would have stripped the
dest_hash. Citation: RNS/Packet.py:354-361.

Both corrections are direct upstream source citations (criterion #2 from
agent.md §1) so they are recorded as verified. todo.md adds an entry to
write tools/verify_link_handshake.py to lock them in with a runtime test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:07 -04:00
Rob
cf169b2a9e Verify §2.3, §4.3, §7.1, §7.4 against upstream RNS 1.2.0 / LXMF 0.9.6
Adds tools/ verifier scripts that exercise upstream RNS / LXMF and confirm
(or correct) the SPEC.md callouts:

- §2.3 HEADER_1→HEADER_2 conversion: verified by stubbing Transport.transmit
  and seeding a multi-hop path_table entry.
- §4.3 app_data 3-element variant: producer in LXMF 0.9.6 actually emits
  2 elements only (supported_functionality at LXMRouter.py:999 is dead
  code); parser tolerates 1/2/3-element + raw UTF-8.
- §7.1 path? always-precedes claim: actually conditional on
  not has_path() AND method==OPPORTUNISTIC.
- §7.4 ratchet ring default 8: actually Destination.RATCHET_COUNT = 512
  at RNS/Destination.py:85.

Also fixes a documentation bug in §1.2: the rnstransport.path.request row
of the well-known-hash table had the dest-hash prefix where the name_hash
should be (correct name_hash is 7926bbe7dd7f9aba88b0).

Seeds test-vectors/identities.json (Alice + Bob) with a regenerator
(tools/regen_identities.py) and verifier (tools/verify_destination_hash.py)
covering §1.1 and §1.2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:14:51 -04:00
Rob
6435c0a0a0 Add todo.md with outstanding work list
Captures the four next-task buckets:
- Outreach (file issue on markqvist/Reticulum)
- Test infrastructure (bootstrap test-vectors/, write priority verifier scripts)
- Open UNVERIFIED items in SPEC.md to resolve
- Spec polishing (split SPEC.md, document Resource and Propagation /get)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:42:56 -04:00