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>
97 lines
5 KiB
Markdown
97 lines
5 KiB
Markdown
# 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 |
|