spec: §10.7 — an exhausted RESOURCE_REQ may carry parts
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) <noreply@anthropic.com>
This commit is contained in:
parent
4c1d3de421
commit
375521c963
2 changed files with 35 additions and 0 deletions
28
SPEC.md
28
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`):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue