- flows/receive-resource.md: inverse of send-resource. ADV
ingestion, accept/reject decision, request_next loop,
receive_part insertion, assemble + decrypt + hash-validate,
RESOURCE_PRF emission, multi-segment continuation.
- flows/receive-link-lxmf.md: responder side of the link
handshake plus inbound LXMF DATA handling. validate_request
-> handshake -> prove (LRPROOF emission) -> link_established
callback wires delivery_packet. PACKET-form inbound runs
delivery_packet directly; RESOURCE-form inbound runs through
delivery_resource_advertised + delivery_resource_concluded
pipeline.
- flows/send-announce.md: random_hash construction (5B random +
5B BE-uint40 timestamp), optional ratchet rotation, signed_data
assembly, sign + pack, the broadcast emission. Notes that
ANNOUNCE packets are NOT encrypted (Packet.pack special-cases
line 189-191) and the periodic re-announce loop drives 5-15min
cadence.
- flows/forward-announce.md: relay-side rebroadcast for
transport-mode nodes. Eligibility checks (transport_enabled,
not PATH_RESPONSE, not rate_blocked), announce_table queue,
Transport.jobs drain with PATH_REQUEST_GRACE = 0.4s,
per-interface announce_queue with ANNOUNCE_CAP = 2.0% airtime
enforcement, lowest-hop-count-first emission order, hops byte
increment, local-rebroadcast counter for loop break.
- flows/send-propagated-lxmf.md: PROPAGATED method end to end.
LXMessage.pack with body encrypted to recipient (propagation
node never decrypts), Link establishment to the propagation
node, optional propagation stamp (1000 PoW rounds vs 3000 for
regular stamps), submission via Link DATA or Resource,
state goes to SENT (not DELIVERED — recipient pulls via /get
later per §5.8.3).
flows/README.md status table updated; receive-propagated-lxmf.md
added as the only remaining ⏳ flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.4 KiB
Flow: receive an LXMF message over a Reticulum Link
The inverse of send-link-lxmf.md, covering both halves of the responder side: accepting the inbound LINKREQUEST, sending the LRPROOF, then handling LXMF DATA on the established link. Pinned against RNS 1.2.0; cross-references ../SPEC.md §6 (Link), §6.5 (PROOF), §6.6 (signalling), §6.7 (KEEPALIVE/teardown), §10 (Resource).
Sequence
1. LINKREQUEST arrives
A Reticulum DATA packet with packet_type = LINKREQUEST (2), addressed to the responder's lxmf.delivery dest_hash. Body (§6.1):
initiator_X25519_pub(32) || initiator_Ed25519_pub(32) || [signalling(3)]
Transport.inbound (RNS/Transport.py:2027-2057) recognizes packet_type == LINKREQUEST + destination_type == SINGLE, looks up the destination in destinations_map, and calls Destination.receive(packet) which routes to Destination.incoming_link_request(data, packet) per RNS/Destination.py:403-406:
def receive(self, packet):
if packet.packet_type == LINKREQUEST:
self.incoming_link_request(packet.data, packet)
2. Responder builds Link state via Link.validate_request
RNS/Link.py:186-227. Length-checks the body (ECPUBSIZE or ECPUBSIZE + LINK_MTU_SIZE), rejects otherwise. On success:
- Build a
Linkobject withpeer_pub_bytes = data[:32]andpeer_sig_pub_bytes = data[32:64]. set_link_id(packet)per §6.3 — the link_id derives fromPacket.get_hashable_part, invariant under HEADER_1↔HEADER_2 conversion.- If signalling present, parse MTU and mode per §6.6.
- Call
link.handshake()— ECDH with the initiator's X25519 ephemeral pub, HKDF over the shared secret withsalt=link_id, derivessigning_key || encrypt_key. StatusPENDING → HANDSHAKE. - Call
link.prove()to emit the LRPROOF. - Register the link in
Transport.active_linksand append to the destination'slinkslist.
3. Responder emits LRPROOF
RNS/Link.py:371-381. Body per §6.2:
proof_data = signature(64) || responder_X25519_pub(32) || [signalling(3)]
where signature = sign(link_id || responder_X25519_pub || responder_long_term_Ed25519_pub || [signalling]). Wire packet: packet_type = PROOF (3), context = LRPROOF (0xFF), dest_hash field carries the link_id.
4. RTT measurement (LRRTT round trip)
After the initiator validates the LRPROOF and sends Link.LRRTT (0xFE) carrying its measured RTT, the responder receives it at Link.receive line 1056-1059 and calls rtt_packet. The responder's RTT cache updates and Link.STATUS = ACTIVE triggers the link_established_callback registered via Destination.set_link_established_callback.
For LXMF, that callback is LXMRouter.delivery_link_established (LXMF/LXMRouter.py:1849-1856):
link.track_phy_stats(True)
link.set_packet_callback(self.delivery_packet) # ← inbound LXMF flows here
link.set_resource_strategy(RNS.Link.ACCEPT_APP)
link.set_resource_callback(self.delivery_resource_advertised)
link.set_resource_started_callback(self.resource_transfer_began)
link.set_resource_concluded_callback(self.delivery_resource_concluded)
link.set_remote_identified_callback(self.delivery_remote_identified)
This is what makes inbound DATA on this link route into LXMF processing.
5. Inbound LXMF DATA — single-packet (PACKET representation)
A regular DATA packet on the link (context = NONE, Token-encrypted with link session key per §3.1). Link.receive decrypts and passes the plaintext to delivery_packet(data, packet) (LXMF/LXMRouter.py:1819-1847) — the same handler used by opportunistic delivery. Differences:
packet.destination_type == LINKsomethod = DIRECT.- The LXMF body arrives with the recipient's dest_hash (§5.2), so no re-prepend like the opportunistic path does at step 9 of
receive-opportunistic-lxmf.md.
The handler calls packet.prove() immediately (mandatory PROOF receipt per §6.5), then dispatches the body to LXMessage.unpack_from_bytes and lxmf_delivery exactly like the opportunistic flow's steps 10-12.
6. Inbound LXMF DATA — Resource representation
A larger LXMF body arrives as a Resource transfer per flows/receive-resource.md. The Link's resource_strategy = ACCEPT_APP triggers delivery_resource_advertised(resource) (LXMF/LXMRouter.py:1864-1871):
def delivery_resource_advertised(self, resource):
size = resource.get_data_size()
if self.delivery_per_transfer_limit and size > self.delivery_per_transfer_limit*1000:
return False # reject — over limit
return True # accept
If accepted, Resource.accept runs and the receiver state machine in flows/receive-resource.md takes over. On completion, delivery_resource_concluded(resource) fires, reads the assembled file, and feeds it through lxmf_delivery exactly like the single-packet path.
7. KEEPALIVE / teardown
Standard §6.7 protocol. The responder reflects every 0xFF ping with a 0xFE pong, emits its own KEEPALIVE-driven STALE→CLOSED transition if last_inbound + 2*keepalive elapses, and accepts inbound LINKCLOSE packets (validating that decrypt(body) == link_id).
8. Backchannel-identify (post-first-delivery)
After a successful inbound LXMF delivery, the LXMRouter on the initiator side may emit a LINKIDENTIFY (context = 0xFB) proof so the responder can record the initiator's long-term identity for backchannel use (flows/send-link-lxmf.md step 9). On the responder side, this triggers delivery_remote_identified which lets the responder send LXMF replies back over the same link without opening its own.
Source map
| Step | File | Function / line |
|---|---|---|
| 1 | RNS/Transport.py |
LINKREQUEST dispatch, line 2027 |
| 1 | RNS/Destination.py |
receive LINKREQUEST branch, line 403 |
| 2 | RNS/Link.py |
validate_request, line 186 |
| 2 | RNS/Link.py |
handshake, line 353 |
| 3 | RNS/Link.py |
prove (LRPROOF emission), line 371 |
| 4 | RNS/Link.py |
rtt_packet, line 534 |
| 4 | LXMF/LXMRouter.py |
delivery_link_established, line 1849 |
| 5 | LXMF/LXMRouter.py |
delivery_packet, line 1819 |
| 6 | LXMF/LXMRouter.py |
delivery_resource_advertised / _concluded, lines 1864-1900 |
| 7 | RNS/Link.py |
KEEPALIVE handling, line 1149 |
| 8 | LXMF/LXMRouter.py |
delivery_remote_identified, line 1849+ |