# Flow: propagation-node discovery and peer synchronization How two LXMF propagation nodes discover each other, authenticate a sync offer, and transfer stored messages. Pinned against **RNS 1.2.4 / LXMF 0.9.7**; cross-references [`../SPEC.md`](../SPEC.md) §5.8, §10, and §11. ## Sequence ### 1. Node announces `lxmf.propagation` The producer emits the seven-element §5.8.5 app_data array containing its timebase, enabled state, transfer/sync limits, stamp costs, peering cost, and metadata. ### 2. Receiving node evaluates autopeering `LXMFPropagationAnnounceHandler` validates app_data. A direct announce creates or updates an autopeer only when the node is enabled, autopeering is enabled, and the path is within `autopeer_maxdepth` (default 4). Ordinary path responses do not create autopeers. An enabled=false direct announce removes an autopeer. ### 3. Initiator prepares a directional peering key The offering node computes a proof-of-work key over: ```text receiving_identity_hash || offering_identity_hash ``` using the receiving node's announced peering cost. The key is cached and can be reused until the peer raises its cost. ### 4. Initiator selects messages and opens a Link The peer state machine sorts unhandled entries by ascending weight: `priority_weight * age_weight * stored_size`. It applies the peer's per-message and per-sync limits, opens a Link to `lxmf.propagation`, and identifies with its propagation-node identity. LXMF 0.9.7 has a sender-side low-stamp prefilter defect: it uses `min(0, required_cost - flexibility)`. Receivers use the correct `max(0, ...)` threshold, so low-value messages can be offered and then rejected. ### 5. Initiator sends `/offer` The generic §11 request data is: ```python [peering_key, [transient_id_1, transient_id_2, ...]] ``` Each transient ID is the full 32-byte hash from §5.8. The receiver validates the directional key and marks the Link as validated. ### 6. Receiver selects wanted IDs The `/offer` response value is: - `False`: receiver already has all offered messages. - `True`: receiver wants all offered messages. - `[wanted_id, ...]`: receiver wants only that subset. ### 7. Initiator transfers requested entries Requested entries are read from the message store with their 32-byte propagation stamps and sent as a Resource: ```python msgpack.packb([time.time(), [stamped_entry_1, stamped_entry_2, ...]]) ``` ### 8. Receiver admits and stores the Resource Multiple messages are accepted only when the Resource Link was validated by a successful `/offer`. An unvalidated Link may submit one message, which supports ordinary client submission. The receiver validates each propagation stamp, stores valid opaque bodies, and tears down/throttles on invalid stamps. ### 9. Initiator marks sync state After a successful Resource transfer, the initiator moves transferred IDs from unhandled to handled state, updates counters, tears down the Link, and may continue immediately under the persistent strategy. ## Source map | Step | File | Function / line | |---|---|---| | 1 | `LXMF/LXMRouter.py` | `get_propagation_node_app_data`, line 306+ | | 2 | `LXMF/Handlers.py` | `LXMFPropagationAnnounceHandler`, line 35+ | | 3-5 | `LXMF/LXMPeer.py` | `generate_peering_key` / `sync`, line 242+ | | 5-6 | `LXMF/LXMRouter.py` | `offer_request`, line 2145+ | | 7, 9 | `LXMF/LXMPeer.py` | `offer_response` / `resource_concluded`, line 395+ | | 8 | `LXMF/LXMRouter.py` | `propagation_resource_concluded`, line 2200+ |