reticiulum-specification/flows/receive-resource.md
Rob cfd0d8249b Re-anchor against RNS 1.2.4 / LXMF 0.9.7 + track upstream distribution shift
Upstream RNS 1.2.4 (2026-05-07) announces it is "probably the last
release that is also published to GitHub" — pip continues until rnpkg
is complete and RNS is self-hosting. All 13 verifiers pass against
1.2.4 / 0.9.7; no wire-format, signing, or protocol behavior changed
between 1.2.0 and 1.2.4, so the changes here are purely currency:

- Pin tools/requirements.txt to rns==1.2.4 / lxmf==0.9.7 so the
  verifier stays reproducible if upstream stops mirroring to PyPI
  before the migration is ready.
- Add an "Upstream distribution shift" watch-list to todo.md (local
  Reticulum node, repo destination hash, rnpkg install/upgrade
  commands, rsg signature verification, mirroring source citations).
- Bump SPEC.md frontmatter and re-anchor ~50 line citations across
  Identity.py, Transport.py, Resource.py, Link.py, Reticulum.py,
  Packet.py, and LXMF/* (Identity.py drift was the heaviest at +13
  to +31 lines; Transport.py was variable). Fix one numeric
  (MAX_RANDOM_BLOBS = 32 → 64) and one semantic (§6.6.3 LRPROOF MTU
  clamp citation pointed at the wrong location — corrected to point
  at the transit-relay clamp at Transport.py:1539-1556).
- Update §10.4 decompression-bomb hazard to note upstream's 1.1.9 cap
  adoption, with citations to Resource.py:686-691 and Buffer.py:95-97
  plus a "do not use one-shot bz2.decompress()" warning.
- Re-anchor 11 flows/ files (version pins + ~30 line citations).
- Bump version labels in tools/README.md, test-vectors/README.md, and
  4 verifier docstrings + 2 hardcoded print strings.

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

97 lines
5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Flow: receive a Resource (large body) over a Link
The inverse of [`send-resource.md`](send-resource.md). What happens chronologically on the receiver when an inbound Resource transfer arrives. Pinned against **RNS 1.2.4**; see [`../SPEC.md`](../SPEC.md) §10 for the wire bytes.
---
## Preconditions
- Link is `ACTIVE` (§6).
- Receiver registered a `resource_strategy` on the Link via `set_resource_strategy(...)``ACCEPT_NONE`, `ACCEPT_APP`, or `ACCEPT_ALL`. Default `ACCEPT_NONE` rejects every Resource on the link; LXMF and NomadNet flip this to `ACCEPT_APP` so an app callback can decide.
---
## Sequence
### 1. RESOURCE_ADV arrives
Inbound Link DATA packet with `context = RESOURCE_ADV (0x02)`. `Link.receive` at `RNS/Link.py:1065-1098` decrypts it and runs `RNS.ResourceAdvertisement.unpack` against the plaintext to extract the msgpack dict (§10.4).
### 2. Resource accept / reject decision
Branch by `resource_strategy`:
- **`ACCEPT_NONE`** → call `RNS.Resource.reject(adv_packet)` which sends `RESOURCE_RCL` back; resource is dropped.
- **`ACCEPT_APP`** → run the application callback `link.callbacks.resource(adv)`. If it returns truthy, `RNS.Resource.accept(...)`; otherwise reject.
- **`ACCEPT_ALL`** → unconditional `RNS.Resource.accept(...)`.
`Resource.accept` (`RNS/Resource.py:167-244`) constructs a receiver-side Resource object, copies fields from the advertisement, sets `status = TRANSFERRING`, and queues the first request.
### 3. Receiver issues the first RESOURCE_REQ
`Resource.request_next()` (`RNS/Resource.py:931-981`) builds the request body per §10.5:
```
exhausted_flag(1) [|| last_map_hash(4)] || resource_hash(32) || requested_map_hashes(N × 4)
```
`N = link.window` initially (default 4). Sent as Link DATA with `context = RESOURCE_REQ (0x03)`.
### 4. Sender fulfills with RESOURCE part packets
For each requested map_hash, the sender (per `flows/send-resource.md` step 5) emits one Link DATA packet with `context = RESOURCE (0x01)`, body = pre-encrypted part bytes. The receiver matches each arriving part to the hashmap by recomputing its 4-byte map_hash (`Resource.receive_part` line 831-932).
Successful match: `parts[i] = part_data`, `consecutive_completed_height` advances. The window grows by 1 each successful round (capped at `window_max`, with rate-detection upgrades to FAST or VERY_SLOW per §10.10).
### 5. Repeat steps 3-4 until `received_count == total_parts`
When the receiver has consumed every map_hash in the current segment, it issues another RESOURCE_REQ. If the hashmap is exhausted (`exhausted_flag = 0xFF`), the sender responds with a RESOURCE_HMU carrying the next hashmap segment (§10.7), and the loop continues.
### 6. `Resource.assemble()` reassembles, validates, decrypts
`Resource.py:672-726`:
1. `stream = b"".join(self.parts)` — concatenate every part.
2. `data = link.decrypt(stream)` — single Link Token decrypt of the whole blob (§10.12: encryption was applied to the whole concatenated body before splitting).
3. Strip the 4-byte `random_hash` prefix.
4. If `compressed`: bz2-decompress.
5. `calculated_hash = SHA256(data || random_hash)`. Compare to `self.hash` (= advertisement's `h` field). On match: `status = COMPLETE`. On mismatch: `status = CORRUPT`; cancel.
6. If `has_metadata`: peel off the 3-byte length-prefixed msgpack metadata blob, write to `meta_storagepath`.
7. Write the data to `storagepath` (file-backed) or hold in `self.data` (memory-backed).
8. Call the application callback (the one passed to `Resource.accept`).
### 7. RESOURCE_PRF emission
`Resource.prove()` (line 755-766) sends back:
```
proof_data = resource_hash(32) || full_proof(32)
where full_proof = SHA256(data_with_random || resource_hash)
```
as a PROOF-type packet with `context = RESOURCE_PRF (0x05)`. The sender's `validate_proof` matches `proof_data[32:]` against its precomputed `expected_proof` and transitions to `COMPLETE` (§10.8).
### 8. Multi-segment continuation
If `segment_index < total_segments`, the sender prepares and sends the next RESOURCE_ADV after receiving this segment's PRF. The receiver loops back to step 1 for the next segment. Each segment is a fully independent Resource transfer; the only thing that ties them together is the `original_hash` field in the advertisement.
### 9. Cancellation paths
`RESOURCE_ICL` (sender cancel) → receiver pops the matching incoming Resource and discards accumulated parts (`Link.py:1131-1138`).
`RESOURCE_RCL` (sender hears the receiver rejected) → already handled receiver-side at step 2.
---
## Source map
| Step | File | Function / line |
|---|---|---|
| 1 | `RNS/Link.py` | `Link.receive` RESOURCE_ADV branch, line 1065 |
| 2 | `RNS/Resource.py` | `Resource.accept`, `Resource.reject`, lines 155-244 |
| 3 | `RNS/Resource.py` | `request_next`, line 934 |
| 4 | `RNS/Resource.py` | `receive_part`, line 831 |
| 5 | `RNS/Link.py` | RESOURCE_HMU branch, line 1122 |
| 6 | `RNS/Resource.py` | `assemble`, line 672 |
| 7 | `RNS/Resource.py` | `prove`, line 755 |
| 8 | `RNS/Resource.py` | `__prepare_next_segment`, line 768 |
| 9 | `RNS/Link.py` | RESOURCE_ICL branch, line 1131 |