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:
Rob 2026-05-19 17:19:56 -04:00 committed by GitHub
commit 375521c963
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 35 additions and 0 deletions

28
SPEC.md
View file

@ -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`):