From 375521c963f28f0a6189c1279051858e8f9ce1fd Mon Sep 17 00:00:00 2001 From: Rob <95710162+thatSFguy@users.noreply.github.com> Date: Tue, 19 May 2026 17:19:56 -0400 Subject: [PATCH] =?UTF-8?q?spec:=20=C2=A710.7=20=E2=80=94=20an=20exhausted?= =?UTF-8?q?=20RESOURCE=5FREQ=20may=20carry=20parts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A conformant sender fulfils any bundled `requested_map_hashes` AND sends the RESOURCE_HMU. Verified against RNS 1.2.9 (`Resource.py:982-1071`): part fulfilment runs unconditionally for every REQ, and the HMU branch runs in addition. The reference receiver (`request_next`) routinely bundles parts into an exhausted REQ. §10.7 now states the correct rule; part-less exhausted REQs are an allowed receiver-side simplification. `playbook.md` §7 records the matching fwdsvc conformance bug (since fixed in `reticulum-forwarding-service` PR #10). Co-Authored-By: Claude Opus 4.7 (1M context) --- SPEC.md | 28 ++++++++++++++++++++++++++++ playbook.md | 7 +++++++ 2 files changed, 35 insertions(+) diff --git a/SPEC.md b/SPEC.md index b424ee2..c58e137 100644 --- a/SPEC.md +++ b/SPEC.md @@ -2508,6 +2508,34 @@ The segment_index is `part_index // HASHMAP_MAX_LEN`. The receiver applies this If the part_index doesn't land on a `HASHMAP_MAX_LEN` boundary, the sender treats it as a sequencing error and cancels the resource (`Resource.py:1043-1046`). +> **An exhausted RESOURCE_REQ MAY still carry parts — a conformant +> sender fulfils them *and* sends the RESOURCE_HMU.** When the +> `hashmap_exhausted_flag` is `0xFF`, the REQ body may still end with a +> non-empty `requested_map_hashes` trailer (§10.5). The sender MUST emit +> the requested part packets **and** the hashmap continuation; the two +> are independent. Serving the HMU is not a substitute for fulfilling +> the bundled part requests. +> +> In the RNS reference (`Resource.py:982-1071`, `request()` — verified +> against RNS 1.2.9, the current release), the part-fulfilment loop runs +> for every REQ regardless of the flag, and the `if wants_more_hashmap:` +> HMU branch runs afterward, in addition. The reference receiver +> (`request_next`, `Resource.py:931-981`) routinely produces this +> packet shape: as its window scan reaches the end of the known +> hashmap, it has already accumulated the still-outstanding part-hashes +> from the known region into `requested_hashes`, then sets the exhausted +> flag and stops — emitting +> `0xFF || last_map_hash(4) || resource_hash(32) || requested_map_hashes`. +> +> A receiver MAY keep part requests and hashmap pulls in separate REQ +> packets — emitting a **part-less** exhausted REQ +> (body `0xFF || last_map_hash(4) || resource_hash(32)`, no trailing +> map_hashes) purely to pull the continuation. This interoperates with +> every conformant sender. But it is a receiver-side simplification +> only: a sender MUST NOT assume peers do this, and MUST NOT skip part +> fulfilment for an exhausted REQ. A sender that does drops every +> bundled part silently — see `playbook.md` §7 (2026-05-19). + ### 10.8 RESOURCE_PRF — final proof When the receiver has assembled the full resource (`received_count == total_parts`), it runs `assemble()` (`Resource.py:672-726`): diff --git a/playbook.md b/playbook.md index 3d8bc47..3ec516e 100644 --- a/playbook.md +++ b/playbook.md @@ -191,6 +191,13 @@ Spec-only repos with a "the source is the source of truth" attitude die slowly b Each entry: date, one-line symptom, spec section that governs it, one-line fix, one-sentence lesson. Append-only. New entries go at the top. +### 2026-05-19 — fwdsvc dropped parts bundled into an exhausted RESOURCE_REQ + +- **Symptom:** Images relayed mobile→mobile through the Fwd service never arrive (whole LXMF message lost); mobile→Sideband through the same service works. Recipient logs hundreds of `RESOURCE chunk did not match any known hashmap slot`. Only triggers for resources large enough to need RESOURCE_HMU (>`HASHMAP_MAX_LEN` ≈ 74 parts). +- **Spec section:** §10.7. An `exhausted == 0xFF` RESOURCE_REQ MAY still carry a `requested_map_hashes` trailer, and a conformant sender serves those parts **and** the RESOURCE_HMU. The fwdsvc Go sender did `if req.Exhausted { serveHmu(req); continue }`, skipping `fulfillRequest` entirely — its own comment claimed this "mirrors upstream `Resource.request()`", but upstream (`Resource.py:982-1071`, checked against RNS 1.2.9, the current release) runs part fulfilment unconditionally and *then* sends the HMU. The mobile receiver flags `exhausted` on the first REQ of each hashmap window and bundles ~74 part-hashes with it — which a reference RNS sender honours — so fwdsvc served HMUs and dropped every bundled part across all 19 windows. +- **Fix:** `resource_sender.go` Run loop now runs `fulfillRequest` for every REQ, then `serveHmu` when `req.Exhausted`. It never skips part fulfilment. (`reticulum-forwarding-service`.) +- **Lesson:** mobile→Sideband "working" was a false green — a reference RNS receiver drains each segment before it flags exhausted, so on a clean link it sends part-less exhausted REQs and never exercised the bug. A lenient/conventional peer masks a divergence as effectively as a self-round-trip does (§5.1); the fault is receiver-dependent in a hop whose sender is constant. A `// mirrors upstream` comment proves nothing without the §2.4 / §6.2 check behind it. + ### 2026-05-10 — LRPROOF signed_data signalling asymmetry - **Symptom:** Mobile-app's Kotlin engine fails LRPROOF signature verification against fwdsvc on every attempt. Falls back to opportunistic; link delivery never works.