Completed the destination-routed DATA and reverse-PROOF three-tier unit. Key findings: Intermediate relays retarget HEADER_2; the final relay delivers HEADER_1. Each relay records independent reverse-table state. PROOF destination hashes remain invariant across transport transformations. A wrong-interface PROOF consumes the reverse entry before being dropped. REVERSE_TIMEOUT is 480 seconds, not 30 seconds. Added Tier 1 audit, two-relay flow, deterministic vectors, regenerator, and runtime verifier. Corrected affected specification, flows, playbook, and status documentation. Verification: Deterministic regeneration: identical SHA-256 Full pinned suite: 22 passed, 0 failed git diff --check: passed No commit created.
288 lines
11 KiB
Python
288 lines
11 KiB
Python
"""
|
|
Verifier for ordinary DATA and PROOF routing through transport relays.
|
|
|
|
Exercises a deterministic two-relay path against upstream RNS 1.2.4
|
|
Transport.inbound. This distinguishes path_table forwarding from link_table
|
|
forwarding and verifies the one-shot reverse_table return path.
|
|
"""
|
|
|
|
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_RT_OUTB_IF, IDX_RT_RCVD_IF, IDX_RT_TIMESTAMP
|
|
|
|
|
|
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
VECTORS_PATH = os.path.join(REPO_ROOT, "test-vectors", "transport-data.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-data-")
|
|
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_state() -> None:
|
|
Transport.packet_hashlist = set()
|
|
Transport.packet_hashlist_prev = set()
|
|
Transport.path_table.clear()
|
|
Transport.reverse_table.clear()
|
|
Transport.link_table.clear()
|
|
Transport.destinations_map.clear()
|
|
Transport.receipts.clear()
|
|
Transport.local_client_interfaces.clear()
|
|
|
|
|
|
def seed_path(destination_hash: bytes, next_hop: bytes, hops: int, outbound_if) -> None:
|
|
Transport.path_table[destination_hash] = [
|
|
time.time(), next_hop, hops, time.time() + 60, [], outbound_if, None,
|
|
]
|
|
|
|
|
|
def expect_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}: wrong outbound interface")
|
|
if captured[0][1] != raw:
|
|
fail(f"{label}: forwarded bytes mismatch")
|
|
|
|
|
|
def packet(raw: bytes) -> RNS.Packet:
|
|
parsed = RNS.Packet(None, raw)
|
|
if not parsed.unpack():
|
|
fail("could not unpack fixture")
|
|
return parsed
|
|
|
|
|
|
def check_reverse_entry(entry: list, ingress, egress, label: str) -> None:
|
|
if entry[IDX_RT_RCVD_IF] is not ingress or entry[IDX_RT_OUTB_IF] is not egress:
|
|
fail(f"{label}: reverse-table interfaces mismatch")
|
|
if entry[IDX_RT_TIMESTAMP] > time.time():
|
|
fail(f"{label}: reverse-table timestamp is in the future")
|
|
|
|
|
|
def verify_data_path(vector: dict, relay_one_identity, relay_two_identity,
|
|
origin_if, between_if, destination_if, captured: list) -> tuple[list, list]:
|
|
expected = vector["expected"]
|
|
destination_hash = bytes.fromhex(expected["destination_hash_hex"])
|
|
proof_hash = bytes.fromhex(expected["proof_destination_hash_hex"])
|
|
|
|
Transport.identity = relay_one_identity
|
|
seed_path(destination_hash, relay_two_identity.hash, 2, between_if)
|
|
Transport.inbound(
|
|
bytes.fromhex(expected["relay_one_inbound_header2_data_raw_hex"]), origin_if
|
|
)
|
|
expect_forward(
|
|
captured, between_if,
|
|
bytes.fromhex(expected["relay_one_forwarded_header2_data_raw_hex"]),
|
|
"intermediate relay DATA",
|
|
)
|
|
if proof_hash not in Transport.reverse_table:
|
|
fail("intermediate relay did not create reverse-table entry")
|
|
relay_one_reverse = Transport.reverse_table[proof_hash]
|
|
check_reverse_entry(relay_one_reverse, origin_if, between_if, "intermediate relay")
|
|
|
|
captured.clear()
|
|
Transport.packet_hashlist.clear()
|
|
Transport.packet_hashlist_prev.clear()
|
|
Transport.path_table.clear()
|
|
Transport.reverse_table.clear()
|
|
Transport.identity = relay_two_identity
|
|
seed_path(destination_hash, destination_hash, 1, destination_if)
|
|
Transport.inbound(
|
|
bytes.fromhex(expected["relay_one_forwarded_header2_data_raw_hex"]), between_if
|
|
)
|
|
expect_forward(
|
|
captured, destination_if,
|
|
bytes.fromhex(expected["relay_two_delivered_header1_data_raw_hex"]),
|
|
"last-hop relay DATA",
|
|
)
|
|
if proof_hash not in Transport.reverse_table:
|
|
fail("last-hop relay did not create reverse-table entry")
|
|
relay_two_reverse = Transport.reverse_table[proof_hash]
|
|
check_reverse_entry(relay_two_reverse, between_if, destination_if, "last-hop relay")
|
|
|
|
forms = [
|
|
bytes.fromhex(expected["origin_header1_data_raw_hex"]),
|
|
bytes.fromhex(expected["relay_one_inbound_header2_data_raw_hex"]),
|
|
bytes.fromhex(expected["relay_one_forwarded_header2_data_raw_hex"]),
|
|
bytes.fromhex(expected["relay_two_delivered_header1_data_raw_hex"]),
|
|
]
|
|
hashes = {packet(raw).getTruncatedHash() for raw in forms}
|
|
if hashes != {proof_hash}:
|
|
fail("DATA proof destination changed across header/hop transformations")
|
|
|
|
print("PASS S12.2 two-relay DATA path retargets HEADER_2 then delivers HEADER_1")
|
|
print("PASS S6.5/S12.2.5 proof destination is invariant and each relay records reverse state")
|
|
return relay_one_reverse, relay_two_reverse
|
|
|
|
|
|
def verify_proof_path(vector: dict, relay_one_identity, relay_two_identity,
|
|
origin_if, between_if, destination_if, captured: list,
|
|
relay_one_reverse: list, relay_two_reverse: list) -> None:
|
|
expected = vector["expected"]
|
|
proof_hash = bytes.fromhex(expected["proof_destination_hash_hex"])
|
|
proof = bytes.fromhex(expected["destination_proof_raw_hex"])
|
|
|
|
captured.clear()
|
|
Transport.packet_hashlist.clear()
|
|
Transport.packet_hashlist_prev.clear()
|
|
Transport.reverse_table = {proof_hash: relay_two_reverse}
|
|
Transport.identity = relay_two_identity
|
|
Transport.inbound(proof, destination_if)
|
|
expect_forward(
|
|
captured, between_if,
|
|
bytes.fromhex(expected["relay_two_forwarded_proof_raw_hex"]),
|
|
"last-hop relay PROOF",
|
|
)
|
|
if proof_hash in Transport.reverse_table:
|
|
fail("last-hop relay did not consume reverse-table entry")
|
|
|
|
captured.clear()
|
|
Transport.packet_hashlist.clear()
|
|
Transport.packet_hashlist_prev.clear()
|
|
Transport.reverse_table = {proof_hash: relay_one_reverse}
|
|
Transport.identity = relay_one_identity
|
|
Transport.inbound(
|
|
bytes.fromhex(expected["relay_two_forwarded_proof_raw_hex"]), between_if
|
|
)
|
|
expect_forward(
|
|
captured, origin_if,
|
|
bytes.fromhex(expected["relay_one_forwarded_proof_raw_hex"]),
|
|
"intermediate relay PROOF",
|
|
)
|
|
if proof_hash in Transport.reverse_table:
|
|
fail("intermediate relay did not consume reverse-table entry")
|
|
|
|
wrong_if = FakeInterface("wrong-proof-interface")
|
|
captured.clear()
|
|
Transport.packet_hashlist.clear()
|
|
Transport.packet_hashlist_prev.clear()
|
|
Transport.reverse_table = {proof_hash: relay_two_reverse}
|
|
Transport.identity = relay_two_identity
|
|
Transport.inbound(proof, wrong_if)
|
|
if captured:
|
|
fail("wrong-interface PROOF was forwarded")
|
|
if proof_hash in Transport.reverse_table:
|
|
fail("wrong-interface PROOF did not consume reverse-table entry")
|
|
|
|
captured.clear()
|
|
Transport.packet_hashlist.clear()
|
|
Transport.packet_hashlist_prev.clear()
|
|
Transport.inbound(proof, destination_if)
|
|
if captured:
|
|
fail("PROOF routed after wrong-interface packet consumed reverse state")
|
|
|
|
print("PASS S12.5.3 PROOF returns through both relays and consumes one-shot reverse entries")
|
|
print("PASS S12.5.3 wrong-interface PROOF is dropped after consuming its reverse entry")
|
|
|
|
|
|
def verify_guards(vector: dict, relay_one_identity, origin_if, between_if,
|
|
captured: list) -> None:
|
|
expected = vector["expected"]
|
|
destination_hash = bytes.fromhex(expected["destination_hash_hex"])
|
|
incoming = bytes.fromhex(expected["relay_one_inbound_header2_data_raw_hex"])
|
|
|
|
captured.clear()
|
|
clear_state()
|
|
Transport.identity = relay_one_identity
|
|
Transport.inbound(incoming, origin_if)
|
|
if captured or Transport.reverse_table:
|
|
fail("DATA with no path was forwarded or recorded")
|
|
|
|
captured.clear()
|
|
clear_state()
|
|
Transport.identity = RNS.Identity()
|
|
seed_path(destination_hash, bytes(16), 1, between_if)
|
|
Transport.inbound(incoming, origin_if)
|
|
if captured or Transport.reverse_table:
|
|
fail("DATA addressed to another transport identity was forwarded or recorded")
|
|
|
|
if Transport.REVERSE_TIMEOUT != 8 * 60:
|
|
fail(f"REVERSE_TIMEOUT is {Transport.REVERSE_TIMEOUT}, expected 480 seconds")
|
|
print("PASS S12.2 unknown-path/wrong-transport DATA drops without reverse state")
|
|
print("PASS S12.5.3 RNS 1.2.4 REVERSE_TIMEOUT is 480 seconds")
|
|
|
|
|
|
def main() -> None:
|
|
print(f"verify_transport_data.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_one_identity = RNS.Identity.from_bytes(bytes.fromhex(alice["inputs"]["private_key_hex"]))
|
|
relay_two_identity = RNS.Identity.from_bytes(bytes.fromhex(bob["inputs"]["private_key_hex"]))
|
|
origin_if = FakeInterface("origin-side")
|
|
between_if = FakeInterface("between-relays")
|
|
destination_if = FakeInterface("destination-side")
|
|
captured: list[tuple[object, bytes]] = []
|
|
original_transmit = Transport.transmit
|
|
original_transport_enabled = RNS.Reticulum.transport_enabled
|
|
original_identity = Transport.identity
|
|
|
|
try:
|
|
clear_state()
|
|
RNS.Reticulum.transport_enabled = staticmethod(lambda: True)
|
|
Transport.transmit = staticmethod(lambda interface, raw: captured.append((interface, raw)))
|
|
relay_one_reverse, relay_two_reverse = verify_data_path(
|
|
vector, relay_one_identity, relay_two_identity,
|
|
origin_if, between_if, destination_if, captured,
|
|
)
|
|
verify_proof_path(
|
|
vector, relay_one_identity, relay_two_identity,
|
|
origin_if, between_if, destination_if, captured,
|
|
relay_one_reverse, relay_two_reverse,
|
|
)
|
|
verify_guards(vector, relay_one_identity, origin_if, between_if, captured)
|
|
print("ALL PASS")
|
|
finally:
|
|
Transport.transmit = original_transmit
|
|
RNS.Reticulum.transport_enabled = original_transport_enabled
|
|
Transport.identity = original_identity
|
|
clear_state()
|
|
try:
|
|
RNS.Reticulum.exit_handler()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|