reticiulum-specification/flows/send-link-lxmf.md

229 lines
17 KiB
Markdown
Raw Permalink Normal View History

Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
# Flow: send an LXMF message over a Reticulum Link (DIRECT method)
What happens chronologically when an app calls `LXMRouter.handle_outbound(lxm)` for an `LXMessage` whose `desired_method == DIRECT` (or whose payload exceeds the opportunistic single-packet content limit and is downgraded from `OPPORTUNISTIC` to `DIRECT` at pack time). The `DIRECT` method runs the LXMF body over an established Reticulum Link rather than a single Reticulum DATA packet.
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
Pinned against **RNS 1.2.4 / LXMF 0.9.7**. Line numbers below are from those versions.
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
Out of scope: opportunistic delivery (see [`send-opportunistic-lxmf.md`](send-opportunistic-lxmf.md)), propagation-node delivery (`PROPAGATED`), and paper messages (`PAPER`).
---
## When DIRECT runs
DIRECT is reached two ways:
1. **App-requested:** `LXMessage(desired_method=LXMessage.DIRECT, …)`. Used for messages too large for one packet, or for sessions where the app wants the link's full-duplex DATA channel (e.g. an interactive chat). The router opens or reuses an `RNS.Link` to the recipient and sends the message over it.
2. **Auto-downgrade from OPPORTUNISTIC:** `LXMessage.pack` at `LXMF/LXMessage.py:394-398` falls back to `DIRECT` if the computed LXMF `content_size` exceeds `ENCRYPTED_PACKET_MAX_CONTENT`. The originator may or may not surface this transition to the user; it's silent at the protocol layer.
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
Within DIRECT there are two **representations** decided at pack time (`LXMF/LXMessage.py:405-421`). Upstream compares its computed `content_size = len(msgpack_payload) - TIMESTAMP_SIZE - STRUCT_OVERHEAD`, not simply `len(content)` or the full signed body:
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
- **PACKET** — the body fits in a single `LINK_PACKET_MAX_CONTENT`-sized DATA packet on the link.
- **RESOURCE** — the body is larger than that and must be sent as an `RNS.Resource` (multi-packet, fragmented, with its own checksum / progress / retransmit machinery). The Resource fragmentation protocol is documented in [`../SPEC.md`](../SPEC.md) §10 and the dedicated send/receive Resource flows. This flow document covers PACKET in full and links to those details for RESOURCE.
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
---
## Sequence
### 1. App constructs `LXMessage` and submits it
Same as steps 1-2 of `send-opportunistic-lxmf.md`. `LXMessage.pack()` builds the same `dest_hash || src_hash || signature || msgpack_payload` bytes per [`../SPEC.md`](../SPEC.md) §5.5; the difference is that for DIRECT delivery the **recipient's `dest_hash` is NOT stripped** before transmission (SPEC.md §5.2 vs §5.1) — the link payload includes it. `LXMRouter.handle_outbound` adds the message to `pending_outbound` and starts `process_outbound` in a thread.
### 2. `process_outbound` enters the DIRECT branch
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
`LXMF/LXMRouter.py:2531-2545`. Logic for an `LXMessage` whose `method == DIRECT`:
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
```python
delivery_destination_hash = lxmessage.get_destination().hash
direct_link = self.direct_links.get(delivery_destination_hash) \
or self.backchannel_links.get(delivery_destination_hash)
if direct_link is not None:
if direct_link.status == RNS.Link.ACTIVE:
# Step 5 below — link is up, transfer the LXM
lxmessage.set_delivery_destination(direct_link)
lxmessage.send()
elif direct_link.status == RNS.Link.CLOSED:
# Drop link, request_path, retry
...
else:
# PENDING — wait for ACTIVE or CLOSED via established_callback
...
else:
# No link exists; establish one IF a path is known, else request_path first.
if RNS.Transport.has_path(delivery_destination_hash):
delivery_link = RNS.Link(lxmessage.get_destination()) # step 3
delivery_link.set_link_established_callback(self.process_outbound)
self.direct_links[delivery_destination_hash] = delivery_link
else:
RNS.Transport.request_path(delivery_destination_hash) # step 0 (path?)
lxmessage.next_delivery_attempt = time.time() + LXMRouter.PATH_REQUEST_WAIT
```
Setting `process_outbound` itself as the link's established-callback is the trick that connects steps 3-4 to step 5: when the LRPROOF arrives and the link transitions to ACTIVE, the callback re-enters `process_outbound`, finds the now-active link in `self.direct_links`, and proceeds to the transfer branch.
### 3. `RNS.Link(destination)` builds and sends a LINKREQUEST
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
`RNS/Link.py:233-330`. The `Link` constructor on the **initiator** side generates a fresh ephemeral X25519 + Ed25519 keypair (`pub_bytes` and `sig_pub_bytes`) and constructs the LINKREQUEST body (`RNS/Link.py:308-324`):
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
```
request_data = initiator_X25519_pub(32) || initiator_Ed25519_pub(32) || [signalling(3)]
```
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
Both initiator-side keys are **fresh-ephemeral**, used only for this link. The optional 3-byte `signalling` field, present iff `Reticulum.link_mtu_discovery()` returns true and the next-hop interface advertises an HW MTU, encodes the path-MTU and link-mode hints (SPEC.md §6.1; encode/decode helpers at `RNS/Link.py:148-152`).
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
```python
self.packet = RNS.Packet(destination, self.request_data, packet_type=RNS.Packet.LINKREQUEST)
self.packet.pack()
self.set_link_id(self.packet) # = SHA256(get_hashable_part)[:16]
RNS.Transport.register_link(self)
self.start_watchdog() # 60s establishment_timeout per hop
self.packet.send()
```
The LINKREQUEST is a **regular Reticulum DATA-routed packet**`packet_type = LINKREQUEST (2)`, `destination_type = SINGLE`, addressed to the recipient's `lxmf.delivery` `dest_hash`. Per `RNS/Packet.py::pack` (`RNS/Packet.py:192-194`) `LINKREQUEST` packets are **not encrypted** — the body is `request_data` verbatim — because the responder needs to decode the public keys to perform the handshake.
The link_id is set immediately on the initiator side via `set_link_id` (SPEC.md §6.3): the SHA-256-truncated-to-16 hash of `get_hashable_part(LINKREQUEST_packet)`, with trailing signalling bytes stripped via `link_id_from_lr_packet`. Since `get_hashable_part` is invariant under HEADER_1↔HEADER_2 conversion (`RNS/Packet.py:354-361`), the responder will arrive at the same link_id even if the LINKREQUEST passed through one or more relays.
The LINKREQUEST then goes through the same `Transport.outbound` path as any other DATA packet (steps 7-9 of `send-opportunistic-lxmf.md`), which means it can itself be subject to the path-table miss → path-request preamble before it leaves.
### 4. Wait for LRPROOF; verify; complete handshake
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
The initiator's link sits in `Link.PENDING` while it waits for the responder's LRPROOF. The responder's side of this exchange (entering `Destination.receive` with `packet_type == LINKREQUEST``incoming_link_request``Link.validate_request``Link.handshake``Link.prove`, all at `RNS/Link.py:186-230, 353-381`) is its own flow document; this flow describes only what the initiator sees.
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
The LRPROOF arrives back at `Transport.inbound` with `packet_type = PROOF`, `context = LRPROOF (0xff)`, `dest_hash = link_id`. `Transport.inbound` looks up the link by its `link_id` and dispatches to `Link.validate_proof` (`RNS/Link.py:396`). For an initiator with link still in `PENDING`:
```
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
proof_data layout (RNS/Link.py:371):
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
signature(64) || responder_X25519_pub(32) || [signalling(3)]
```
Validation (`RNS/Link.py:410-422`):
1. Parse `signature = packet.data[:64]`, `peer_pub_bytes = packet.data[64:96]`.
2. Read the responder's long-term Ed25519 public key from the `Destination.identity` we already had cached (from a prior announce — line 412: `self.destination.identity.get_public_key()[ECPUBSIZE//2:ECPUBSIZE]`). The Ed25519 pub is **not** sent in the LRPROOF body; it must be known locally already.
3. Derive `signed_data = link_id || responder_X25519_pub || responder_long_term_Ed25519_pub || [signalling]` (line 417).
4. Verify the Ed25519 signature against `signed_data` using the responder's long-term Ed25519 pub.
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
5. On success: `link.handshake()` runs (`RNS/Link.py:353-370`) — ECDH with the responder's fresh X25519 pub, then HKDF over the shared secret with `salt = link_id`, derives `signing_key(32) || encrypt_key(32)` per SPEC.md §6.4. Link state transitions to `Link.ACTIVE`.
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
6. The initiator's `established_callback` registered at step 2 — `LXMRouter.process_outbound` — fires, re-entering step 2 with the link now ACTIVE.
Resolve issue #6 — LRRTT and HEADER_1 for link-addressed DATA (§6.4.2, §6.4.3) Upstream RNS enforces two requirements in code that SPEC.md left implicit; both caused silent message loss in a clean-room Go LXMF service against upstream Python rns 1.2.4 / lxmf 0.9.7. §6.4.2 LRRTT — initiator's link-activation packet - HEADER_1, DATA, dest_type=LINK (0x03), ctx=0xfe; body is `umsgpack.packb(rtt_seconds)` encrypted with the link's session keys. - The responder transitions HANDSHAKE→ACTIVE only on LRRTT receipt (Link.py:534-553), which is also what fires the link_established callback. LXMF's set_resource_strategy(ACCEPT_APP) is installed from that callback; without it, every RESOURCE_ADV the initiator sends hits the silent ACCEPT_NONE branch at Link.py:1087. §6.4.3 Header type for post-handshake DATA and Resource - Link-addressed packets are routed via link_table, which forwards header bytes verbatim (Transport.py:1587-1622). HEADER_2 with a relay's transport_id therefore arrives at the destination intact and is dropped by packet_filter (Transport.py:1283-1285) as "for another transport instance". - Mandates HEADER_1 with no transport_id for all post-handshake link DATA / Resource / control packets regardless of hop count. - Asymmetry with LINKREQUEST (which IS path_table-routed and so HEADER_2-eligible) is spelled out. Companion changes: - §6.4 renamed to "Session keys and link activation"; existing HKDF content moved into §6.4.1. - §2.5 LRRTT context-byte entry points at §6.4.2. - §12.5.2 (Link DATA forwarding) cross-references §6.4.3. - §14 failure-modes table: two new entries for the silent-drop chains documented above. - flows/send-link-lxmf.md step 4 strengthened (LRRTT is mandatory, not informational); step 6 corrected (Transport.outbound does NOT apply HEADER_1→HEADER_2 for link DATA — that conversion is path_table-keyed, link DATA is link_table-keyed). - test-vectors/links.json extended with an LRRTT entry: pinned rtt_seconds=0.05 + pinned 16-byte IV produces deterministic wire bytes for the encrypted body. - tools/regen_links.py drives the LRRTT generation with an os.urandom patch for the Token IV. - tools/verify_link_lrrtt.py (new) locks the wire claims: HEADER_1, ctx=0xfe, dest=link_id, body decrypts under derived_key to msgpack float64 matching rtt_seconds. Citations all verified against installed RNS 1.2.4 / LXMF 0.9.7. All 14 verifiers PASS.
2026-05-10 14:35:56 -04:00
A confirmed-MTU hint in the LRPROOF (length is `64+32+3 = 99` instead of `64+32 = 96`) updates `link.mtu` to the smaller of the responder's hint and the initiator's view (`RNS/Link.py:404-408`).
Immediately after step 5 succeeds — and before any application DATA leaves — the initiator MUST emit a Link Round-Trip Time packet (`context = LRRTT (0xfe)`, `RNS/Link.py:440-442`). This is **not** an informational hint; the responder uses LRRTT receipt as the sole trigger to transition its own link state from `HANDSHAKE` to `ACTIVE` and to fire the application's `link_established_callback` (per SPEC.md §6.4.2). On the LXMF responder that callback is what installs `set_resource_strategy(ACCEPT_APP)` (`LXMF/LXMRouter.py:1855`); without it, every Resource ADV the initiator sends — including any LXM body large enough to spill from packet form into Resource form — silently hits the `ACCEPT_NONE` branch on the responder and is dropped. The initiator transitions to `ACTIVE` independently on LRPROOF validation, but the responder does not.
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
### 5. Transfer the LXM body over the active link
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
Back in `process_outbound` (`LXMF/LXMRouter.py:2620-2630`), with `direct_link.status == ACTIVE`:
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
```python
lxmessage.set_delivery_destination(direct_link) # __delivery_destination = link
lxmessage.send() # LXMessage.send branches on representation
```
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
`LXMessage.send` for DIRECT/PACKET (`LXMF/LXMessage.py:471-490`) calls `self.__as_packet()` which constructs:
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
```python
RNS.Packet(self.__delivery_destination, self.packed) # full LXMF body, dest_hash included
```
i.e. the destination is the **Link object**, and the data is the full `dest_hash || src_hash || signature || msgpack_payload` (SPEC.md §5.2 — Link delivery does NOT strip dest_hash from the body, in contrast to opportunistic delivery).
When `RNS.Packet.pack()` runs for this packet (`RNS/Packet.py:176+`), the destination type is `LINK`, so:
- The header is `flags(1) || hops(1) || link_id(16) || context=0x00(1)`. The `link_id` occupies the dest_hash position.
- `destination.encrypt(plaintext)` for a Link calls into the link's session-key Token form (`RNS/Cryptography/Token.py`). Per SPEC.md §3.1 the Link-derived Token form omits the `ephemeral_pub` prefix because both sides already share the session key from the handshake — the wire body is `iv(16) || aes_ciphertext || hmac(32)`.
`Packet.send()` returns a `PacketReceipt`; LXMF binds:
```python
receipt.set_delivery_callback(self.__mark_delivered)
receipt.set_timeout_callback(self.__link_packet_timed_out)
```
so the LXMessage advances `SENT → DELIVERED` when the recipient's PROOF for this DATA packet arrives (SPEC.md §6.5 — every CTX_NONE DATA packet on a link gets a mandatory PROOF receipt).
### 6. Wire bytes leave (KISS / HDLC framing)
Resolve issue #6 — LRRTT and HEADER_1 for link-addressed DATA (§6.4.2, §6.4.3) Upstream RNS enforces two requirements in code that SPEC.md left implicit; both caused silent message loss in a clean-room Go LXMF service against upstream Python rns 1.2.4 / lxmf 0.9.7. §6.4.2 LRRTT — initiator's link-activation packet - HEADER_1, DATA, dest_type=LINK (0x03), ctx=0xfe; body is `umsgpack.packb(rtt_seconds)` encrypted with the link's session keys. - The responder transitions HANDSHAKE→ACTIVE only on LRRTT receipt (Link.py:534-553), which is also what fires the link_established callback. LXMF's set_resource_strategy(ACCEPT_APP) is installed from that callback; without it, every RESOURCE_ADV the initiator sends hits the silent ACCEPT_NONE branch at Link.py:1087. §6.4.3 Header type for post-handshake DATA and Resource - Link-addressed packets are routed via link_table, which forwards header bytes verbatim (Transport.py:1587-1622). HEADER_2 with a relay's transport_id therefore arrives at the destination intact and is dropped by packet_filter (Transport.py:1283-1285) as "for another transport instance". - Mandates HEADER_1 with no transport_id for all post-handshake link DATA / Resource / control packets regardless of hop count. - Asymmetry with LINKREQUEST (which IS path_table-routed and so HEADER_2-eligible) is spelled out. Companion changes: - §6.4 renamed to "Session keys and link activation"; existing HKDF content moved into §6.4.1. - §2.5 LRRTT context-byte entry points at §6.4.2. - §12.5.2 (Link DATA forwarding) cross-references §6.4.3. - §14 failure-modes table: two new entries for the silent-drop chains documented above. - flows/send-link-lxmf.md step 4 strengthened (LRRTT is mandatory, not informational); step 6 corrected (Transport.outbound does NOT apply HEADER_1→HEADER_2 for link DATA — that conversion is path_table-keyed, link DATA is link_table-keyed). - test-vectors/links.json extended with an LRRTT entry: pinned rtt_seconds=0.05 + pinned 16-byte IV produces deterministic wire bytes for the encrypted body. - tools/regen_links.py drives the LRRTT generation with an os.urandom patch for the Token IV. - tools/verify_link_lrrtt.py (new) locks the wire claims: HEADER_1, ctx=0xfe, dest=link_id, body decrypts under derived_key to msgpack float64 matching rtt_seconds. Citations all verified against installed RNS 1.2.4 / LXMF 0.9.7. All 14 verifiers PASS.
2026-05-10 14:35:56 -04:00
Same as `send-opportunistic-lxmf.md` step 9 — the framed link DATA packet leaves the interface. Note that, in contrast to opportunistic DATA, **`Transport.outbound` does NOT apply the HEADER_1→HEADER_2 conversion for link-addressed packets**, regardless of how many transport hops the link traverses. The HEADER_1→HEADER_2 path is keyed on a `path_table` lookup against `dest_hash`; a link-addressed packet's `dest_hash` is the `link_id`, which lives in `link_table`, not `path_table`. Relays then forward it via `link_table` forwarding — which preserves the header bytes verbatim — so emitting HEADER_2 with `transport_id` set on a link-addressed packet would have it dropped at the destination's `packet_filter` as "for another transport instance" (SPEC.md §6.4.3). The next-hop interface for a link is cached on the link object (`link.attached_interface`) so all subsequent traffic uses that same interface without further path-table lookup.
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
### 7. PROOF receipt arrives → `__mark_delivered` fires
The recipient (per [`receive-link-lxmf.md`](receive-link-lxmf.md)) decrypts the link DATA, parses the LXMF body via `LXMessage.unpack_from_bytes`, validates the signature, and emits a PROOF for this packet (`Packet.prove` from inside `LXMRouter.delivery_packet` at line 1820, same as the opportunistic receive). The PROOF travels back along the link, `PacketReceipt.proven` resolves on the sender, and `__mark_delivered` puts the LXMessage in `DELIVERED`.
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
### 8. (Optional) RESOURCE representation for large bodies
If computed `content_size` exceeds `LINK_PACKET_MAX_CONTENT` (`LXMF/LXMessage.py:405-421`), `representation` is set to `RESOURCE` and step 5 instead constructs an `RNS.Resource` (`__as_resource`, line 651):
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
```python
RNS.Resource(self.packed, self.__delivery_destination, callback=..., progress_callback=..., auto_compress=...)
```
The `RNS.Resource` machinery handles fragmentation, ordering, retransmission, and progress reporting on top of the link's DATA channel. Each fragment is a DATA packet with `context=RESOURCE`; Resource-level completion is acknowledged by `RESOURCE_PRF`. See SPEC.md §10 and `flows/send-resource.md` / `flows/receive-resource.md`.
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
When the resource concludes successfully, the same `__mark_delivered` path runs as for PACKET.
### 9. Backchannel identification (optional, after first successful DIRECT delivery)
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
`LXMF/LXMRouter.py:2531-2545`. After a DIRECT delivery completes (`lxmessage.state == DELIVERED`), if the link doesn't yet have a backchannel identity associated, the initiator's `LXMRouter` calls `direct_link.identify(backchannel_identity)`:
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
- `direct_link.identify` sends an IDENTIFY packet on the link bound to one of the **sender's** local `lxmf.delivery` destinations.
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
- The receiving side's `delivery_link_established` callback (set at `LXMRouter.py:1852-1858`) installs `delivery_packet` on the link's packet callback, so subsequent inbound DATA on this link reaches the LXMF parser.
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
- The link is now usable in **both directions** without each side having to open its own Link to the other. The receiver can now reply over this same link.
This enables an interactive conversation over a single link rather than each message opening a new link.
### 10. Link teardown
A link stays in `ACTIVE` until either side calls `link.teardown()`, the watchdog times out, or the next-hop interface goes down. `RNS/Link.py` keepalives use `KEEPALIVE (0xFA)`; their cadence is dynamically clamped between `KEEPALIVE_MIN = 5s` and `KEEPALIVE_MAX = 360s`.
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
Teardown sends an encrypted `LINKCLOSE (0xFC)` packet whose plaintext body is `link_id`; once teardown completes, the link is removed from `Transport.active_links` and from `LXMRouter.direct_links`, and the next DIRECT message to the same destination has to repeat the full handshake from step 3.
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
---
## Wire-byte summary
The handshake in three packets:
```
LINKREQUEST (initiator → responder), unencrypted body:
[ 1B flags ][ 1B hops ][ 16B responder_dest_hash ][ 1B context=0x00 ]
[ 32B initiator_X25519_pub ][ 32B initiator_Ed25519_pub ][ optional 3B signalling ]
LRPROOF (responder → initiator), unencrypted body:
[ 1B flags ][ 1B hops ][ 16B link_id ][ 1B context=0xff ]
[ 64B Ed25519_signature ][ 32B responder_X25519_pub ][ optional 3B signalling ]
DATA on link (either direction), Token-encrypted (no eph_pub prefix):
[ 1B flags ][ 1B hops ][ 16B link_id ][ 1B context=0x00 ]
[ 16B iv ][ N×16B aes_ciphertext ][ 32B hmac_sha256 ]
(plaintext = full LXMF body: dest_hash || src_hash || signature || msgpack_payload)
```
At the default 319-byte computed-content boundary, the full LXMF plaintext is
431 bytes and the complete Link DATA wire packet is 499 bytes. At 320,
upstream switches to Resource. See `test-vectors/link-lxmf.json`.
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
Per SPEC.md §6.5 the receiver of any CTX_NONE DATA packet on the link MUST emit a PROOF receipt back; this is the mandatory `Packet.prove_packet` step on the receiving side, and is what resolves the sender's `PacketReceipt`.
---
## Source map for this flow
| Step | File | Function / line |
|---|---|---|
| 1 | `LXMF/LXMessage.py` | `pack`, line 352 |
| 2 | `LXMF/LXMRouter.py` | `process_outbound` DIRECT branch, line 2599 |
| 3 | `RNS/Link.py` | `Link.__init__` initiator branch, line 308 |
| 3 | `RNS/Packet.py` | `pack` LINKREQUEST not-encrypted, line 192 |
| 3 | `RNS/Link.py` | `set_link_id` / `link_id_from_lr_packet`, line 340 |
| 3 | `RNS/Packet.py` | `get_hashable_part`, line 354 |
| 4 | `RNS/Link.py` | `validate_proof`, line 396 |
| 4 | `RNS/Link.py` | `handshake`, line 353 |
| 5 | `LXMF/LXMessage.py` | `send` DIRECT branch, line 471 |
| 5 | `LXMF/LXMessage.py` | `__as_packet` DIRECT, line 632 |
| 5 | `RNS/Packet.py` | Link-destination encrypt path |
| 5 | `RNS/Cryptography/Token.py` | Token encrypt (no eph_pub prefix for Link) |
| 6 | `RNS/Transport.py` | `outbound`, line 1031 |
| 7 | `RNS/Packet.py` | `prove` |
| 8 | `RNS/Resource.py` | RESOURCE machinery (SPEC.md §10) |
Add flows/ docs: receive-opportunistic and send-link receive-opportunistic-lxmf.md mirrors the send flow on the recipient side: KISS/HDLC deframe -> Transport.inbound -> packet_filter dedup -> DATA/SINGLE branch -> Destination.receive -> Identity.decrypt with the ratchet ring + long-term-key fallback -> LXMRouter.delivery_packet (which fires the PROOF receipt before parsing) -> LXMessage.unpack_from_bytes with msgpack stamp-strip -> ticket/stamp/dedup checks -> __delivery_callback to the app. Notes upstream's narrower variant tolerance vs SPEC.md §5.6 and the missing clockless-sender fix-up vs §9.6. send-link-lxmf.md walks the DIRECT method end-to-end: process_outbound DIRECT branch decides reuse-vs-establish, RNS.Link.__init__ builds the unencrypted LINKREQUEST body (initiator_X25519_pub || initiator_Ed25519_pub || optional signalling), link_id derived from get_hashable_part, LRPROOF arrives back and validate_proof verifies signature against the responder's long-term Ed25519 pub recalled from a prior announce, handshake() does ECDH+HKDF over the shared secret with salt=link_id, lxmessage.send sends the full LXMF body (with dest_hash, per §5.2) over the link with Token encryption that omits the eph_pub prefix per §3.1, mandatory PROOF receipts per §6.5 resolve the PacketReceipt. Sketches the RESOURCE representation for oversize bodies and the backchannel-identify trick that makes the link bidirectional. flows/README.md status table updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:24:24 -04:00
| 9 | `LXMF/LXMRouter.py` | backchannel identify, line 2532 |
| 10 | `RNS/Link.py` | watchdog / teardown |