reticiulum-specification/flows/send-propagated-lxmf.md
John Poole 02456e0772 Completed the propagated-LXMF three-tier work unit.
Key corrections:
Transient IDs are full 32-byte hashes, not truncated.
Accepted submissions always include a 32-byte propagation stamp, even at cost zero.
/get returns a plain message list inside the generic Link RESPONSE envelope.
Corrected propagation error constants.
Corrected recipient and sender flow documentation.
Added:
Tier 1 audit
Deterministic vectors
Vector regenerator
Runtime verifier
Verification:
Vector regeneration is byte-identical across runs.
Full pinned suite: 19 passed, 0 failed.
git diff --check passes.
No commit was created. The next logical work unit is propagation-node announce and peer-to-peer /offer synchronization.
2026-06-08 14:18:42 -07:00

92 lines
6.4 KiB
Markdown

# Flow: send a PROPAGATED LXMF message via a propagation node
What happens when an LXMF client submits a message to a propagation node for store-and-forward delivery — the path used when the recipient is offline, intermittent, or simply somewhere the sender can't reach directly. Pinned against **RNS 1.2.4 / LXMF 0.9.7**; cross-references [`../SPEC.md`](../SPEC.md) §5.8 (propagation protocol), §6 (Link), §10 (Resource), §11 (REQUEST/RESPONSE).
---
## Preconditions
- Sender has discovered at least one propagation node via its `lxmf.propagation` announce (§4.4 / §5.8.5). `LXMRouter.set_outbound_propagation_node(propagation_node_dest_hash)` records which one to use.
- Sender has a path to the propagation node in `Transport.path_table`.
---
## Sequence
### 1. App constructs an LXMessage with `desired_method = PROPAGATED`
Same as `send-opportunistic-lxmf.md` step 1 except the desired_method differs. The router's `handle_outbound` (`LXMF/LXMRouter.py:1644+`) will eventually route this to the propagation pipeline.
### 2. `LXMessage.pack()` — propagation-specific encryption
`LXMF/LXMessage.py:423-441`. Differences from the opportunistic and direct paths:
- The body is encrypted **to the recipient's public key** (Token form per §3.1 with eph_pub prefix), the same as opportunistic — the propagation node never decrypts.
- The encrypted bytes form `pn_encrypted_data`; the wire body delivered to the propagation node is `dest_hash || pn_encrypted_data` (the recipient's destination_hash is preserved so the propagation node can route to the right recipient on retrieval).
- `transient_id = SHA256(lxmf_data)` (full hash of the encrypted body) — the propagation node's storage key.
- After propagation-stamp generation in step 5, the accepted submission form is wrapped as `propagation_packed = msgpack.packb([time.time(), [lxmf_data || propagation_stamp]])`.
`representation` is set to `PACKET` if `propagation_packed` fits in `link.mdu`, else `RESOURCE`.
### 3. `LXMRouter.process_outbound` for PROPAGATED method
`LXMF/LXMRouter.py:2544+` (the `PROPAGATED` branch is structurally similar to the `DIRECT` branch in `send-link-lxmf.md`). High-level state:
- If a Link to the propagation node already exists and is `ACTIVE`: reuse it.
- Else if the path is known: open a fresh `RNS.Link(propagation_node_destination)` with `LXMRouter.process_outbound` registered as the `link_established_callback` so the LXM is sent as soon as the link establishes.
- Else: `Transport.request_path(propagation_node_dest_hash)` and defer for `LXMRouter.PATH_REQUEST_WAIT`.
### 4. Link establishes (per `send-link-lxmf.md` steps 3-4)
LINKREQUEST → LRPROOF → ACTIVE. The propagation node's `delivery_link_established` analogue for the propagation destination wires `LXMRouter.propagation_packet` as the link's packet callback.
### 5. Stamp generation for propagation cost
The sender computes a 32-byte propagation stamp at the propagation node's
announced `stamp_cost` (element [5][0] per §5.8.5) via
`LXMessage.get_propagation_stamp(target_cost)` (`LXMessage.py:326-350`).
The algorithm is the same as §5.7 stamps but uses
`WORKBLOCK_EXPAND_ROUNDS_PN = 1000` rounds and computes over `transient_id`
rather than `message_id`. A zero-cost node still requires the 32-byte stamp;
the cost only changes its required proof-of-work value.
The stamp is appended to the wire body: `lxmf_data += propagation_stamp` (the propagation node validates the stamp before storing).
### 6. Submit via Link DATA (PACKET representation)
If `representation == PACKET`, the sender emits a Link DATA packet (`context = NONE`) carrying `propagation_packed`. The propagation node's `propagation_packet` callback (`LXMRouter.py:2080+`) decodes the msgpack outer, extracts the stamped entries, validates and strips each required propagation stamp, and stores each unstamped body in `propagation_entries[transient_id]`.
The Link DATA packet gets the standard mandatory PROOF receipt per §6.5; the receipt resolves the sender's `PacketReceipt` and `LXMessage.state` advances to `SENT`. **The state goes to `SENT`, not `DELIVERED`** — propagated messages are "delivered to the propagation node" but not yet "delivered to the recipient". The recipient pulls them later via `/get` (§5.8.3).
### 7. Submit via Resource (RESOURCE representation)
If `representation == RESOURCE`, the sender emits the `propagation_packed` blob as a Resource transfer per `flows/send-resource.md`. The propagation node accepts via `delivery_resource_advertised`, and on completion runs `propagation_resource_concluded` (`LXMRouter.py:2194+`) which decodes the `[time, [lxmf_data || propagation_stamp, ...]]` outer, validates and strips each stamp, and stores each contained LXMF body.
### 8. The link can stay open or be torn down
After a successful propagation submission, the sender either tears down the link (`link.teardown()` per §6.7) or keeps it for another submission. The propagation node doesn't care — it has the messages and will offer them to peers via §5.8.2 sync independently.
### 9. Eventual delivery — recipient pulls via `/get`
When the recipient comes online (or just periodically), they open a Link to the propagation node, run `link.identify(my_identity)` so the propagation node knows whose mail to deliver, then issue a `/get` REQUEST per §5.8.3. The propagation node returns a plain list of stored recipient-encrypted LXMF bodies inside the generic Link RESPONSE envelope. The recipient passes each through `lxmf_propagation()`, which decrypts and reconstructs the canonical LXMF body before handing it to the same `lxmf_delivery` path used by other delivery methods.
This step is detailed in the recipient's flow
(`receive-propagated-lxmf.md`), but is noted here so the full lifecycle is
visible.
---
## Source map
| Step | File | Function / line |
|---|---|---|
| 1 | (app code) | `LXMessage(..., desired_method=PROPAGATED)` |
| 2 | `LXMF/LXMessage.py` | `pack` PROPAGATED branch, line 423-441 |
| 3 | `LXMF/LXMRouter.py` | `process_outbound` PROPAGATED branch, line 2547+ |
| 4 | (see `send-link-lxmf.md` steps 3-4) | |
| 5 | `LXMF/LXMessage.py` | `get_propagation_stamp`, line 326-350 |
| 5 | `LXMF/LXStamper.py` | `WORKBLOCK_EXPAND_ROUNDS_PN = 1000` |
| 6 | `LXMF/LXMRouter.py` | `propagation_packet`, line 2080+ |
| 7 | `LXMF/LXMRouter.py` | `propagation_resource_concluded`, line 2194 |
| 9 | `LXMF/LXMRouter.py` | `request_messages_from_propagation_node`, line 485 |
| 9 | `LXMF/LXMPeer.py` | client-side `/get` flow |