Completed the transport-relayed Link three-tier unit.

Key findings:

Valid established-Link traffic uses HEADER_1 and link_table.
HEADER_2 Link traffic can cross the addressed relay, then is dropped by the next node.
Relay forwarding requires correct interface and hop count.
Relay forwarding does not require IDX_LT_VALIDATED or destination_type=LINK.
Endpoint Link delivery does require destination_type=LINK.
Link-addressed PROOF uses link_table; ordinary DATA proofs use reverse_table.
Added:

Tier 1 audit
Transport-Link flow
Verifier
Deterministic vectors
Updated SPEC.md, playbook.md, README files, and existing Link flow documentation.

Verification:

Deterministic vector regeneration: identical SHA-256
Full pinned suite: 21 passed, 0 failed
git diff --check: passed
No commit created.
This commit is contained in:
John Poole 2026-06-08 17:50:52 -07:00
commit 4a14dca3a4
11 changed files with 633 additions and 20 deletions

View file

@ -57,6 +57,9 @@ 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 — §12.5 transport-relayed Link forwarding gates and proof routing.**
Earlier prose implied that established-Link forwarding begins only after LRPROOF validation, that relay lookup requires `destination_type == LINK`, and that Link proofs use `reverse_table`. Upstream RNS 1.2.4 forwards matching link-id traffic based on interface and hop count without checking `IDX_LT_VALIDATED` or destination type; endpoints still require LINK destination type for active-Link DATA dispatch. Link-addressed proofs use `link_table`, while `reverse_table` is for ordinary destination-routed DATA proofs. Corrected and runtime-locked by `tools/verify_transport_link.py`.
- **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. 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`.

34
SPEC.md
View file

@ -109,7 +109,7 @@ Source citations refer to the standard `pip install rns lxmf` install layout (`R
- [12.2 DATA forwarding rules](#122-data-forwarding-rules)
- [12.3 ANNOUNCE rebroadcasting](#123-announce-rebroadcasting)
- [12.4 Path table management](#124-path-table-management)
- [12.5 Reverse-table link transport](#125-reverse-table-link-transport)
- [12.5 Link-table and reverse-table transport](#125-link-table-and-reverse-table-transport)
- [12.6 Tunnels and shared-instance protocol](#126-tunnels-and-shared-instance-protocol)
- [12.7 Source map for §12](#127-source-map-for-12)
- [13. Threading and concurrency model](#13-threading-and-concurrency-model)
@ -1406,7 +1406,7 @@ new_raw += packet.raw[2:]
Transport.transmit(outbound_interface, new_raw)
```
A HEADER_2 link DATA packet would therefore arrive at the destination with `transport_id` intact, where the receiver's `Transport.packet_filter` (`RNS/Transport.py:1283-1285`) drops it as "for another transport instance" because the embedded `transport_id` is the relay's identity, not the receiver's:
A HEADER_2 link DATA packet addressed to the first relay can cross that relay with `transport_id` intact. The following node's `Transport.packet_filter` (`RNS/Transport.py:1283-1285`) then drops it as "for another transport instance", because the embedded `transport_id` still names the prior relay:
```python
if packet.transport_id != None and packet.packet_type != RNS.Packet.ANNOUNCE:
@ -3218,9 +3218,9 @@ new_raw += packet.raw[18:] # original dest_hash + ctx + bo
This is the inverse of the §2.3 originator HEADER_1→HEADER_2 conversion: the relay strips the transport_id when the packet has reached its last hop.
#### 12.2.3 `remaining_hops == 0`local destination, just bump hops
#### 12.2.3 `remaining_hops == 0`zero-hop path entry, just bump hops
The destination is registered on the relay itself (it's both our path-table next-hop AND a local destination). Just increment hops and pass through unchanged for local processing — the standard `Destination.receive` path takes over from there.
The relay preserves the existing header layout, increments hops, and transmits on the path entry's recorded interface. This case is used for zero-hop path entries such as destinations behind a connected local client; it is still an outbound transmit, not direct local `Destination.receive` dispatch.
#### 12.2.4 LINKREQUEST forwarding extras
@ -3228,7 +3228,7 @@ When the forwarded packet is a `LINKREQUEST`, the relay also writes a `link_tabl
```
[ now, # 0 IDX_LT_TIMESTAMP
next_hop, # 1 IDX_LT_NH_ID — next-hop transport_id
next_hop, # 1 IDX_LT_NH_TRID — next-hop transport_id
outbound_interface, # 2 IDX_LT_NH_IF
remaining_hops, # 3 IDX_LT_REM_HOPS
packet.receiving_interface, # 4 IDX_LT_RCVD_IF
@ -3320,9 +3320,9 @@ A relay also evicts path entries whose underlying interface has been removed (`r
If `[reticulum] persist_paths = Yes`, the path_table is serialized to `{storagepath}/paths` (a pickled dict in upstream RNS) so it survives restarts. The repeater repo's `pre_build.py` adds a "skip redundant path writes" patch to avoid hammering the on-board flash on nRF52 — for clean-room implementations, the persistence cadence is implementation-private.
### 12.5 Reverse-table link transport
### 12.5 Link-table and reverse-table transport
Once a Link's LINKREQUEST has been forwarded by a relay (§12.2.4 wrote the `link_table` entry), every subsequent Link packet — DATA, KEEPALIVE, PROOF, LINKCLOSE — must be forwarded by the same relay in the appropriate direction. `Transport.inbound` uses `link_table` and `reverse_table` for this:
Once a Link's LINKREQUEST has been forwarded by a relay (§12.2.4 wrote the `link_table` entry), every subsequent Link packet — DATA, KEEPALIVE, PROOF, LINKCLOSE — follows `link_table` in the appropriate direction. Separately, proofs for ordinary destination-routed DATA follow `reverse_table`.
#### 12.5.1 LRPROOF forwarding
@ -3334,17 +3334,21 @@ Transport.transmit(link_entry[IDX_LT_RCVD_IF], new_raw)
link_table[packet.destination_hash][IDX_LT_VALIDATED] = True
```
After validation, the link_table entry is marked `validated`, and from now on the relay forwards Link DATA in both directions transparently.
After validation, the link_table entry is marked `validated`. This flag controls stale-entry handling in `Transport.jobs`; it is **not** checked by the established-Link forwarding branch. Matching link-id traffic can be forwarded before LRPROOF validation.
#### 12.5.2 Link DATA forwarding
#### 12.5.2 Established-Link forwarding
For a `DATA` packet with `destination_type == LINK` whose `dest_hash` is in `link_table`, the relay forwards on the appropriate direction's interface. The link_table entry remembers both sides via `IDX_LT_NH_IF` (toward initiator end) and `IDX_LT_RCVD_IF` (toward responder end); the relay picks based on which interface the inbound packet arrived on.
For any non-ANNOUNCE, non-LINKREQUEST packet whose context is not LRPROOF and whose `dest_hash` is in `link_table`, the relay forwards on the appropriate direction's interface when the incremented hop count also matches. The link_table entry remembers both sides via `IDX_LT_NH_IF` (toward responder) and `IDX_LT_RCVD_IF` (toward initiator); the relay picks based on ingress interface and the direction's expected hop count.
Unlike the path_table forwarding in §12.2 — which strips `transport_id` (`HEADER_2``HEADER_1`) at the last hop — link_table forwarding does NOT touch the header. The relay bumps `hops` and emits `packet.raw[0:1] + struct.pack("!B", packet.hops) + packet.raw[2:]` (`Transport.py:1618-1620`). Whatever header bytes the initiator emitted reach the destination verbatim. This is why the originator's wire conventions for post-handshake link DATA are constrained — see §6.4.3: senders MUST emit `HEADER_1` with no `transport_id` for every link-addressed packet, because a `HEADER_2` link packet would arrive at the destination with `transport_id` intact and be dropped by the destination's `packet_filter` as "for another transport instance".
Link-table forwarding preserves the existing header layout and only rewrites the hops byte: `packet.raw[0:1] + struct.pack("!B", packet.hops) + packet.raw[2:]` (`Transport.py:1618-1620`). Valid post-handshake Link traffic is HEADER_1. An artificial HEADER_2 Link packet addressed to the first relay can cross that relay unchanged apart from hops, but the following node rejects it because the retained `transport_id` still names the prior relay (§6.4.3).
#### 12.5.3 PROOF receipt forwarding via `reverse_table`
The relay branch keys on `dest_hash`, interface, and hop count; it does not require `destination_type == LINK` and does not check `IDX_LT_VALIDATED`. Endpoints do require `destination_type == LINK` before dispatching local DATA to an active Link. A malformed destination type can therefore cross a relay and still be silently ignored at the endpoint.
`Transport.py:2199-2208`. When a PROOF arrives whose `dest_hash` is in `reverse_table` (i.e. an opportunistic-DATA proof being routed back to its originator), the relay pops the entry, checks the proof arrived on the correct outbound interface (`receiving_interface == reverse_entry[IDX_RT_OUTB_IF]`), and forwards on the originally-receiving interface:
#### 12.5.3 Link PROOF versus ordinary PROOF forwarding
A PROOF addressed to `link_id` follows the same bidirectional `link_table` branch as Link DATA. It does not consume `reverse_table`.
For an ordinary destination-routed DATA proof, `Transport.py:2199-2208` uses `reverse_table`. The relay pops the entry, checks the proof arrived on the correct outbound interface (`receiving_interface == reverse_entry[IDX_RT_OUTB_IF]`), and forwards on the originally-receiving interface:
```python
new_raw = packet.raw[0:1] + struct.pack("!B", packet.hops) + packet.raw[2:]
@ -3404,6 +3408,7 @@ The wire protocol for shared-instance loopback is just the same Reticulum packet
| `RNS/Transport.py:1500-1580` | DATA forwarding (HEADER_1↔HEADER_2 conversion for relay) |
| `RNS/Transport.py:1556-1565` | `link_table` entry shape |
| `RNS/Transport.py:1570-1574` | `reverse_table` entry shape |
| `RNS/Transport.py:1587-1622` | Bidirectional established-Link forwarding via `link_table` |
| `RNS/Transport.py:1810-1975` | ANNOUNCE rebroadcast queue and per-interface dispatch |
| `RNS/Transport.py:2110-2145` | LRPROOF forwarding via `link_table` |
| `RNS/Transport.py:2199-2208` | PROOF receipt forwarding via `reverse_table` |
@ -3555,7 +3560,8 @@ A client running on a constrained device (less RAM, slower CPU) can scale all of
| Sender sees DATA bursts repeatedly retransmitted, link dies | §6.5 — receiver isn't emitting the mandatory PROOF receipt for each CTX_NONE Link DATA packet | `tools/verify_proof_packet.py` |
| Some peers work, others reject every PROOF I send | §6.5.2 — wrong proof body length. Upstream default emits 64-byte implicit proofs (`signature` only) but your peer expects 96-byte explicit (`packet_hash \|\| signature`). Validator dispatches on length | `tools/verify_proof_packet.py` |
| Initiator and responder complete LRPROOF but every Resource ADV / link DATA the initiator sends is silently dropped at the responder | §6.4.2 — initiator never emitted LRRTT after LRPROOF. Responder stays in `HANDSHAKE`, `link_established` callback never fires, LXMF's `set_resource_strategy(ACCEPT_APP)` never installs, and `Link.receive` hits the silent `ACCEPT_NONE` branch on every RESOURCE_ADV | §6.4.2 |
| Single-hop link works, but the same flow over a multi-hop link silently drops every link DATA / Resource part at the destination | §6.4.3 — link-addressed packets emitted as `HEADER_2` with `transport_id` set to the next-hop relay. link_table forwarding doesn't strip `transport_id`, so the destination's `packet_filter` rejects it as "for another transport instance" (LOG_EXTREME). Use `HEADER_1` and `transport_id=None` regardless of hop count | §6.4.3 |
| Single-hop link works, but the same flow over a multi-hop link silently drops every link DATA / Resource part | §6.4.3 — link-addressed packets emitted as `HEADER_2`. The addressed relay can forward it, but the following node rejects the retained `transport_id` as naming another transport instance. Use `HEADER_1` and `transport_id=None` regardless of hop count | `tools/verify_transport_link.py` |
| Relay capture shows Link DATA forwarded, but endpoint callback never runs | §12.5.2 — verify `destination_type == LINK` and the active Link's attached interface. Relay link-table lookup does not itself require LINK destination type | `tools/verify_transport_link.py` |
### Resource transfers (large bodies)

View file

@ -170,7 +170,7 @@ Initial confidence assessment (subjective, not authoritative — re-do this audi
| §10 Resource fragmentation | Source-audited against RNS 1.2.4 and runtime-verified by `tools/verify_resource.py`, including deterministic vectors, receiver assembly/proof, multi-segment sizing, and negative cases. |
| §11 REQUEST/RESPONSE | Source-audited against RNS 1.2.4 and runtime-verified by `tools/verify_request_response.py`, including packet/Resource forms, request-ID domains, correlation, and authorization constants. |
| §18 Test vectors | Populated with identities, announces, opportunistic LXMF, Link establishment, link-delivered LXMF, Resource, and REQUEST/RESPONSE. Future work should add broader negative vectors. |
| §12 Source map | High |
| §12 Transport | Source-audited against RNS 1.2.4. Transport-relayed Link establishment, bidirectional traffic, and failure cases are runtime-verified by `tools/verify_transport_link.py`; broader announce/path/tunnel behavior remains source-cited. |
**Historical bootstrap tasks from the initial audit, now mostly complete:**

View file

@ -0,0 +1,81 @@
# Tier 1 Audit: Transport-Relayed Links
Question: How does an RNS 1.2.4 transport node forward LINKREQUEST, LRPROOF,
established-Link traffic, and proofs, and which failure conditions are hidden
by direct-Link tests?
Evidence baseline:
- RNS package: `rns==1.2.4`
- Primary source: `RNS/Transport.py`
- Audit date: 2026-06-08
Tier 2 evidence is `tools/verify_transport_link.py` and
`test-vectors/transport-link.json`. Confirmed findings are promoted into
`SPEC.md`, `flows/transport-link.md`, and `playbook.md`.
## Confirmed Model
1. LINKREQUEST is destination-routed through `path_table`. At a last-hop
relay, HEADER_2 is rewritten to HEADER_1 and a `link_table` entry is
created. The link ID remains invariant because its hashable part excludes
mutable transport-header bytes.
2. A relay's link-table entry records both interfaces and two expected hop
counts. Established-Link traffic is forwarded only when its ingress
interface and incremented hop count identify a valid direction.
3. LRPROOF has a stricter branch than established-Link traffic. It must arrive
from `IDX_LT_NH_IF`, match `IDX_LT_REM_HOPS`, and carry a valid responder
signature before it is forwarded and `IDX_LT_VALIDATED` becomes true.
4. The established-Link forwarding branch does **not** check
`IDX_LT_VALIDATED`. Matching link-id traffic can therefore be forwarded
while the entry is still awaiting LRPROOF. The flag controls stale-entry
handling in `Transport.jobs`; it is not a forwarding authorization gate.
5. The established-Link forwarding branch also does **not** require
`destination_type == LINK`; it keys on destination hash, packet kind/context
exclusions, interface, and hop count. A wrong destination type can cross a
relay, but the endpoint's local DATA dispatch only delivers
`destination_type == LINK` to an active Link.
6. Link DATA, Resource traffic, and Link-addressed PROOF packets follow
`link_table`, not `reverse_table`. `reverse_table` is used by ordinary
destination-routed DATA and its returning proof.
7. Valid post-handshake Link packets are HEADER_1. Link-table forwarding
preserves the existing header layout and only changes the hops byte. An
artificial HEADER_2 Link packet addressed to the first relay can cross that
relay, but the following node rejects it because its `transport_id` still
names the prior relay.
## Corrections to Bootstrap Prose
- §12.5 was titled "Reverse-table link transport", conflating two distinct
mechanisms. Link traffic uses `link_table`; ordinary DATA proofs use
`reverse_table`.
- §12.5 stated that forwarding begins after LRPROOF validation. The runtime
forwards matching traffic before validation.
- The playbook stated that omitting `dest_type=LINK` prevents relay lookup.
Relay lookup still occurs; endpoint Link dispatch is where that malformed
DATA is not delivered.
- The link-table constant at entry index 1 is `IDX_LT_NH_TRID`, not
`IDX_LT_NH_ID`.
## Tier 2 Scope
`tools/verify_transport_link.py` drives upstream `Transport.inbound` and
verifies:
1. Last-hop LINKREQUEST HEADER_2-to-HEADER_1 conversion and link-table shape.
2. LRPROOF success plus bad signature, wrong-interface, and wrong-hop drops.
3. Established-Link DATA forwarding in both directions.
4. Wrong-hop and wrong-interface drops.
5. Forwarding while `IDX_LT_VALIDATED` is false.
6. Relay forwarding of a wrong-destination-type packet, distinguishing relay
lookup from endpoint dispatch.
7. Link-addressed PROOF forwarding without `reverse_table`.
8. HEADER_2 Link traffic crossing the addressed relay and being rejected by
the next node's packet filter.

View file

@ -143,7 +143,9 @@ so the LXMessage advances `SENT → DELIVERED` when the recipient's PROOF for th
### 6. Wire bytes leave (KISS / HDLC framing)
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.
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 forward it via `link_table`, preserving the header layout and changing only hops. If a clean-room sender emits HEADER_2, the addressed relay can forward it, but the next node drops it because the retained `transport_id` still names the prior relay (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.
See [`transport-link.md`](transport-link.md) for direction-specific relay checks and failure diagnostics.
### 7. PROOF receipt arrives → `__mark_delivered` fires

49
flows/transport-link.md Normal file
View file

@ -0,0 +1,49 @@
# Flow: Transport-Relayed Link
This flow follows a Link across one transport relay, pinned to RNS 1.2.4.
It focuses on behavior that direct-Link testing cannot expose.
## Establishment
1. The initiator sends LINKREQUEST toward the responder's destination hash.
As normal destination-routed traffic, a multi-hop LINKREQUEST can be
HEADER_2 with `transport_id` naming the relay.
2. The relay matches that `transport_id` and the destination's `path_table`
entry. On the last hop it strips the transport slot, emits HEADER_1 toward
the responder, and creates a `link_table[link_id]` entry.
3. The responder emits LRPROOF as HEADER_1 toward `link_id`.
4. The relay accepts LRPROOF only from the responder-side interface at the
recorded remaining-hop count and only after validating the responder's
signature. It forwards LRPROOF toward the initiator and marks the table
entry validated.
## Established Traffic
Post-handshake packets use HEADER_1 and are addressed to `link_id`.
- Initiator to responder: packet arrives on `IDX_LT_RCVD_IF`; after inbound
hop increment it must equal `IDX_LT_HOPS`; relay sends on `IDX_LT_NH_IF`.
- Responder to initiator: packet arrives on `IDX_LT_NH_IF`; after inbound hop
increment it must equal `IDX_LT_REM_HOPS`; relay sends on
`IDX_LT_RCVD_IF`.
- The relay preserves every byte except the hops byte.
- DATA, Resource packets, control packets, and Link-addressed PROOF packets all
use this link-table path.
The forwarding branch is not gated by `IDX_LT_VALIDATED`. Validation affects
entry lifetime, while interface and hop-count checks determine whether a
packet is forwarded.
## Failure Diagnostics
| Observation | Meaning |
|---|---|
| LINKREQUEST crosses relay, LRPROOF does not | Check LRPROOF signature, ingress interface, and expected hop count. |
| Traffic works direct but not through relay | Check that post-handshake packets are HEADER_1 and use `link_id`; then check relay interface/hop direction. |
| Relay forwards DATA but endpoint silently ignores it | Confirm `destination_type == LINK` and that the endpoint Link is attached to the receiving interface. |
| First relay forwards HEADER_2 Link DATA, next node drops it | The stale `transport_id` still names the first relay; established-Link traffic must be HEADER_1. |
| Link traffic forwards before LRPROOF | Expected upstream behavior; `IDX_LT_VALIDATED` is not checked by the forwarding branch. |
Executable evidence: `tools/verify_transport_link.py` and
`test-vectors/transport-link.json`.

View file

@ -228,10 +228,10 @@ Each entry: date, one-line symptom, spec section that governs it, one-line fix,
### 2026-04 — Link DATA addressed without dest_type=LINK silently dropped
- **Symptom:** Outbound link DATA from the mobile-app arrives at the relay but is never forwarded to the responder.
- **Spec section:** §12.5.2. Packets addressed to a `link_id` MUST have `dest_type = LINK`; otherwise the relay's `link_table` lookup never fires and the packet is dropped.
- **Symptom:** Outbound link DATA from the mobile-app crosses the relay but is never delivered to the responder's active Link.
- **Spec section:** §12.5.2. Packets addressed to a `link_id` MUST have `dest_type = LINK`. Upstream relay lookup still keys on the link ID and can forward a wrong-type packet, but endpoint DATA dispatch only hands `dest_type = LINK` to active Links.
- **Fix:** Set `dest_type = DEST_LINK` on all post-handshake link-bound packets.
- **Lesson:** Look at every field the spec mandates, not just the ones that "look right."
- **Lesson:** Distinguish relay forwarding from endpoint dispatch; a relay capture alone does not prove delivery.
### 2026-03 — REQUEST path_hash truncation

View file

@ -15,8 +15,9 @@ Populated against RNS 1.2.4 / LXMF 0.9.7:
- ✅ `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`).
- ✅ `transport-link.json` — one-relay LINKREQUEST, LRPROOF, established-Link DATA, and invalid HEADER_2 fixtures (regenerator: `../tools/regen_transport_link.py`, verifier: `../tools/verify_transport_link.py`).
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.
All ten 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.
@ -33,6 +34,7 @@ Each vector lives in a per-domain JSON file, e.g.:
- `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
- `transport-link.json` — transport-relayed Link establishment and traffic forms
Each entry should include:
@ -62,5 +64,6 @@ For the spec to claim "an implementation that passes all test vectors interopera
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.
11. **Transport-relayed Link** — LINKREQUEST/LRPROOF relay state, bidirectional established-Link forwarding, and invalid-header failure behavior.
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,29 @@
{
"_about": "Transport-relayed Link fixtures derived from links.json and link-lxmf.json. The runtime verifier exercises upstream Transport.inbound with a one-relay path.",
"inputs": {
"link_vector_label": "alice_to_bob_aes256cbc",
"link_lxmf_vector_label": "alice_to_bob_direct_packet_boundary",
"relay_identity_label": "alice",
"relay_identity_hash_hex": "28d43a11abc1094301a59ed3b44f127b",
"next_hop_transport_id_hex": "202122232425262728292a2b2c2d2e2f"
},
"expected": {
"link_id_hex": "7ee5fe3e4952c9ac4519b537f6278474",
"destination_hash_hex": "8c670c64308e0325ea0fd7c72787449d",
"incoming_header2_linkrequest_raw_hex": "520028d43a11abc1094301a59ed3b44f127b8c670c64308e0325ea0fd7c72787449d007b4e909bbe7ffe44c465a220037d608ee35897d31ef972f07f74892cb0f73f13a09aa5f47a6759802ff955f8dc2d2a14a5c99d23be97f864127ff9383455a4f02001f4",
"forwarded_header1_linkrequest_raw_hex": "02018c670c64308e0325ea0fd7c72787449d007b4e909bbe7ffe44c465a220037d608ee35897d31ef972f07f74892cb0f73f13a09aa5f47a6759802ff955f8dc2d2a14a5c99d23be97f864127ff9383455a4f02001f4",
"incoming_lrproof_raw_hex": "0f007ee5fe3e4952c9ac4519b537f6278474ff1de2168a36a816163aec0bb0749ff6792f78eb4f7b39156f8ee5c8693e83ebd67439ac28d9e4603334428713154edd04395b0b8acec2f703c05c3d38af133e0c7b0d47d93427f8311160781c7c733fd89f88970aef490d8aa0ee19a4cb8a1b142001f4",
"forwarded_lrproof_raw_hex": "0f017ee5fe3e4952c9ac4519b537f6278474ff1de2168a36a816163aec0bb0749ff6792f78eb4f7b39156f8ee5c8693e83ebd67439ac28d9e4603334428713154edd04395b0b8acec2f703c05c3d38af133e0c7b0d47d93427f8311160781c7c733fd89f88970aef490d8aa0ee19a4cb8a1b142001f4",
"incoming_link_data_raw_hex": "0c007ee5fe3e4952c9ac4519b537f6278474005152535455565758595a5b5c5d5e5f6013989c7e9cb3ad3d547b14028cb2c8cb6d253f5e0de613cfa9ee588413147738ab5810b8e24c5174bf4d2f8e06a127c29703cd7ab276e020bdef730d1df6aac088518a1be128b927401a3998454207ff967f64bbe2cf58e8aea7759e3dd8d197b086d70ba654fe656bb7196b825f080c0edbe793d0b34c188fe0f56ea4f9c6912c01647c78fefe1f9b5cde08a322b7a5c86570018b7f4ea55ed8a07f1f4d6b34dbe75f3009965de388286f3971558fb7d0f0baae1ebafe0423aab50742d83d6042a25781553d808228f16a479c5d41fa23fee3cd01ab88b4d36c0fe9546baf6954c9a393026b04d0397692221f931548ec48afe7e6553c9e2f3c142eb38e692cb1cecaa880fe7372fd494d4dc9bd4852b172755074aff59634959e45338e90d3d7066480ab410a70dafaaaf579e953698dd7dab6ec2968e51e4f77550a1b7392c731066d8b53625cc163a3155f71b4a2201ffb7c7fc31d397e9140402afd7d722e447ef2e723e158c37ebf92828f0383a8eed02f233e1996a07d9d2ca572a1dbf49d58780d15c829fd3d53cc27b5016ce4fdd696c384b8d0ef71d1aba605c14320b5b0e0c20899e65e80cb35e80fa20fe5947055b2ad173bd1e58297affb8d4af5f1c1b27e1959b17e27c53625ddd264",
"forwarded_link_data_raw_hex": "0c017ee5fe3e4952c9ac4519b537f6278474005152535455565758595a5b5c5d5e5f6013989c7e9cb3ad3d547b14028cb2c8cb6d253f5e0de613cfa9ee588413147738ab5810b8e24c5174bf4d2f8e06a127c29703cd7ab276e020bdef730d1df6aac088518a1be128b927401a3998454207ff967f64bbe2cf58e8aea7759e3dd8d197b086d70ba654fe656bb7196b825f080c0edbe793d0b34c188fe0f56ea4f9c6912c01647c78fefe1f9b5cde08a322b7a5c86570018b7f4ea55ed8a07f1f4d6b34dbe75f3009965de388286f3971558fb7d0f0baae1ebafe0423aab50742d83d6042a25781553d808228f16a479c5d41fa23fee3cd01ab88b4d36c0fe9546baf6954c9a393026b04d0397692221f931548ec48afe7e6553c9e2f3c142eb38e692cb1cecaa880fe7372fd494d4dc9bd4852b172755074aff59634959e45338e90d3d7066480ab410a70dafaaaf579e953698dd7dab6ec2968e51e4f77550a1b7392c731066d8b53625cc163a3155f71b4a2201ffb7c7fc31d397e9140402afd7d722e447ef2e723e158c37ebf92828f0383a8eed02f233e1996a07d9d2ca572a1dbf49d58780d15c829fd3d53cc27b5016ce4fdd696c384b8d0ef71d1aba605c14320b5b0e0c20899e65e80cb35e80fa20fe5947055b2ad173bd1e58297affb8d4af5f1c1b27e1959b17e27c53625ddd264",
"invalid_header2_link_data_raw_hex": "5c0028d43a11abc1094301a59ed3b44f127b7ee5fe3e4952c9ac4519b537f6278474005152535455565758595a5b5c5d5e5f6013989c7e9cb3ad3d547b14028cb2c8cb6d253f5e0de613cfa9ee588413147738ab5810b8e24c5174bf4d2f8e06a127c29703cd7ab276e020bdef730d1df6aac088518a1be128b927401a3998454207ff967f64bbe2cf58e8aea7759e3dd8d197b086d70ba654fe656bb7196b825f080c0edbe793d0b34c188fe0f56ea4f9c6912c01647c78fefe1f9b5cde08a322b7a5c86570018b7f4ea55ed8a07f1f4d6b34dbe75f3009965de388286f3971558fb7d0f0baae1ebafe0423aab50742d83d6042a25781553d808228f16a479c5d41fa23fee3cd01ab88b4d36c0fe9546baf6954c9a393026b04d0397692221f931548ec48afe7e6553c9e2f3c142eb38e692cb1cecaa880fe7372fd494d4dc9bd4852b172755074aff59634959e45338e90d3d7066480ab410a70dafaaaf579e953698dd7dab6ec2968e51e4f77550a1b7392c731066d8b53625cc163a3155f71b4a2201ffb7c7fc31d397e9140402afd7d722e447ef2e723e158c37ebf92828f0383a8eed02f233e1996a07d9d2ca572a1dbf49d58780d15c829fd3d53cc27b5016ce4fdd696c384b8d0ef71d1aba605c14320b5b0e0c20899e65e80cb35e80fa20fe5947055b2ad173bd1e58297affb8d4af5f1c1b27e1959b17e27c53625ddd264"
},
"rns_version_at_generation": "1.2.4",
"generator_script": "tools/regen_transport_link.py",
"verifies_spec_sections": [
"6.3",
"6.4.3",
"12.2.4",
"12.5"
]
}

View file

@ -0,0 +1,96 @@
"""
Regenerator for test-vectors/transport-link.json.
Derives deterministic relay-facing Link packets from the existing handshake
and DIRECT-LXMF vectors. Runtime forwarding behavior is verified separately by
tools/verify_transport_link.py against upstream RNS 1.2.4.
"""
from __future__ import annotations
import json
import os
import RNS
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LINKS_PATH = os.path.join(REPO_ROOT, "test-vectors", "links.json")
LINK_LXMF_PATH = os.path.join(REPO_ROOT, "test-vectors", "link-lxmf.json")
IDS_PATH = os.path.join(REPO_ROOT, "test-vectors", "identities.json")
OUT_PATH = os.path.join(REPO_ROOT, "test-vectors", "transport-link.json")
NEXT_HOP_ID = bytes.fromhex("202122232425262728292a2b2c2d2e2f")
def load_json(path: str):
with open(path, "r", encoding="utf-8") as input_file:
return json.load(input_file)
def header_two(raw: bytes, transport_id: bytes) -> bytes:
flags = (
(RNS.Packet.HEADER_2 << 6)
| (RNS.Transport.TRANSPORT << 4)
| (raw[0] & 0x0F)
)
return bytes([flags, raw[1]]) + transport_id + raw[2:]
def bump_hops(raw: bytes) -> bytes:
return raw[:1] + bytes([raw[1] + 1]) + raw[2:]
def main() -> None:
links = load_json(LINKS_PATH)["vectors"][0]
link_lxmf = load_json(LINK_LXMF_PATH)["vectors"][0]
identities = load_json(IDS_PATH)["vectors"]
alice = next(item for item in identities if item["label"] == "alice")
relay_id = bytes.fromhex(alice["expected"]["identity_hash_hex"])
linkrequest = bytes.fromhex(links["expected"]["linkrequest_raw_hex"])
lrproof = bytes.fromhex(links["expected"]["lrproof_raw_hex"])
link_data = bytes.fromhex(link_lxmf["expected"]["link_packet_raw_hex"])
incoming_linkrequest = header_two(linkrequest, relay_id)
forwarded_linkrequest = bump_hops(linkrequest)
forwarded_lrproof = bump_hops(lrproof)
forwarded_link_data = bump_hops(link_data)
header_two_link_data = header_two(link_data, relay_id)
vector = {
"_about": (
"Transport-relayed Link fixtures derived from links.json and "
"link-lxmf.json. The runtime verifier exercises upstream "
"Transport.inbound with a one-relay path."
),
"inputs": {
"link_vector_label": links["label"],
"link_lxmf_vector_label": link_lxmf["label"],
"relay_identity_label": "alice",
"relay_identity_hash_hex": relay_id.hex(),
"next_hop_transport_id_hex": NEXT_HOP_ID.hex(),
},
"expected": {
"link_id_hex": links["expected"]["link_id_hex"],
"destination_hash_hex": linkrequest[2:18].hex(),
"incoming_header2_linkrequest_raw_hex": incoming_linkrequest.hex(),
"forwarded_header1_linkrequest_raw_hex": forwarded_linkrequest.hex(),
"incoming_lrproof_raw_hex": lrproof.hex(),
"forwarded_lrproof_raw_hex": forwarded_lrproof.hex(),
"incoming_link_data_raw_hex": link_data.hex(),
"forwarded_link_data_raw_hex": forwarded_link_data.hex(),
"invalid_header2_link_data_raw_hex": header_two_link_data.hex(),
},
"rns_version_at_generation": RNS.__version__,
"generator_script": "tools/regen_transport_link.py",
"verifies_spec_sections": ["6.3", "6.4.3", "12.2.4", "12.5"],
}
with open(OUT_PATH, "w", encoding="utf-8") as output_file:
json.dump(vector, output_file, indent=2)
output_file.write("\n")
print(f"Wrote {OUT_PATH}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,344 @@
"""
Verifier for transport-relayed Link establishment and established-Link traffic.
Exercises upstream RNS 1.2.4 Transport.inbound with a synthetic one-relay
topology. This catches behavior that direct-Link and self-round-trip tests do
not expose.
"""
from __future__ import annotations
import json
import os
import sys
import tempfile
import time
import RNS
from RNS import Transport
from RNS.Transport import (
IDX_LT_DSTHASH,
IDX_LT_HOPS,
IDX_LT_NH_IF,
IDX_LT_NH_TRID,
IDX_LT_PROOF_TMO,
IDX_LT_RCVD_IF,
IDX_LT_REM_HOPS,
IDX_LT_VALIDATED,
IDX_PT_EXPIRES,
IDX_PT_HOPS,
IDX_PT_NEXT_HOP,
IDX_PT_PACKET,
IDX_PT_RANDBLOBS,
IDX_PT_RVCD_IF,
IDX_PT_TIMESTAMP,
)
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
VECTORS_PATH = os.path.join(REPO_ROOT, "test-vectors", "transport-link.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 init_minimal_rns():
config_dir = tempfile.mkdtemp(prefix="rns-verify-transport-link-")
config_path = os.path.join(config_dir, "config")
with open(config_path, "w", encoding="utf-8") as config:
config.write("[reticulum]\nenable_transport = No\nshare_instance = No\n")
return RNS.Reticulum(configdir=config_dir, loglevel=0)
class FakeInterface:
OUT = True
IN = True
HW_MTU = RNS.Reticulum.MTU
AUTOCONFIGURE_MTU = True
FIXED_MTU = True
bitrate = 1_000_000
mode = RNS.Interfaces.Interface.Interface.MODE_FULL
def __init__(self, name: str):
self.name = name
def __str__(self):
return self.name
def clear_transport_state() -> None:
Transport.packet_hashlist = set()
Transport.packet_hashlist_prev = set()
Transport.path_table.clear()
Transport.link_table.clear()
Transport.reverse_table.clear()
Transport.destinations_map.clear()
Transport.pending_links.clear()
Transport.active_links.clear()
Transport.local_client_interfaces.clear()
def assert_forward(captured: list, interface, raw: bytes, label: str) -> None:
if len(captured) != 1:
fail(f"{label}: expected one forwarded packet, got {len(captured)}")
if captured[0][0] is not interface:
fail(f"{label}: forwarded on wrong interface")
if captured[0][1] != raw:
fail(f"{label}: forwarded bytes mismatch")
def assert_drop(captured: list, label: str) -> None:
if captured:
fail(f"{label}: expected drop, got {len(captured)} forwarded packet(s)")
def seed_path(destination_hash: bytes, next_hop: bytes, outbound_interface) -> None:
Transport.path_table[destination_hash] = [
time.time(), next_hop, 1, time.time() + 60, [],
outbound_interface, None,
]
def make_link_entry(next_hop: bytes, responder_if, initiator_if,
destination_hash: bytes, validated: bool = False) -> list:
return [
time.time(), next_hop, responder_if, 1, initiator_if, 1,
destination_hash, validated, time.time() + 60,
]
def verify_linkrequest(vector: dict, initiator_if, responder_if, captured: list) -> list:
expected = vector["expected"]
destination_hash = bytes.fromhex(expected["destination_hash_hex"])
link_id = bytes.fromhex(expected["link_id_hex"])
next_hop = bytes.fromhex(vector["inputs"]["next_hop_transport_id_hex"])
seed_path(destination_hash, next_hop, responder_if)
Transport.inbound(
bytes.fromhex(expected["incoming_header2_linkrequest_raw_hex"]),
initiator_if,
)
assert_forward(
captured, responder_if,
bytes.fromhex(expected["forwarded_header1_linkrequest_raw_hex"]),
"LINKREQUEST last-hop forwarding",
)
if link_id not in Transport.link_table:
fail("LINKREQUEST did not create link_table entry")
entry = Transport.link_table[link_id]
if entry[IDX_LT_NH_TRID] != next_hop:
fail("link_table next-hop transport ID mismatch")
if entry[IDX_LT_NH_IF] is not responder_if or entry[IDX_LT_RCVD_IF] is not initiator_if:
fail("link_table interfaces do not preserve Link direction")
if entry[IDX_LT_REM_HOPS] != 1 or entry[IDX_LT_HOPS] != 1:
fail("link_table expected-hop values mismatch")
if entry[IDX_LT_DSTHASH] != destination_hash or entry[IDX_LT_VALIDATED]:
fail("link_table destination/initial validation state mismatch")
if entry[IDX_LT_PROOF_TMO] <= time.time():
fail("link_table proof timeout is not in the future")
print("PASS S12.2.4 relayed LINKREQUEST strips HEADER_2 and creates directional link_table state")
return entry
def verify_lrproof(vector: dict, bob_identity, initiator_if, responder_if,
captured: list) -> None:
expected = vector["expected"]
link_id = bytes.fromhex(expected["link_id_hex"])
destination_hash = bytes.fromhex(expected["destination_hash_hex"])
next_hop = bytes.fromhex(vector["inputs"]["next_hop_transport_id_hex"])
RNS.Identity.remember(bytes(32), destination_hash, bob_identity.get_public_key(), None)
captured.clear()
Transport.inbound(bytes.fromhex(expected["incoming_lrproof_raw_hex"]), responder_if)
assert_forward(
captured, initiator_if, bytes.fromhex(expected["forwarded_lrproof_raw_hex"]),
"valid LRPROOF",
)
if not Transport.link_table[link_id][IDX_LT_VALIDATED]:
fail("valid LRPROOF did not validate link_table entry")
for label, raw, interface in [
("wrong-interface LRPROOF", bytes.fromhex(expected["incoming_lrproof_raw_hex"]), initiator_if),
("wrong-hop LRPROOF", bytes.fromhex(expected["incoming_lrproof_raw_hex"])[:1] + b"\x01" + bytes.fromhex(expected["incoming_lrproof_raw_hex"])[2:], responder_if),
]:
captured.clear()
Transport.link_table[link_id] = make_link_entry(next_hop, responder_if, initiator_if, destination_hash)
Transport.inbound(raw, interface)
assert_drop(captured, label)
if Transport.link_table[link_id][IDX_LT_VALIDATED]:
fail(f"{label}: invalid proof marked link validated")
tampered = bytearray.fromhex(expected["incoming_lrproof_raw_hex"])
tampered[-4] ^= 0x01
captured.clear()
Transport.link_table[link_id] = make_link_entry(next_hop, responder_if, initiator_if, destination_hash)
Transport.inbound(bytes(tampered), responder_if)
assert_drop(captured, "bad-signature LRPROOF")
print("PASS S12.5.1 LRPROOF requires valid signature, direction, and hop count")
def verify_established_traffic(vector: dict, initiator_if, responder_if,
captured: list) -> None:
expected = vector["expected"]
link_id = bytes.fromhex(expected["link_id_hex"])
destination_hash = bytes.fromhex(expected["destination_hash_hex"])
next_hop = bytes.fromhex(vector["inputs"]["next_hop_transport_id_hex"])
incoming = bytes.fromhex(expected["incoming_link_data_raw_hex"])
forwarded = bytes.fromhex(expected["forwarded_link_data_raw_hex"])
for label, ingress, egress, validated in [
("initiator-to-responder", initiator_if, responder_if, True),
("responder-to-initiator", responder_if, initiator_if, True),
("pre-LRPROOF forwarding", initiator_if, responder_if, False),
]:
captured.clear()
Transport.packet_hashlist.clear()
Transport.packet_hashlist_prev.clear()
Transport.link_table[link_id] = make_link_entry(
next_hop, responder_if, initiator_if, destination_hash, validated
)
Transport.inbound(incoming, ingress)
assert_forward(captured, egress, forwarded, label)
for label, raw, ingress in [
("wrong-hop Link DATA", incoming[:1] + b"\x01" + incoming[2:], initiator_if),
("wrong-interface Link DATA", incoming, FakeInterface("unrelated")),
]:
captured.clear()
Transport.packet_hashlist.clear()
Transport.packet_hashlist_prev.clear()
Transport.link_table[link_id] = make_link_entry(
next_hop, responder_if, initiator_if, destination_hash, True
)
Transport.inbound(raw, ingress)
assert_drop(captured, label)
wrong_type = bytes([(incoming[0] & ~0x0C) | (RNS.Destination.SINGLE << 2)]) + incoming[1:]
captured.clear()
Transport.packet_hashlist.clear()
Transport.packet_hashlist_prev.clear()
Transport.link_table[link_id] = make_link_entry(
next_hop, responder_if, initiator_if, destination_hash, True
)
Transport.inbound(wrong_type, initiator_if)
assert_forward(captured, responder_if, wrong_type[:1] + b"\x01" + wrong_type[2:], "non-LINK destination type")
class EndpointLink:
def __init__(self):
self.link_id = link_id
self.attached_interface = responder_if
self.received = []
def receive(self, packet):
self.received.append(packet)
endpoint_link = EndpointLink()
Transport.link_table.clear()
Transport.active_links.append(endpoint_link)
Transport.packet_hashlist.clear()
Transport.packet_hashlist_prev.clear()
Transport.inbound(wrong_type, responder_if)
if endpoint_link.received:
fail("endpoint delivered non-LINK destination type to active Link")
Transport.packet_hashlist.clear()
Transport.packet_hashlist_prev.clear()
Transport.inbound(incoming, responder_if)
if len(endpoint_link.received) != 1:
fail("endpoint did not deliver LINK destination type to active Link")
Transport.active_links.clear()
proof = bytes([(incoming[0] & ~0x03) | RNS.Packet.PROOF]) + incoming[1:35]
captured.clear()
Transport.packet_hashlist.clear()
Transport.packet_hashlist_prev.clear()
Transport.link_table[link_id] = make_link_entry(
next_hop, responder_if, initiator_if, destination_hash, True
)
Transport.inbound(proof, responder_if)
assert_forward(captured, initiator_if, proof[:1] + b"\x01" + proof[2:], "Link PROOF")
if Transport.reverse_table:
fail("Link PROOF unexpectedly used reverse_table")
print("PASS S12.5.2 link_table forwards both directions and Link PROOF without reverse_table")
print("PASS S12.5.2 relay lookup ignores VALIDATED/dest_type; endpoint Link dispatch requires dest_type=LINK")
def verify_invalid_header_two(vector: dict, initiator_if, responder_if,
captured: list, original_identity) -> None:
expected = vector["expected"]
link_id = bytes.fromhex(expected["link_id_hex"])
destination_hash = bytes.fromhex(expected["destination_hash_hex"])
next_hop = bytes.fromhex(vector["inputs"]["next_hop_transport_id_hex"])
raw = bytes.fromhex(expected["invalid_header2_link_data_raw_hex"])
captured.clear()
Transport.packet_hashlist.clear()
Transport.packet_hashlist_prev.clear()
Transport.link_table[link_id] = make_link_entry(
next_hop, responder_if, initiator_if, destination_hash, True
)
Transport.inbound(raw, initiator_if)
assert_forward(captured, responder_if, raw[:1] + b"\x01" + raw[2:], "HEADER_2 Link DATA first relay")
packet = RNS.Packet(None, captured[0][1])
if not packet.unpack():
fail("could not unpack forwarded HEADER_2 Link DATA")
Transport.packet_hashlist.clear()
Transport.packet_hashlist_prev.clear()
Transport.identity = RNS.Identity()
if Transport.packet_filter(packet):
fail("next node accepted HEADER_2 Link DATA addressed to prior relay")
Transport.identity = original_identity
print("PASS S6.4.3 HEADER_2 Link DATA survives first link_table relay but is rejected by the next node")
def main() -> None:
print(f"verify_transport_link.py against RNS {RNS.__version__}")
init_minimal_rns()
vector = load_json(VECTORS_PATH)
identities = load_json(IDS_PATH)["vectors"]
alice = next(item for item in identities if item["label"] == "alice")
bob = next(item for item in identities if item["label"] == "bob")
relay_identity = RNS.Identity.from_bytes(bytes.fromhex(alice["inputs"]["private_key_hex"]))
bob_identity = RNS.Identity.from_bytes(bytes.fromhex(bob["inputs"]["private_key_hex"]))
initiator_if = FakeInterface("initiator-side")
responder_if = FakeInterface("responder-side")
captured: list[tuple[object, bytes]] = []
original_transmit = Transport.transmit
original_transport_enabled = RNS.Reticulum.transport_enabled
original_identity = Transport.identity
try:
clear_transport_state()
if relay_identity.hash.hex() != vector["inputs"]["relay_identity_hash_hex"]:
fail("pinned relay identity hash mismatch")
Transport.identity = relay_identity
RNS.Reticulum.transport_enabled = staticmethod(lambda: True)
Transport.transmit = staticmethod(lambda interface, raw: captured.append((interface, raw)))
verify_linkrequest(vector, initiator_if, responder_if, captured)
verify_lrproof(vector, bob_identity, initiator_if, responder_if, captured)
verify_established_traffic(vector, initiator_if, responder_if, captured)
verify_invalid_header_two(vector, initiator_if, responder_if, captured, relay_identity)
print("ALL PASS")
finally:
Transport.transmit = original_transmit
RNS.Reticulum.transport_enabled = original_transport_enabled
Transport.identity = original_identity
clear_transport_state()
try:
RNS.Reticulum.exit_handler()
except Exception:
pass
if __name__ == "__main__":
main()