Completed the propagation-node announce and peer-sync three-tier unit.

Added:
Tier 1 audit
Peer-sync flow
Deterministic vectors
Regenerator
Verifier
Corrected §5.8 regarding:
Directional peering-key identity ordering.
Public versus control destination handlers.
Permissive announce parser behavior.
Autopeer rules.
Peer Resource framing and admission.
PN_STAMP_THROTTLE = 180 seconds.
Two documented LXMF 0.9.7 hazards.
Verification: deterministic regeneration passed; full pinned suite passed 20/20; git diff --check passed. No commit created.
This commit is contained in:
John Poole 2026-06-08 17:32:55 -07:00
commit 5aa3920b76
12 changed files with 776 additions and 12 deletions

View file

@ -58,7 +58,7 @@ As content grows, `SPEC.md` will be split into per-layer files (packet header, i
Errata that may invalidate code built against an earlier revision of `SPEC.md`. Newest first. Feature additions and ordinary edits live in `git log` — this section is reserved for cases where the spec said one thing, that turned out to be wrong, and an implementer who pulled the bad version needs to fix their code.
- **2026-06-08 — §5.8 propagated-LXMF transient IDs, `/get` framing, and error constants.**
Earlier §5.8 text described transient IDs as 16-byte truncated hashes; upstream LXMF 0.9.7 uses the full 32-byte `SHA256(lxmf_data)`. It also incorrectly described `/get` responses as propagation bundles shaped `[time, [messages]]`; the `/get` handler actually returns a plain message list carried by the generic `[request_id, response]` Link RESPONSE. Accepted submission and peer-transfer entries always append a required 32-byte propagation stamp, including at cost zero. Finally, `ERROR_THROTTLED` is `0xf6`, `ERROR_NOT_FOUND` is `0xfd`, and `0xf5` is `ERROR_INVALID_STAMP`. Implementations using the earlier text will offer/request the wrong IDs, misparse retrieval responses, omit required stamps, or mishandle errors. Corrected and runtime-locked by `tools/verify_propagated_lxmf.py`.
Earlier §5.8 text described transient IDs as 16-byte truncated hashes; upstream LXMF 0.9.7 uses the full 32-byte `SHA256(lxmf_data)`. It also incorrectly described `/get` responses as propagation bundles shaped `[time, [messages]]`; the `/get` handler actually returns a plain message list carried by the generic `[request_id, response]` Link RESPONSE. Accepted submission and peer-transfer entries always append a required 32-byte propagation stamp, including at cost zero. The section also incorrectly placed operator handlers on the public propagation destination, described the announce parser as exact/strict, and stated a 30-minute peer throttle; operator handlers use `lxmf.propagation.control`, the parser is deliberately permissive, and `PN_STAMP_THROTTLE` is 180 seconds. Finally, `ERROR_THROTTLED` is `0xf6`, `ERROR_NOT_FOUND` is `0xfd`, and `0xf5` is `ERROR_INVALID_STAMP`. Corrected and runtime-locked by `tools/verify_propagated_lxmf.py` and `tools/verify_propagation_peer.py`.
- **2026-06-08 — §11.4 REQUEST authorization constants were reversed.**
Earlier §11.4 text assigned `ALLOW_LIST = 0x01` and `ALLOW_ALL = 0x02`. Upstream RNS 1.2.4 defines `ALLOW_NONE = 0x00`, `ALLOW_ALL = 0x01`, and `ALLOW_LIST = 0x02` in `RNS/Destination.py`. An implementation following the prior table would expose list-restricted handlers publicly and incorrectly restrict public handlers. §11.4 is corrected and runtime-locked by `tools/verify_request_response.py`. The same audit corrected §11.2 file metadata wording: Resource advertisement field `m` is always the hashmap; file metadata is carried inside Resource plaintext and signaled by flag `x`.

71
SPEC.md
View file

@ -890,16 +890,21 @@ self.propagation_destination = RNS.Destination(
Per §1.2, the well-known `name_hash` is `e03a09b77ac21b22258e` (`SHA256("lxmf.propagation")[:10]`). The propagation node's identity is its own — different propagation nodes have different identity hashes and therefore different destination hashes. Receivers of `lxmf.propagation` announces filter by name_hash to surface "propagation node available" UI separately from "messageable peer available" UI per §4.4.
The destination registers four request handlers via `register_request_handler` (`LXMRouter.py:651-655`):
The public propagation destination registers two `ALLOW_ALL` request handlers
(`LXMRouter.py:650-651`):
| Path | Constant | Allow | Purpose |
|---|---|---|---|
| `/offer` | `LXMPeer.OFFER_REQUEST_PATH` | `ALLOW_ALL` | Peer-to-peer message-set offer (§5.8.2) |
| `/get` | `LXMPeer.MESSAGE_GET_PATH` | `ALLOW_ALL` | Client message retrieval (§5.8.3) |
| `/stats` | `LXMRouter.STATS_GET_PATH` | implementation-defined | Operator stats query |
| `/sync` | `LXMRouter.SYNC_REQUEST_PATH` | `ALLOW_LIST` | Operator-triggered sync push |
All four are reached over an active Reticulum Link via the §11 REQUEST/RESPONSE protocol. The link must be `identify()`-d before `/offer` and `/get` requests are honored — that's how the propagation node knows which client / peer is making the request.
Operator handlers are on a distinct `lxmf.propagation.control` SINGLE
destination and use `ALLOW_LIST`: `/pn/get/stats`, `/pn/peer/sync`, and
`/pn/peer/unpeer` (`LXMRouter.py:653-655`). Do not expose these paths on the
public propagation destination.
All handlers are reached over an active Reticulum Link via §11. The Link must
be `identify()`-d before `/offer` and `/get` are honored.
#### 5.8.2 Peer-to-peer sync via `/offer`
@ -914,6 +919,11 @@ Where:
- `peering_key` is a 32-byte proof-of-work key per §5.8.4.
- `transient_id_N` is the full 32-byte hash of an LXM (`= SHA256(lxmf_data)`) that the offering node has and thinks the receiving node might want.
Before offering, the initiator sorts unhandled messages by ascending
`priority_weight * age_weight * stored_size`, drops messages exceeding the
peer's per-message limit, and keeps the batch strictly below the peer's
per-sync limit. It then opens and identifies a Link to the peer.
The receiving node validates the peering_key, then for each `transient_id`:
- If it already has the message in `propagation_entries`: skip.
@ -943,6 +953,18 @@ computes the full transient ID over the unstamped `lxmf_data`, and writes the
opaque body plus stamp to its propagation store. Propagation nodes never
decrypt the recipient payload.
A successful `/offer` marks that Link ID as peering-key validated. A Resource
on an unvalidated Link may contain one message, supporting ordinary client
submission; a multi-message Resource requires a validated Link and is
otherwise ignored with Link teardown. The exact announce, `/offer`, and
peer-Resource forms are verified by `tools/verify_propagation_peer.py` and
`test-vectors/propagation-peer.json`.
> **LXMF 0.9.7 hazard:** malformed `/offer` validation uses `and` instead of
> `or`. A one-element list raises internally and returns `None`, not
> `ERROR_INVALID_DATA`. Senders must use the specified shape and should treat a
> missing/`None` response as failure.
Error responses (`LXMPeer.py:14-50`):
| Constant | Hex | Meaning |
@ -952,7 +974,7 @@ Error responses (`LXMPeer.py:14-50`):
| `ERROR_INVALID_KEY` | `0xf3` | Peering key failed proof-of-work validation. |
| `ERROR_INVALID_DATA` | `0xf4` | Offer payload didn't match the expected `[key, [ids]]` shape. |
| `ERROR_INVALID_STAMP` | `0xf5` | A submitted or peer-transferred message has an invalid propagation stamp. |
| `ERROR_THROTTLED` | `0xf6` | Peer is rate-limiting; postpone for `PN_STAMP_THROTTLE` (default 30 minutes). |
| `ERROR_THROTTLED` | `0xf6` | Peer is rate-limiting; postpone for `PN_STAMP_THROTTLE` (default 180 seconds). |
| `ERROR_NOT_FOUND` | `0xfd` | Used by `/sync` and stats-query paths. |
| `ERROR_TIMEOUT` | `0xfe` | Peer operation timed out. |
@ -989,14 +1011,20 @@ path. These framing and transient-ID rules are verified by
Two propagation nodes that want to peer must each compute a peering key for the relationship (`LXStamper.py::validate_peering_key` and `stamp_workblock` with `WORKBLOCK_EXPAND_ROUNDS_PEERING = 25`):
```python
peering_id = self.identity.hash + remote_identity.hash # 32 bytes (16 + 16)
peering_id = receiving_identity.hash + offering_identity.hash # 32 bytes
workblock = stamp_workblock(peering_id, expand_rounds=25)
peering_key = (find any 32B value such that
SHA256(workblock || peering_key)
has at least target_cost leading zero bits)
```
`target_cost` is the receiving node's `peering_cost` (announced in element [5][2] of the propagation announce app_data, see §5.8.5). With only 25 rounds of HKDF expansion (vs 3000 for regular message stamps in §5.7), the workblock is ~6 KiB and peering keys can be computed in milliseconds. Peering keys are amortized: computed once between two propagation nodes and reused for every subsequent `/offer` for the lifetime of the peering.
`target_cost` is the receiving node's `peering_cost` (announced in element
[5][2], see §5.8.5). Direction is normative: the offering node generates over
`remote || local`, while the receiver validates `local || remote`; both are
therefore `receiving || offering`. Reversing the identities produces a
different workblock. With 25 HKDF expansion rounds, the workblock is 6400
bytes. Peering keys are cached as `[key, value]`, reused across offers, and
regenerated when the peer raises its required cost.
#### 5.8.5 Propagation node announce app_data
@ -1024,7 +1052,27 @@ Element [5] sub-fields:
| `[5][1]` | `stamp_cost_flexibility` | Tolerance — client stamps within this many bits below `stamp_cost` are still accepted |
| `[5][2]` | `peering_cost` | PoW cost for peering keys per §5.8.4 |
Receivers parse this via `pn_announce_data_is_valid` (`LXMF/LXMF.py:191-206`), which insists on exactly 7 elements with type-correct positions. **A client that misparses element [5] as a single integer (rather than a 3-element list) silently fails to compute the right peering key or submission stamp and is rejected** — this is the most common interop break in custom propagation-node implementations.
`node_state` is true only when propagation is enabled and the node is not
configured `from_static_only`. A direct enabled announce can create/update an
autopeer when it is within `autopeer_maxdepth` (default 4); path responses do
not create ordinary autopeers, and a direct disabled announce removes one.
Receivers parse this via `pn_announce_data_is_valid`. The LXMF 0.9.7 parser
requires **at least** seven elements, ignores element [0] and trailing
elements, accepts numeric fields that `int()` can convert, and accepts
`True`/`False` plus integer `1`/`0` for node state. Element [5] must still be a
list with at least three convertible values, and [6] must be a dict. Producers
should emit the canonical strict seven-element form above; receivers should
match the permissive acceptance boundary for interoperability.
Peer defaults in LXMF 0.9.7 are `AUTOPEER=True`, `AUTOPEER_MAXDEPTH=4`,
`MAX_PEERS=20`, `PEERING_COST=18`, `MAX_PEERING_COST=26`, and
`PN_STAMP_THROTTLE=180` seconds.
> **LXMF 0.9.7 hazard:** the outbound peer-sync low-stamp prefilter uses
> `min(0, required_cost - flexibility)`, while the receiver correctly uses
> `max(0, ...)`. This can cause low-value messages to be offered and then
> rejected/throttled. Clean-room senders should prefilter with `max(0, ...)`.
#### 5.8.6 Source map
@ -1034,15 +1082,18 @@ Receivers parse this via `pn_announce_data_is_valid` (`LXMF/LXMF.py:191-206`), w
| `LXMF/LXMRouter.py:173` | propagation_destination construction |
| `LXMF/LXMRouter.py:306-322` | propagation announce app_data shape |
| `LXMF/LXMRouter.py:650-651` | `/offer` and `/get` handler registration |
| `LXMF/LXMRouter.py:653-655` | control-destination handler registration |
| `LXMF/LXMRouter.py:1426-1500` | `message_get_request` handler (client `/get`) |
| `LXMF/LXMRouter.py:1506-1589` | recipient-side `/get` response processing and acknowledgement |
| `LXMF/LXMRouter.py:2110-2142` | incoming packet submission bundle and stamp validation |
| `LXMF/LXMRouter.py:2145-2200` | `offer_request` handler (peer `/offer`) |
| `LXMF/LXMRouter.py:2200-2290` | peer Resource admission and stamp validation |
| `LXMF/LXMRouter.py:2315-2368` | `lxmf_propagation` transient-ID, delivery, and storage path |
| `LXMF/LXMPeer.py:14-50` | path constants and error-response constants |
| `LXMF/LXMPeer.py:370-486` | initiator-side `/offer` flow |
| `LXMF/LXMPeer.py:242-486` | peering-key generation, offer selection, and transfer |
| `LXMF/LXStamper.py::validate_peering_key` | peering-key PoW validation |
| `LXMF/LXMF.py:191-206` | `pn_announce_data_is_valid` parser |
| `LXMF/LXMF.py:224-251` | `pn_announce_data_is_valid` parser |
| `LXMF/Handlers.py:35-101` | propagation announce and autopeer handling |
### 5.9 LXMF field constants and helper specifiers

View file

@ -0,0 +1,110 @@
# Tier 1 Audit: Propagation-Node Announce and Peer Sync
Question: Does `SPEC.md` §5.8 accurately describe propagation-node discovery,
peering-key authentication, `/offer`, and peer Resource synchronization in
upstream RNS 1.2.4 / LXMF 0.9.7?
Evidence baseline:
- RNS package: `rns==1.2.4`
- LXMF package: `lxmf==0.9.7`
- Sources: `LXMF/Handlers.py`, `LXMF/LXMF.py`, `LXMF/LXMRouter.py`,
`LXMF/LXMPeer.py`, and `LXMF/LXStamper.py`
- Audit date: 2026-06-08
Tier 2 evidence lives in `tools/verify_propagation_peer.py` and
`test-vectors/propagation-peer.json`.
## Confirmed Model
1. A propagation announce producer emits seven MessagePack elements:
`[False, timebase, enabled, transfer_limit_kb, sync_limit_kb,
[stamp_cost, flexibility, peering_cost], metadata]`. `enabled` is true
only when propagation is enabled and `from_static_only` is false.
2. The LXMF 0.9.7 parser is more permissive than the producer:
- It requires at least seven elements, not exactly seven.
- It ignores element 0 and any elements after index 6.
- Numeric fields only need to be accepted by `int()`.
- The enabled field comparison accepts booleans and integer `0`/`1`.
- Element 5 must be a list with at least three int-convertible elements.
- Element 6 must be a dict.
3. A propagation node autopeers from a valid direct announce only when
`autopeer` is enabled, the announced node is enabled, and its path depth is
within `autopeer_maxdepth`. Path responses do not create ordinary
autopeers. An enabled=false direct announce unpeers an existing autopeer.
Static peers follow separate rules.
4. Public `/offer` and `/get` handlers are registered on
`lxmf.propagation`. Operator `/pn/get/stats`, `/pn/peer/sync`, and
`/pn/peer/unpeer` handlers are registered on the distinct
`lxmf.propagation.control` destination with `ALLOW_LIST`.
5. Peering proof material is directional:
```
peering_id = receiving_identity_hash(16) || offering_identity_hash(16)
```
The initiator generates the same ordering as
`remote_identity.hash || local_identity.hash`. Reversing the order produces
a different workblock and invalid key. Peering keys are cached as
`[key, value]` and regenerated if the peer raises its required cost.
6. `/offer` data is `[peering_key, transient_ids]`. After key validation, the
receiver returns `False` when it has all offered IDs, `True` when it wants
all, or the wanted subset. Successful validation marks the Link ID in
`validated_peer_links`.
7. Requested peer messages are always sent as Resource plaintext:
```
msgpack([time.time(), [lxmf_data || propagation_stamp, ...]])
```
There is no extra nesting around the message list. A receiver validates and
strips each stamp before storage.
8. A Resource arriving on a Link without a validated `/offer` peering key may
contain one message. Multiple messages require the Link ID to be present in
`validated_peer_links`; otherwise the receiver tears down the Link and
ignores the transfer.
9. Default peer constants are `AUTOPEER=True`, `AUTOPEER_MAXDEPTH=4`,
`MAX_PEERS=20`, `PEERING_COST=18`, `MAX_PEERING_COST=26`, and
`PN_STAMP_THROTTLE=180` seconds.
## Upstream 0.9.7 Hazards
1. `offer_request()` checks malformed input with:
```python
if type(data) != list and len(data) < 2:
```
The `and` means a one-element list reaches `data[1]`, raises, and returns
`None` instead of `ERROR_INVALID_DATA`. Implementations should send the
correct shape and tolerate a missing/`None` response from malformed peers.
2. `LXMPeer.sync()` computes its outbound low-stamp prefilter with
`min(0, required_cost - flexibility)`. The receiving node correctly uses
`max(0, ...)`. In normal positive-cost configurations, the sender therefore
offers low-value messages that the receiver later rejects, tears down, and
throttles. Clean-room senders should use `max(0, ...)`.
## Tier 2 Scope
`tools/verify_propagation_peer.py` verifies:
1. Exact deterministic announce app_data and parser acceptance boundary.
2. Autopeer, path-response, and disabled-node announce behavior.
3. Directional peering-key validation.
4. `/offer` all/none/subset, invalid-key, unidentified, and malformed cases.
5. Exact peer-sync Resource plaintext and required stamps.
6. One-message admission versus multi-message rejection on unvalidated Links.
7. Peer defaults and throttle interval.
Offer scheduling order, transfer-size filtering, persistence, and the outbound
low-stamp prefilter defect remain source-cited behavior.

View file

@ -20,6 +20,7 @@ The two views are complementary: SPEC.md tells you what each piece looks like; t
| [`forward-announce.md`](forward-announce.md) (transport-node rebroadcast logic, announce_cap, queue) | ✅ |
| [`send-propagated-lxmf.md`](send-propagated-lxmf.md) (PROPAGATED method, via a propagation node) | ✅ |
| [`receive-propagated-lxmf.md`](receive-propagated-lxmf.md) (recipient pulling messages via `/get`) | ✅ |
| [`propagation-peer-sync.md`](propagation-peer-sync.md) (propagation-node announce, `/offer`, and peer Resource sync) | ✅ |
| [`lxmf-outbound-retry.md`](lxmf-outbound-retry.md) (process_outbound retry loop, per-message state machine, fail_message) | ✅ |
## Conventions

View file

@ -0,0 +1,94 @@
# 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+ |

View file

@ -191,6 +191,13 @@ Spec-only repos with a "the source is the source of truth" attitude die slowly b
Each entry: date, one-line symptom, spec section that governs it, one-line fix, one-sentence lesson. Append-only. New entries go at the top.
### 2026-06-08 — Propagation peers rejected reversed peering keys
- **Symptom:** `/offer` consistently returns `ERROR_INVALID_KEY` even though both nodes use the same identities and target cost.
- **Spec section:** §5.8.4. Peering-key material is directional: `receiving_identity_hash || offering_identity_hash`. The offering node computes `remote || local`; the receiver validates `local || remote`.
- **Fix:** Build peering workblocks in receiver-first order and use the receiving node's announced peering cost. Locked by `tools/verify_propagation_peer.py`.
- **Lesson:** A relationship key built from the same two identities is not necessarily symmetric; verify concatenation order from both producer and validator.
### 2026-06-08 — Propagated-LXMF clients requested truncated IDs and misparsed `/get`
- **Symptom:** A clean-room propagation client can submit messages but retrieves none, or treats a valid `/get` RESPONSE as malformed because it expects a `[time, [messages]]` bundle.

View file

@ -14,8 +14,9 @@ Populated against RNS 1.2.4 / LXMF 0.9.7:
- ✅ `link-lxmf.json` — DIRECT LXMF PACKET and Resource vectors at the exact 319/320 computed-content boundary (regenerator: `../tools/regen_link_lxmf.py`, verifier: `../tools/verify_link_lxmf.py`).
- ✅ `request-response.json` — Link REQUEST/RESPONSE packet and Resource forms with deterministic correlation IDs (regenerator: `../tools/regen_request_response.py`, verifier: `../tools/verify_request_response.py`).
- ✅ `propagated-lxmf.json` — PROPAGATED LXMF submission packet/Resource boundary, full transient IDs, and `/get` framing (regenerator: `../tools/regen_propagated_lxmf.py`, verifier: `../tools/verify_propagated_lxmf.py`).
- ✅ `propagation-peer.json` — propagation-node announce, directional peering key, `/offer`, and peer-sync Resource plaintext (regenerator: `../tools/regen_propagation_peer.py`, verifier: `../tools/verify_propagation_peer.py`).
All eight files are byte-deterministic across runs: regenerators pin every random source (ephemeral keys, IVs, `random_hash` values, timestamps) so the output is reproducible against a fixed upstream RNS / LXMF version.
All nine files are byte-deterministic across runs: regenerators pin every random source (ephemeral keys, IVs, `random_hash` values, timestamps) so the output is reproducible against a fixed upstream RNS / LXMF version.
See [`../agent.md`](../agent.md) §3 and [`../todo.md`](../todo.md) for the evidence model and remaining task list.
@ -31,6 +32,7 @@ Each vector lives in a per-domain JSON file, e.g.:
- `link-lxmf.json` — DIRECT LXMF Link DATA and Resource boundary forms
- `request-response.json` — Link REQUEST/RESPONSE packet and Resource forms
- `propagated-lxmf.json` — PROPAGATED submission and `/get` response forms
- `propagation-peer.json` — propagation-node announce and peer-sync forms
Each entry should include:
@ -59,5 +61,6 @@ For the spec to claim "an implementation that passes all test vectors interopera
7. **Resource transfer** — encrypt once, split into parts, validate ADV/hashmap, assemble, and emit the expected proof.
8. **REQUEST/RESPONSE** — packet and Resource RPC forms, request-ID derivation, and response correlation.
9. **Propagated LXMF** — recipient-encrypted submission, full transient-ID derivation, packet/Resource selection, and `/get` framing.
10. **Propagation peer sync** — node announce, directional peering key, `/offer` selection, and stamped Resource transfer.
A separate vector set for FAILURE cases is also useful: malformed announces, expired ratchets, mismatched signatures. An implementation should reject those as a regression-prevention measure.

View file

@ -0,0 +1,56 @@
{
"_about": "Deterministic propagation-node announce, directional peering-key, /offer, and peer-sync Resource plaintext vectors.",
"inputs": {
"offering_identity_label": "alice",
"receiving_identity_label": "bob",
"announce_time": 1700000120,
"sync_time": 1700000180.0,
"peering_cost": 4
},
"announce": {
"app_data_hex": "97c2ce6553f178c3cd0100cd2800931003128101c409566563746f7220504e",
"decoded": {
"legacy_support": false,
"timebase": 1700000120,
"propagation_enabled": true,
"transfer_limit_kb": 256,
"sync_limit_kb": 10240,
"stamp_costs": [
16,
3,
18
],
"name_utf8": "Vector PN"
}
},
"peering": {
"peering_id_hex": "c090410e5b5bf8956194c1872dccec3b28d43a11abc1094301a59ed3b44f127b",
"peering_key_hex": "0000000000000000000000000000000000000000000000000000000000000005",
"peering_key_value": 5,
"offer_data_hex": "92c420000000000000000000000000000000000000000000000000000000000000000592c420d76e78e5110ddaae1de9fbde393de9347edfbb49baf0b5f62179040f90aa2a9cc420cdd5d40738f0485ccf425c9a2a3ca2e029913790b96d2d0f551d7492b137b017",
"offered_transient_ids_hex": [
"d76e78e5110ddaae1de9fbde393de9347edfbb49baf0b5f62179040f90aa2a9c",
"cdd5d40738f0485ccf425c9a2a3ca2e029913790b96d2d0f551d7492b137b017"
],
"expected_responses": {
"receiver_has_none": true,
"receiver_has_all": false,
"receiver_has_first_hex": [
"cdd5d40738f0485ccf425c9a2a3ca2e029913790b96d2d0f551d7492b137b017"
]
},
"sync_resource_plaintext_hex": "92cb41d954fc6d00000092c501309695d17f22fa6e45d2b0cd3439a7ca7e489cebba7a166c619835fe12eedc581dbfc1b36de10c830e4f0bc1c48734864b2132435465768798a9bacbdcedfe0f10f9e41eaa8cf9c7de09f9fd59cf762ce5237da20d9c4c87b9d6875a0a0c436c02cedda981a2f708ba5ffd36c716980184bf4dd60d2b28f41f062344389f38b9ca59f3693d0348189ee883852be33c9ba43275fc1c2d2b9a18c6a3c32a0b544694d9b2d56e8f3b7fd42415e91cb8bcbe68fa1becb62af8b1802c93db203d66bfb1bdd6baabcaf109b6bfb4f71b175bc73de7ac8a935f8fb51aab9f80fe561a6b9a3d9abd21e3aeda66fdc014933c2d9e37eb5b91643dd4d0b77f39a00833e7d21acc87465513d212fc3adb20f8009903e35555555555555555555555555555555555555555555555555555555555555555c501409695d17f22fa6e45d2b0cd3439a7ca7e489cebba7a166c619835fe12eedc581dbfc1b36de10c830e4f0bc1c48734864b2132435465768798a9bacbdcedfe0f10f9e41eaa8cf9c7de09f9fd59cf762ce53a72d3e943b1c2a18f5f7f7461b188f8f8afa1ad99a5f25ab72e4b5717984a1ef1b53e721fbd6515574d4390b88ec22534f57d981dca42beed7db88b7d339f244710b635246f85903426286c7e1e33768ea7994586861ce8a133c14dd1dfb23fa550675f4b4a88888a1181c07daaba1d2af8d7c63bf8959c29c52d52f9ff91fffa44284c2d6aa0cb14f96e5e9774e6e2515608411394539f77e394f64619f05c2c49456687367940e57c7edebb0282bc4c8bf540ed6f9bd040f08f3e19e444f7a238395eb1116c5cec70d0d7e5c0c0825555555555555555555555555555555555555555555555555555555555555555",
"submitted_entries_hex": [
"9695d17f22fa6e45d2b0cd3439a7ca7e489cebba7a166c619835fe12eedc581dbfc1b36de10c830e4f0bc1c48734864b2132435465768798a9bacbdcedfe0f10f9e41eaa8cf9c7de09f9fd59cf762ce5237da20d9c4c87b9d6875a0a0c436c02cedda981a2f708ba5ffd36c716980184bf4dd60d2b28f41f062344389f38b9ca59f3693d0348189ee883852be33c9ba43275fc1c2d2b9a18c6a3c32a0b544694d9b2d56e8f3b7fd42415e91cb8bcbe68fa1becb62af8b1802c93db203d66bfb1bdd6baabcaf109b6bfb4f71b175bc73de7ac8a935f8fb51aab9f80fe561a6b9a3d9abd21e3aeda66fdc014933c2d9e37eb5b91643dd4d0b77f39a00833e7d21acc87465513d212fc3adb20f8009903e35555555555555555555555555555555555555555555555555555555555555555",
"9695d17f22fa6e45d2b0cd3439a7ca7e489cebba7a166c619835fe12eedc581dbfc1b36de10c830e4f0bc1c48734864b2132435465768798a9bacbdcedfe0f10f9e41eaa8cf9c7de09f9fd59cf762ce53a72d3e943b1c2a18f5f7f7461b188f8f8afa1ad99a5f25ab72e4b5717984a1ef1b53e721fbd6515574d4390b88ec22534f57d981dca42beed7db88b7d339f244710b635246f85903426286c7e1e33768ea7994586861ce8a133c14dd1dfb23fa550675f4b4a88888a1181c07daaba1d2af8d7c63bf8959c29c52d52f9ff91fffa44284c2d6aa0cb14f96e5e9774e6e2515608411394539f77e394f64619f05c2c49456687367940e57c7edebb0282bc4c8bf540ed6f9bd040f08f3e19e444f7a238395eb1116c5cec70d0d7e5c0c0825555555555555555555555555555555555555555555555555555555555555555"
]
},
"rns_version_at_generation": "1.2.4",
"lxmf_version_at_generation": "0.9.7",
"generator_script": "tools/regen_propagation_peer.py",
"verifies_spec_sections": [
"5.8.2",
"5.8.4",
"5.8.5"
]
}

10
todo.md
View file

@ -70,6 +70,16 @@ Outstanding work for the spec repo.
length, `/get` response framing, propagation error constants, and the
recipient flow.
- [x] **Propagation announce and peer-sync three-tier work unit.** Tier 1 is
recorded in
`audits/propagation-peer-tier1-rns-1.2.4-lxmf-0.9.7.md`; Tier 2 adds
`test-vectors/propagation-peer.json`,
`tools/regen_propagation_peer.py`, and
`tools/verify_propagation_peer.py`; Tier 3 corrects announce parsing,
control-destination handlers, peering-key direction, peer Resource
admission, and throttle timing, and adds
`flows/propagation-peer-sync.md`.
## Open `⚠️ UNVERIFIED` items in SPEC.md
These need either a runtime test or a stronger upstream source citation

View file

@ -60,6 +60,7 @@ Populated against RNS 1.2.4 / LXMF 0.9.7:
| `verify_resource.py` | §10.2, §10.4, §10.6-§10.9, §10.11, §10.12 — vectors, whole-stream encryption/slicing, receiver assembly/proof, control behavior, multi-segment size, and negative cases | ✅ |
| `verify_request_response.py` | §11.1-§11.5 — packet/Resource RPC forms, request-ID domains, correlation, authorization constants, receipt states | ✅ |
| `verify_propagated_lxmf.py` | §5.8 — PROPAGATED bundle, full transient ID, packet/Resource boundary, recipient decrypt/signature, `/get` handler and framing, error constants | ✅ |
| `verify_propagation_peer.py` | §5.8.2, §5.8.4, §5.8.5 — propagation announce parser/handler, directional peering key, `/offer`, peer Resource admission, defaults | ✅ |
| `regen_identities.py` | regenerates `test-vectors/identities.json` | ✅ |
| `regen_announces.py` | regenerates `test-vectors/announces.json` (deterministic announce wire bytes, with and without ratchet) | ✅ |
| `regen_lxmf.py` | regenerates `test-vectors/lxmf.json` (deterministic opportunistic-LXMF plaintext + Token ciphertext) | ✅ |
@ -68,5 +69,6 @@ Populated against RNS 1.2.4 / LXMF 0.9.7:
| `regen_resources.py` | regenerates `test-vectors/resources.json` (deterministic Resource ciphertext, parts, ADV, and PRF body) | ✅ |
| `regen_request_response.py` | regenerates `test-vectors/request-response.json` (deterministic packet and Resource RPC forms) | ✅ |
| `regen_propagated_lxmf.py` | regenerates `test-vectors/propagated-lxmf.json` (deterministic PROPAGATED submission and `/get` forms) | ✅ |
| `regen_propagation_peer.py` | regenerates `test-vectors/propagation-peer.json` (deterministic propagation announce, peering key, `/offer`, and peer-sync Resource plaintext) | ✅ |
See [`../agent.md`](../agent.md) §3 and [`../todo.md`](../todo.md) for the evidence model and remaining priority order.

View file

@ -0,0 +1,154 @@
"""
Regenerator for test-vectors/propagation-peer.json.
Builds deterministic propagation-node announce app_data, directional peering
key and /offer payloads, and the stamped peer-sync Resource plaintext.
"""
from __future__ import annotations
import json
import os
import sys
import LXMF
import RNS
from LXMF import LXStamper
from LXMF.LXMRouter import LXMRouter
from RNS.vendor import umsgpack
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT_PATH = os.path.join(REPO_ROOT, "test-vectors", "propagation-peer.json")
IDS_PATH = os.path.join(REPO_ROOT, "test-vectors", "identities.json")
PROPAGATED_PATH = os.path.join(REPO_ROOT, "test-vectors", "propagated-lxmf.json")
FIXED_ANNOUNCE_TIME = 1700000120
FIXED_SYNC_TIME = 1700000180.0
PEERING_COST = 4
def load_json(path: str):
with open(path, "r", encoding="utf-8") as input_file:
return json.load(input_file)
def load_identities():
vectors = load_json(IDS_PATH)["vectors"]
alice = next(vector for vector in vectors if vector["label"] == "alice")
bob = next(vector for vector in vectors if vector["label"] == "bob")
return (
RNS.Identity.from_bytes(bytes.fromhex(alice["inputs"]["private_key_hex"])),
RNS.Identity.from_bytes(bytes.fromhex(bob["inputs"]["private_key_hex"])),
)
def build_announce() -> bytes:
class FakeRouter:
name = "Vector PN"
propagation_node = True
from_static_only = False
propagation_stamp_cost = 16
propagation_stamp_cost_flexibility = 3
peering_cost = 18
propagation_per_transfer_limit = 256
propagation_per_sync_limit = 10240
def get_propagation_node_announce_metadata(self):
return LXMRouter.get_propagation_node_announce_metadata(self)
router_module = sys.modules["LXMF.LXMRouter"]
real_time = router_module.time.time
router_module.time.time = lambda: FIXED_ANNOUNCE_TIME
try:
return LXMRouter.get_propagation_node_app_data(FakeRouter())
finally:
router_module.time.time = real_time
def find_peering_key(peering_id: bytes) -> tuple[bytes, int]:
workblock = LXStamper.stamp_workblock(
peering_id, expand_rounds=LXStamper.WORKBLOCK_EXPAND_ROUNDS_PEERING
)
for candidate_number in range(1_000_000):
candidate = candidate_number.to_bytes(32, "big")
value = LXStamper.stamp_value(workblock, candidate)
if value >= PEERING_COST:
return candidate, value
raise RuntimeError("could not find deterministic peering key")
def main() -> None:
print(f"regen_propagation_peer.py against RNS {RNS.__version__} / LXMF {LXMF.__version__}")
alice_id, bob_id = load_identities()
propagated = load_json(PROPAGATED_PATH)
first = propagated["boundary"]["largest_packet"]
second = propagated["boundary"]["first_resource"]
transient_ids = [
bytes.fromhex(first["transient_id_hex"]),
bytes.fromhex(second["transient_id_hex"]),
]
submitted_entries = [
bytes.fromhex(first["submitted_entry_hex"]),
bytes.fromhex(second["submitted_entry_hex"]),
]
# Receiver validates receiver_identity_hash || offering_identity_hash.
peering_id = bob_id.hash + alice_id.hash
peering_key, peering_value = find_peering_key(peering_id)
announce_data = build_announce()
offer_data = [peering_key, transient_ids]
sync_data = [FIXED_SYNC_TIME, submitted_entries]
payload = {
"_about": (
"Deterministic propagation-node announce, directional peering-key, "
"/offer, and peer-sync Resource plaintext vectors."
),
"inputs": {
"offering_identity_label": "alice",
"receiving_identity_label": "bob",
"announce_time": FIXED_ANNOUNCE_TIME,
"sync_time": FIXED_SYNC_TIME,
"peering_cost": PEERING_COST,
},
"announce": {
"app_data_hex": announce_data.hex(),
"decoded": {
"legacy_support": False,
"timebase": FIXED_ANNOUNCE_TIME,
"propagation_enabled": True,
"transfer_limit_kb": 256,
"sync_limit_kb": 10240,
"stamp_costs": [16, 3, 18],
"name_utf8": "Vector PN",
},
},
"peering": {
"peering_id_hex": peering_id.hex(),
"peering_key_hex": peering_key.hex(),
"peering_key_value": peering_value,
"offer_data_hex": umsgpack.packb(offer_data).hex(),
"offered_transient_ids_hex": [transient_id.hex() for transient_id in transient_ids],
"expected_responses": {
"receiver_has_none": True,
"receiver_has_all": False,
"receiver_has_first_hex": [transient_ids[1].hex()],
},
"sync_resource_plaintext_hex": umsgpack.packb(sync_data).hex(),
"submitted_entries_hex": [entry.hex() for entry in submitted_entries],
},
"rns_version_at_generation": RNS.__version__,
"lxmf_version_at_generation": LXMF.__version__,
"generator_script": "tools/regen_propagation_peer.py",
"verifies_spec_sections": ["5.8.2", "5.8.4", "5.8.5"],
}
with open(OUT_PATH, "w", encoding="utf-8", newline="\n") as output:
json.dump(payload, output, indent=2, sort_keys=False)
output.write("\n")
print(f"Wrote {OUT_PATH}")
print("ALL PASS")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,276 @@
"""
Verifier for SPEC.md S5.8 propagation-node announces and peer synchronization.
"""
from __future__ import annotations
import json
import os
import sys
import LXMF
import RNS
from LXMF import LXStamper
from LXMF.Handlers import LXMFPropagationAnnounceHandler
from LXMF.LXMF import PN_META_NAME, pn_announce_data_is_valid, pn_name_from_app_data, pn_stamp_cost_from_app_data
from LXMF.LXMRouter import LXMRouter
from LXMF.LXMPeer import LXMPeer
from RNS.vendor import umsgpack
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
VECTORS_PATH = os.path.join(REPO_ROOT, "test-vectors", "propagation-peer.json")
IDS_PATH = os.path.join(REPO_ROOT, "test-vectors", "identities.json")
def fail(message: str) -> None:
print(f"FAIL: {message}")
sys.exit(1)
def load_json(path: str):
with open(path, "r", encoding="utf-8") as input_file:
return json.load(input_file)
def load_identities():
vectors = load_json(IDS_PATH)["vectors"]
alice = next(vector for vector in vectors if vector["label"] == "alice")
bob = next(vector for vector in vectors if vector["label"] == "bob")
return (
RNS.Identity.from_bytes(bytes.fromhex(alice["inputs"]["private_key_hex"])),
RNS.Identity.from_bytes(bytes.fromhex(bob["inputs"]["private_key_hex"])),
)
def verify_announce(vector: dict) -> None:
announce = bytes.fromhex(vector["announce"]["app_data_hex"])
if not pn_announce_data_is_valid(announce):
fail("S5.8.5 canonical propagation announce was rejected")
decoded = umsgpack.unpackb(announce)
expected = vector["announce"]["decoded"]
if (
decoded[:6] != [
expected["legacy_support"],
expected["timebase"],
expected["propagation_enabled"],
expected["transfer_limit_kb"],
expected["sync_limit_kb"],
expected["stamp_costs"],
]
or decoded[6][PN_META_NAME].decode("utf-8") != expected["name_utf8"]
):
fail("S5.8.5 propagation announce fields mismatch")
if pn_name_from_app_data(announce) != expected["name_utf8"] or pn_stamp_cost_from_app_data(announce) != 16:
fail("S5.8.5 announce helper parsing mismatch")
# Upstream 0.9.7 accepts >=7 elements, ignores element 0, coerces numeric
# fields with int(), and accepts 0/1 because they compare equal to bools.
permissive = [b"ignored", "1700000120", 1, "256", 10240.0, ["16", 3.0, "18"], {}, b"extra"]
if not pn_announce_data_is_valid(umsgpack.packb(permissive)):
fail("S5.8.5 parser no longer accepts documented permissive variant")
for malformed in [
umsgpack.packb([False] * 6),
umsgpack.packb([False, 1, 2, 3, 4, [5, 6, 7], {}]),
umsgpack.packb([False, 1, True, 3, 4, [5, 6], {}]),
]:
if pn_announce_data_is_valid(malformed):
fail("S5.8.5 malformed announce was accepted")
print("PASS S5.8.5 announce producer fields, helpers, and parser acceptance boundary")
def verify_announce_handler(vector: dict) -> None:
announce = bytes.fromhex(vector["announce"]["app_data_hex"])
destination_hash = bytes.fromhex("11" * 16)
class FakeRouter:
propagation_node = True
static_peers = []
autopeer = True
autopeer_maxdepth = 4
peers = {}
pending_outbound = []
outbound_processing_lock = type("Lock", (), {"locked": lambda self: False})()
calls = []
@staticmethod
def get_outbound_propagation_node():
return None
def peer(self, **kwargs):
self.calls.append(("peer", kwargs))
def unpeer(self, destination_hash, timestamp):
self.calls.append(("unpeer", destination_hash, timestamp))
router = FakeRouter()
handler = LXMFPropagationAnnounceHandler(router)
real_hops_to = RNS.Transport.hops_to
RNS.Transport.hops_to = lambda destination: 4
try:
handler.received_announce(destination_hash, None, announce, bytes(32), False)
if len(router.calls) != 1 or router.calls[0][0] != "peer":
fail("S5.8.5 eligible direct announce did not trigger autopeer")
handler.received_announce(destination_hash, None, announce, bytes(32), True)
if len(router.calls) != 1:
fail("S5.8.5 path response incorrectly triggered autopeer")
disabled = umsgpack.unpackb(announce)
disabled[2] = False
handler.received_announce(destination_hash, None, umsgpack.packb(disabled), bytes(32), False)
if router.calls[-1][0] != "unpeer":
fail("S5.8.5 disabled node announce did not trigger unpeer")
finally:
RNS.Transport.hops_to = real_hops_to
print("PASS S5.8.5 announce handler autopeer, path-response, and disabled-node behavior")
def make_offer_router(identity, peering_cost: int, entries: dict):
class FakeRouter:
throttled_peers = {}
from_static_only = False
static_peers = []
validated_peer_links = {}
propagation_entries = entries
router = FakeRouter()
router.identity = identity
router.peering_cost = peering_cost
return router
def verify_offer(vector: dict, alice_id, bob_id) -> None:
peer = vector["peering"]
peering_id = bytes.fromhex(peer["peering_id_hex"])
peering_key = bytes.fromhex(peer["peering_key_hex"])
transient_ids = [bytes.fromhex(value) for value in peer["offered_transient_ids_hex"]]
offer_data = umsgpack.unpackb(bytes.fromhex(peer["offer_data_hex"]))
if offer_data != [peering_key, transient_ids]:
fail("S5.8.2 /offer vector decode mismatch")
expected_id = bob_id.hash + alice_id.hash
if peering_id != expected_id or not LXStamper.validate_peering_key(peering_id, peering_key, vector["inputs"]["peering_cost"]):
fail("S5.8.4 directional peering key did not validate")
if LXStamper.validate_peering_key(alice_id.hash + bob_id.hash, peering_key, vector["inputs"]["peering_cost"]):
fail("S5.8.4 peering key unexpectedly validated in reverse direction")
link_id = bytes.fromhex("22" * 16)
cases = [
({}, True),
({transient_ids[0]: [], transient_ids[1]: []}, False),
({transient_ids[0]: []}, [transient_ids[1]]),
]
for entries, expected in cases:
router = make_offer_router(bob_id, vector["inputs"]["peering_cost"], entries)
response = LXMRouter.offer_request(router, "/offer", offer_data, bytes(16), link_id, alice_id, 0)
if response != expected or router.validated_peer_links.get(link_id) is not True:
fail(f"S5.8.2 /offer response selection mismatch: {response!r}")
invalid_key_offer = [bytes(reversed(peering_key)), transient_ids]
router = make_offer_router(bob_id, vector["inputs"]["peering_cost"], {})
if LXMRouter.offer_request(router, "/offer", invalid_key_offer, bytes(16), link_id, alice_id, 0) != LXMPeer.ERROR_INVALID_KEY:
fail("S5.8.2 invalid peering key was not rejected")
if LXMRouter.offer_request(router, "/offer", offer_data, bytes(16), link_id, None, 0) != LXMPeer.ERROR_NO_IDENTITY:
fail("S5.8.2 unidentified /offer was not rejected")
# Version-specific hazard: malformed one-element lists pass the guard and
# then return None from the exception handler instead of INVALID_DATA.
malformed_result = LXMRouter.offer_request(router, "/offer", [peering_key], bytes(16), link_id, alice_id, 0)
if malformed_result is not None:
fail("S5.8.2 malformed /offer behavior changed; update hazard documentation")
print("PASS S5.8.2/S5.8.4 directional peering key and /offer response behavior")
def verify_sync_resource(vector: dict) -> None:
peer = vector["peering"]
decoded = umsgpack.unpackb(bytes.fromhex(peer["sync_resource_plaintext_hex"]))
entries = [bytes.fromhex(value) for value in peer["submitted_entries_hex"]]
if decoded != [vector["inputs"]["sync_time"], entries]:
fail("S5.8.2 peer-sync Resource plaintext mismatch")
if not all(LXStamper.validate_pn_stamp(entry, 0)[0] is not None for entry in entries):
fail("S5.8.2 peer-sync entries did not carry valid propagation stamps")
print("PASS S5.8.2 peer-sync Resource is [time, stamped_entry_list]")
def verify_unvalidated_transfer_limit(vector: dict) -> None:
entries = [bytes.fromhex(value) for value in vector["peering"]["submitted_entries_hex"]]
class Data:
def __init__(self, payload):
self.payload = payload
def read(self):
return self.payload
class Link:
link_id = bytes.fromhex("33" * 16)
def __init__(self):
self.torn_down = False
@staticmethod
def get_remote_identity():
return None
def teardown(self):
self.torn_down = True
class Resource:
status = RNS.Resource.COMPLETE
def __init__(self, messages):
self.link = Link()
self.data = Data(umsgpack.packb([vector["inputs"]["sync_time"], messages]))
class FakeRouter:
propagation_stamp_cost = 0
propagation_stamp_cost_flexibility = 0
def __init__(self):
self.validated_peer_links = {}
self.client_propagation_messages_received = 0
self.received = []
def lxmf_propagation(self, lxmf_data, **kwargs):
self.received.append(lxmf_data)
single_router = FakeRouter()
single = Resource(entries[:1])
LXMRouter.propagation_resource_concluded(single_router, single)
if len(single_router.received) != 1 or single.link.torn_down:
fail("S5.8.2 unvalidated single-message transfer was not accepted")
multi_router = FakeRouter()
multi = Resource(entries)
LXMRouter.propagation_resource_concluded(multi_router, multi)
if multi_router.received or not multi.link.torn_down:
fail("S5.8.2 unvalidated multi-message transfer was not rejected")
print("PASS S5.8.2 unvalidated links may transfer one message, not multiple")
def verify_constants() -> None:
if (
LXMRouter.AUTOPEER,
LXMRouter.AUTOPEER_MAXDEPTH,
LXMRouter.MAX_PEERS,
LXMRouter.PEERING_COST,
LXMRouter.MAX_PEERING_COST,
LXMRouter.PN_STAMP_THROTTLE,
) != (True, 4, 20, 18, 26, 180):
fail("S5.8 peer constants changed")
print("PASS S5.8 peer defaults and throttle interval")
def main() -> None:
print(f"verify_propagation_peer.py against RNS {RNS.__version__} / LXMF {LXMF.__version__}")
vector = load_json(VECTORS_PATH)
alice_id, bob_id = load_identities()
verify_announce(vector)
verify_announce_handler(vector)
verify_offer(vector, alice_id, bob_id)
verify_sync_resource(vector)
verify_unvalidated_transfer_limit(vector)
verify_constants()
print("ALL PASS")
if __name__ == "__main__":
main()