From 7311dc85445fa13863fca288e3706d5c72abd738 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Mon, 24 Aug 2026 17:03:48 +0200 Subject: [PATCH 01/16] Reduced path table lock acquisitions in inbound processing --- RNS/Transport.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/RNS/Transport.py b/RNS/Transport.py index fb5e6b93..0166eb74 100755 --- a/RNS/Transport.py +++ b/RNS/Transport.py @@ -2009,9 +2009,10 @@ class Transport: if packet.transport_id != None and packet.packet_type != RNS.Packet.ANNOUNCE: if not packet.transport_id == Transport.identity.hash: return else: - if packet.destination_hash in Transport.path_table: - next_hop = Transport.path_table[packet.destination_hash][IDX_PT_NEXT_HOP] - remaining_hops = Transport.path_table[packet.destination_hash][IDX_PT_HOPS] + path_entry = Transport.path_table.get(packet.destination_hash) + if path_entry != None: + next_hop = path_entry[IDX_PT_NEXT_HOP] + remaining_hops = path_entry[IDX_PT_HOPS] if remaining_hops > 1: # Just increase hop count and transmit @@ -2044,7 +2045,7 @@ class Transport: new_raw += struct.pack("!B", packet.hops) new_raw += packet.raw[2:] - outbound_interface = Transport.path_table[packet.destination_hash][IDX_PT_RVCD_IF] + outbound_interface = path_entry[IDX_PT_RVCD_IF] if packet.packet_type == RNS.Packet.LINKREQUEST: now = time.time() @@ -2100,7 +2101,7 @@ class Transport: if Transport.local_hops_delta != 0 and from_local_client and not to_local_client: new_raw = Transport.mangle_hops(new_raw, Transport.local_hops_delta) Transport.transmit(outbound_interface, new_raw) - with Transport.path_table_lock: Transport.path_table[packet.destination_hash][IDX_PT_TIMESTAMP] = time.time() + path_entry[IDX_PT_TIMESTAMP] = time.time() else: # TODO: There should probably be some kind of REJECT @@ -2639,10 +2640,8 @@ class Transport: if peer_identity.validate(signature, signed_data) and not link_entry[IDX_LT_VALIDATED]: RNS.log(f"Re-balancing path to {RNS.prettyhexrep(link_destination)} from link-request proof ({link_entry[IDX_LT_REM_HOPS]}->{packet.hops})", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None link_entry[IDX_LT_REM_HOPS] = packet.hops - with Transport.path_table_lock: - if link_destination in Transport.path_table: - path_entry = Transport.path_table[link_destination] - path_entry[IDX_PT_HOPS] = packet.hops + path_entry = Transport.path_table.get(link_destination) + if path_entry: path_entry[IDX_PT_HOPS] = packet.hops elif not link_entry[IDX_LT_VALIDATED]: RNS.log(f"Aborting link request proof path re-balancing for {RNS.prettyhexrep(link_destination)} on link {RNS.prettyhexrep(packet.destination_hash)} due to invalid signature", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None else: pass @@ -2709,15 +2708,14 @@ class Transport: signature = packet_data[:RNS.Identity.SIGLENGTH//8] if link.destination.identity.validate(signature, signed_data): - with Transport.path_table_lock: - if not link.rebalanced: - RNS.log(f"Re-balancing path to {RNS.prettyhexrep(link.destination.hash)} at link terminus ({link.expected_hops}->{packet.hops})", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None - link.rebalanced = time.time() - link.expected_hops = packet.hops - if link.destination.hash in Transport.path_table: - path_entry = Transport.path_table[link.destination.hash] - path_entry[IDX_PT_HOPS] = packet.hops - RNS.log(f"Path table re-balanced for {RNS.prettyhexrep(link.destination.hash)}", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None + if not link.rebalanced: + RNS.log(f"Re-balancing path to {RNS.prettyhexrep(link.destination.hash)} at link terminus ({link.expected_hops}->{packet.hops})", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None + link.rebalanced = time.time() + link.expected_hops = packet.hops + path_entry = Transport.path_table.get(link.destination.hash) + if path_entry: + path_entry[IDX_PT_HOPS] = packet.hops + RNS.log(f"Path table re-balanced for {RNS.prettyhexrep(link.destination.hash)}", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None else: RNS.log(f"Aborting path re-balancing at link terminus for {RNS.prettyhexrep(link.destination.hash)} on link {link} due to invalid signature", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None except Exception as e: RNS.log("Error while validating link request proof for path re-balancing at link terminus. The contained exception was: "+str(e), REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None From 629e4fde2d9095246952874d4b0ce3965b16d0b9 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Mon, 24 Aug 2026 17:45:29 +0200 Subject: [PATCH 02/16] Added hash map lookups for pending and active links --- RNS/Transport.py | 97 +++++++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/RNS/Transport.py b/RNS/Transport.py index 0166eb74..7fe6a70a 100755 --- a/RNS/Transport.py +++ b/RNS/Transport.py @@ -164,6 +164,8 @@ class Transport: destinations_map = {} # Destination hash map of active destinations pending_links = [] # Links that are being established active_links = [] # Links that are active + pending_links_map = {} # Link ID hash map of pending links + active_links_map = {} # Link ID hash map of active links packet_hashlist = set() # A list of packet hashes for duplicate detection packet_hashlist_prev = set() receipts = [] # Receipts of all outgoing packets for proof processing @@ -714,14 +716,18 @@ class Transport: closed_pending_links.append(link) - for closed_link in closed_pending_links: Transport.pending_links.remove(closed_link) + for closed_link in closed_pending_links: + Transport.pending_links.remove(closed_link) + Transport.pending_links_map.pop(closed_link.link_id, None) with Transport.active_links_lock: closed_links = [] for link in Transport.active_links: if link.status == RNS.Link.CLOSED: closed_links.append(link) - for closed_link in closed_links: Transport.active_links.remove(closed_link) + for closed_link in closed_links: + Transport.active_links.remove(closed_link) + Transport.active_links_map.pop(closed_link.link_id, None) Transport.links_last_checked = time.time() @@ -2572,32 +2578,29 @@ class Transport: elif packet.packet_type == RNS.Packet.DATA: if packet.destination_type == RNS.Destination.LINK: with Transport.active_links_lock: - for link in Transport.active_links: - if link.link_id == packet.destination_hash: - if link.attached_interface == packet.receiving_interface: - packet.link = link - if packet.context == RNS.Packet.CACHE_REQUEST: - cached_packet = Transport.get_cached_packet(packet.data) - if cached_packet != None: - if not cached_packet.unpack(): return - RNS.Packet(destination=link, data=cached_packet.data, - packet_type=cached_packet.packet_type, context=cached_packet.context).send() - - else: link.receive(packet) - break - - else: - # In the strange and rare case that an interface - # is partly malfunctioning, and a link-associated - # packet is being received on an interface that - # has failed sending, and transport has failed over - # to another path, we remove this packet hash from - # the filter hashlist so the link can receive the - # packet when it finally arrives over another path. - while packet.packet_hash in Transport.packet_hashlist: - Transport.packet_hashlist.remove(packet.packet_hash) - while packet.packet_hash in Transport.packet_hashlist_prev: - Transport.packet_hashlist_prev.remove(packet.packet_hash) + link = Transport.active_links_map.get(packet.destination_hash) + if link != None: + if link.attached_interface == packet.receiving_interface: + packet.link = link + if packet.context == RNS.Packet.CACHE_REQUEST: + cached_packet = Transport.get_cached_packet(packet.data) + if cached_packet != None: + if not cached_packet.unpack(): return + RNS.Packet(destination=link, data=cached_packet.data, + packet_type=cached_packet.packet_type, context=cached_packet.context).send() + + else: link.receive(packet) + + else: + # In the strange and rare case that an interface + # is partly malfunctioning, and a link-associated + # packet is being received on an interface that + # has failed sending, and transport has failed over + # to another path, we remove this packet hash from + # the filter hashlist so the link can receive the + # packet when it finally arrives over another path. + while packet.packet_hash in Transport.packet_hashlist: Transport.packet_hashlist.remove(packet.packet_hash) + while packet.packet_hash in Transport.packet_hashlist_prev: Transport.packet_hashlist_prev.remove(packet.packet_hash) else: destination = None with Transport.destinations_map_lock: @@ -2685,8 +2688,9 @@ class Transport: # Check if we can deliver it to a local pending link pending_link = None with Transport.pending_links_lock: - for link in Transport.pending_links: - if link.link_id == packet.destination_hash: + link = Transport.pending_links_map.get(packet.destination_hash) + if link != None: + # TODO: Cleanup indentation if packet.hops != link.expected_hops and link.status == RNS.Link.PENDING and Transport.ALLOW_LINK_PATH_REBALANCE: RNS.log(f"Unbalanced link path ({packet.hops}/{link.expected_hops}) detected on link {link}, validating signature for re-balancing...", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None try: @@ -2726,23 +2730,16 @@ class Transport: # for this system, and then validate the proof Transport.add_packet_hash(packet.packet_hash) pending_link = link - break if pending_link: pending_link.validate_proof(packet) elif packet.context == RNS.Packet.RESOURCE_PRF: - with Transport.active_links_lock: - for link in Transport.active_links: - if link.link_id == packet.destination_hash: - link.receive(packet) - break + link = Transport.active_links_map.get(packet.destination_hash) + if link != None: link.receive(packet) else: if packet.destination_type == RNS.Destination.LINK: - with Transport.active_links_lock: - for link in Transport.active_links: - if link.link_id == packet.destination_hash: - packet.link = link - break + link = Transport.active_links_map.get(packet.destination_hash) + if link != None: packet.link = link if len(packet.data) == RNS.PacketReceipt.EXPL_LENGTH: proof_hash = packet.data[:RNS.Identity.HASHLENGTH//8] else: proof_hash = None @@ -2945,9 +2942,13 @@ class Transport: def register_link(link): RNS.log("Registering link "+str(link), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None if link.initiator: - with Transport.pending_links_lock: Transport.pending_links.append(link) + with Transport.pending_links_lock: + Transport.pending_links.append(link) + Transport.pending_links_map[link.link_id] = link else: - with Transport.active_links_lock: Transport.active_links.append(link) + with Transport.active_links_lock: + Transport.active_links.append(link) + Transport.active_links_map[link.link_id] = link @staticmethod def activate_link(link): @@ -2956,10 +2957,14 @@ class Transport: if link in Transport.pending_links: if link.status != RNS.Link.ACTIVE: raise IOError("Invalid link state for link activation: "+str(link.status)) Transport.pending_links.remove(link) - with Transport.active_links_lock: Transport.active_links.append(link) + Transport.pending_links_map.pop(link.link_id, None) + with Transport.active_links_lock: + Transport.active_links.append(link) + Transport.active_links_map[link.link_id] = link + link.status = RNS.Link.ACTIVE - else: - RNS.log("Attempted to activate a link that was not in the pending table", RNS.LOG_ERROR) + + else: RNS.log("Attempted to activate a link that was not in the pending table", RNS.LOG_ERROR) @staticmethod def register_announce_handler(handler): From d81421dad3badb7672ac2171e253af6643c5ecdd Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Mon, 24 Aug 2026 17:58:29 +0200 Subject: [PATCH 03/16] Avoid additional packet hashing under lock in inbound --- RNS/Packet.py | 10 +++++++--- RNS/Transport.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/RNS/Packet.py b/RNS/Packet.py index 8c6676f9..6ff842f4 100755 --- a/RNS/Packet.py +++ b/RNS/Packet.py @@ -350,6 +350,10 @@ class Packet: def getTruncatedHash(self): return RNS.Identity.truncated_hash(self.get_hashable_part()) + @property + def truncated_packet_hash(self): + return self.packet_hash[:RNS.Reticulum.TRUNCATED_HASHLENGTH//8] + def get_hashable_part(self): hashable_part = bytes([self.raw[0] & 0b00001111]) if self.header_type == Packet.HEADER_2: hashable_part += self.raw[(RNS.Identity.TRUNCATED_HASHLENGTH//8)+2:] @@ -380,7 +384,7 @@ class Packet: class ProofDestination: def __init__(self, packet): - self.hash = packet.get_hash()[:RNS.Reticulum.TRUNCATED_HASHLENGTH//8]; + self.hash = packet.truncated_packet_hash; self.type = RNS.Destination.SINGLE def encrypt(self, plaintext): return plaintext @@ -405,8 +409,8 @@ class PacketReceipt: # Creates a new packet receipt from a sent packet def __init__(self, packet): - self.hash = packet.get_hash() - self.truncated_hash = packet.getTruncatedHash() + self.hash = packet.packet_hash + self.truncated_hash = packet.truncated_packet_hash self.sent = True self.sent_at = time.time() self.proved = False diff --git a/RNS/Transport.py b/RNS/Transport.py index 7fe6a70a..749291fc 100755 --- a/RNS/Transport.py +++ b/RNS/Transport.py @@ -2103,7 +2103,7 @@ class Transport: outbound_interface, # 1: Outbound interface time.time() ] # 2: Timestamp - with Transport.reverse_table_lock: Transport.reverse_table[packet.getTruncatedHash()] = reverse_entry + with Transport.reverse_table_lock: Transport.reverse_table[packet.truncated_packet_hash] = reverse_entry if Transport.local_hops_delta != 0 and from_local_client and not to_local_client: new_raw = Transport.mangle_hops(new_raw, Transport.local_hops_delta) Transport.transmit(outbound_interface, new_raw) From 5e013464da0c85f147ab8512edb93507e34e1df4 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Mon, 24 Aug 2026 20:42:57 +0200 Subject: [PATCH 04/16] Added throughput benchmarker --- tests/throughput.py | 883 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 883 insertions(+) create mode 100644 tests/throughput.py diff --git a/tests/throughput.py b/tests/throughput.py new file mode 100644 index 00000000..7112eaf0 --- /dev/null +++ b/tests/throughput.py @@ -0,0 +1,883 @@ +#!/usr/bin/env python3 +# +# Usage examples: +# +# python3 tests/transport_throughput.py +# python3 tests/transport_throughput.py --scenario transit_single_135 +# python3 tests/transport_throughput.py --mode inline --runs 5 +# python3 tests/transport_throughput.py --list-scenarios +# + +import unittest + +import os +import sys +import time +import gc +import struct +import platform +import tempfile +import statistics + +# Ensure that the Reticulum tree this suite lives in is the one being +# benchmarked, even if a different version of Reticulum is installed in +# site-packages. +_SUITE_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if os.path.isdir(os.path.join(_SUITE_ROOT, "RNS")): + sys.path.insert(0, _SUITE_ROOT) + +import RNS + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +BENCHMARK_CONFIG = { + "scenarios": None, # None = all, or a list of scenario names + "mode": "both", # "inline", "drainer" or "both" + "runs": 3, # measurement runs per scenario/mode, median is reported +} + +SCENARIO_DESCRIPTIONS = { + "transit_single_135": "SINGLE transit relay, 135 B payload, 3-hop path", + "transit_single_475": "SINGLE transit relay, 475 B payload, 3-hop path", + "transit_single_1024": "SINGLE transit relay, 1 KiB payload, 3-hop path", + "transit_single_16384": "SINGLE transit relay, 16 KiB payload, 3-hop path", + "transit_single_final": "SINGLE transit relay, final hop, header strip, 135 B", + "transit_link_135": "LINK transit relay via link table, cross-interface, 135 B", + "transit_link_475": "LINK transit relay via link table, cross-interface, 475 B", + "transit_link_1024": "LINK transit relay via link table, cross-interface, 1024 B", + "transit_link_16384": "LINK transit relay via link table, cross-interface, 16384 B", + "terminus_link": "LINK terminus delivery, token decrypt, 135 B", + "terminus_single": "SINGLE local delivery, ephemeral-key decrypt, 135 B", + "announce_ingress": "Announce ingress, fresh destinations, validation + path insert", + "outbound_path": "Outbound insertion into transport, known 3-hop path, 135 B", +} + +# Default packet counts per scenario and mode. +DEFAULT_PACKETS = { + "transit_single_135": {"inline": 20000, "drainer": 20000}, + "transit_single_475": {"inline": 20000, "drainer": 20000}, + "transit_single_1024": {"inline": 20000, "drainer": 20000}, + "transit_single_16384": {"inline": 10000, "drainer": 10000}, + "transit_single_final": {"inline": 20000, "drainer": 20000}, + "transit_link_135": {"inline": 20000, "drainer": 20000}, + "transit_link_475": {"inline": 20000, "drainer": 20000}, + "transit_link_1024": {"inline": 20000, "drainer": 20000}, + "transit_link_16384": {"inline": 10000, "drainer": 10000}, + "terminus_link": {"inline": 6000, "drainer": 6000}, + "terminus_single": {"inline": 3000, "drainer": 3000}, + "announce_ingress": {"inline": 1000, "drainer": 1000}, + "outbound_path": {"inline": 8000, "drainer": 0}, +} + +INLINE_SAMPLE_STEP = 64 # sample a timing point every N packets for p50/p95 +DRAINER_CHUNK = 256 # packets fed per drainer backpressure step + + +class BenchmarkInterface(RNS.Interfaces.Interface.Interface): + HW_MTU = 1048576 + BITRATE = 1_000_000_000 + + def __init__(self, name="bench"): + super().__init__() + self.name = name + self.IN = True + self.OUT = True + self.online = True + self.bitrate = self.BITRATE + self.HW_MTU = BenchmarkInterface.HW_MTU + self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL + self.gravity = 0 + self.ifac_size = 0 + self.ifac_key = None + self.ifac_identity = None + self.parent_interface = None + self.ingress_control = False + self.announce_rate_target = RNS.Interfaces.Interface.Interface.DEFAULT_AR_TARGET + self.announce_rate_grace = RNS.Interfaces.Interface.Interface.DEFAULT_AR_GRACE + self.announce_rate_penalty = RNS.Interfaces.Interface.Interface.DEFAULT_AR_PENALTY + + def process_outgoing(self, data): + self.txb += len(data) + + def __str__(self): + return f"BenchmarkInterface[{self.name}]" + + +class Scenario: + kind = "ingress" # "ingress" or "outbound" + + def __init__(self, name, description, interface, frames, packets, runs, + inline_offset, drainer_offset, size, completion_counter=None, + fresh_per_run=False): + self.name = name + self.description = description + self.interface = interface + self.frame_size = size + self.frames = frames # pool of unique raw frames + self.packets_inline = packets.get("inline", 0) + self.packets_drainer = packets.get("drainer", 0) + self.runs = runs + self.inline_offset = inline_offset + self.drainer_offset = drainer_offset + self.completion_counter = completion_counter + # When true, each run uses a dedicated fresh slice of frames + # (required where processing is not idempotent, e.g. announces). + # When false, runs reuse the same pool with the duplicate filter + # reset in between. + self.fresh_per_run = fresh_per_run + + def can_drainer(self): + return self.packets_drainer > 0 and self.completion_counter is not None + + def n_for_mode(self, mode): + if mode == "inline": return self.packets_inline + if mode == "drainer": return self.packets_drainer + + +class CountingDestination(RNS.Destination): + """A destination that counts successful deliveries, for use as a + completion counter in drainer mode.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.delivered = 0 + + def receive(self, packet): + result = super().receive(packet) + if result: + self.delivered += 1 + return result + + +def _register_link(link): + with RNS.Transport.active_links_lock: + RNS.Transport.active_links.append(link) + if hasattr(RNS.Transport, "active_links_map"): + RNS.Transport.active_links_map[link.link_id] = link + + +def _make_minimal_link(instance, interface): + """Constructs a minimal, active Link object suitable for benchmark delivery.""" + import importlib + _link_mod = importlib.import_module("RNS.Link") + + link = RNS.Link.__new__(RNS.Link) + link.mode = RNS.Link.MODE_DEFAULT + link.rtt = 0.1 + link.mtu = RNS.Reticulum.MTU + link.establishment_cost = 0 + link.establishment_rate = None + link.expected_rate = None + link.callbacks = _link_mod.LinkCallbacks() + link.resource_strategy = RNS.Link.ACCEPT_NONE + link.last_resource_window = None + link.last_resource_eifr = None + link.outgoing_resources = [] + link.incoming_resources = [] + link.pending_requests = [] + link.last_inbound = 0 + link.last_outbound = 0 + link.last_keepalive = 0 + link.last_proof = 0 + link.last_data = 0 + link.tx = 0 + link.rx = 0 + link.txbytes = 0 + link.rxbytes = 0 + link.rssi = None + link.snr = None + link.q = None + link.traffic_timeout_factor = RNS.Link.TRAFFIC_TIMEOUT_FACTOR + link.keepalive_timeout_factor = RNS.Link.KEEPALIVE_TIMEOUT_FACTOR + link.keepalive = RNS.Link.KEEPALIVE + link.stale_time = RNS.Link.STALE_TIME + link.watchdog_lock = False + link.status = RNS.Link.ACTIVE + link.activated_at = time.time() + link.type = RNS.Destination.LINK + link.owner = instance + link.initiator = False + link.expected_hops = 1 + link.rebalanced = None + link.attached_interface = interface + link._channel = None + link._Link__remote_identity = None + link._Link__track_phy_stats = False + link.derived_key = os.urandom(64) + link.token = RNS.Cryptography.Token(link.derived_key) + link.link_id = os.urandom(RNS.Reticulum.TRUNCATED_HASHLENGTH//8) + link.hash = link.link_id + link.hexhash = link.link_id.hex() + from RNS.Cryptography.Proxies import Ed25519PrivateKeyProxy + link.sig_prv = Ed25519PrivateKeyProxy.from_private_bytes(link.derived_key[:32]) + + class _DestinationShim: + pass + + link.destination = _DestinationShim() + link.destination.type = RNS.Destination.LINK + link.destination.proof_strategy = RNS.Destination.PROVE_NONE + _dest_mod = importlib.import_module("RNS.Destination") + link.destination.callbacks = _dest_mod.Callbacks() + link.destination.status = RNS.Link.ACTIVE + link.destination.last_outbound = 0 + link.destination.tx = 0 + link.destination.txbytes = 0 + link.destination.attached_interface = interface + link.destination.mtu = RNS.Reticulum.MTU + link.destination.rssi = None + link.destination.snr = None + link.destination.q = None + link.destination.rtt = RNS.Link.TRAFFIC_TIMEOUT_MIN_MS/1000 + link.destination.traffic_timeout_factor = 1.0 + + return link + + +def _transit_single_frame(interface, dst_h, payload): + """One HEADER_2|TRANSPORT|SINGLE|DATA frame addressed to this instance.""" + flags = (RNS.Packet.HEADER_2 << 6) | (RNS.Transport.TRANSPORT << 4) \ + | (RNS.Destination.SINGLE << 2) | RNS.Packet.DATA + return (struct.pack("!B", flags) + struct.pack("!B", 1) + + RNS.Transport.identity.hash + dst_h + + bytes([RNS.Packet.NONE]) + payload) + + +def _transit_link_frame(interface, link_id, payload): + """One HEADER_2|TRANSPORT|LINK|DATA frame addressed to this instance.""" + flags = (RNS.Packet.HEADER_2 << 6) | (RNS.Transport.TRANSPORT << 4) \ + | (RNS.Destination.LINK << 2) | RNS.Packet.DATA + return (struct.pack("!B", flags) + struct.pack("!B", 1) + + RNS.Transport.identity.hash + link_id + + bytes([RNS.Packet.NONE]) + payload) + + +def _terminus_link_frame(link, payload): + """One HEADER_1|BROADCAST|LINK|DATA frame for a local active link, + encrypted with the link token.""" + flags = (RNS.Packet.HEADER_1 << 6) | (RNS.Transport.BROADCAST << 4) \ + | (RNS.Destination.LINK << 2) | RNS.Packet.DATA + ciphertext = link.encrypt(payload) + return (struct.pack("!B", flags) + struct.pack("!B", 0) + + link.link_id + bytes([RNS.Packet.NONE]) + ciphertext) + + +def _payload(i, size, marker): + """Deterministic, unique payload of `size` bytes.""" + return i.to_bytes(8, "big") + bytes([marker]) * (size - 8) + + +def build_scenarios(instance, interface, interface_b, requested=None): + """Builds all benchmark scenarios and returns a dict name -> Scenario.""" + scenarios = {} + runs = BENCHMARK_CONFIG["runs"] + + def _slices(packets, fresh=False): + inline_n = packets.get("inline", 0) + drainer_n = packets.get("drainer", 0) + if fresh: + # Fresh frames per run for both modes + total = (inline_n + drainer_n) * runs + return total, inline_n, drainer_n, 0, inline_n * runs + else: + # One re-usable pool of the largest mode's frame count + total = max(inline_n, drainer_n) + return total, inline_n, drainer_n, 0, 0 + + # --- SINGLE transit relay, several payload sizes ----------------------- + sizes = [("135", 135), ("475", 475), ("1024", 1024), ("16384", 16384)] + for suffix, size in sizes: + name = f"transit_single_{suffix}" + total, inline_n, drainer_n, off_i, off_d = _slices(DEFAULT_PACKETS[name]) + dst_h = RNS.Cryptography.hkdf(length=16, + derive_from=f"T{suffix}dst".encode(), + salt=b"bench", context=None) + next_hop = RNS.Cryptography.hkdf(length=16, + derive_from=f"T{suffix}nh".encode(), + salt=b"bench", context=None) + now = time.time() + with RNS.Transport.path_table_lock: + RNS.Transport.path_table[dst_h] = [ + now, next_hop, 3, now + 3600, [], interface, bytes(32) + ] + frames = [_transit_single_frame(interface, dst_h, + _payload(i, size, 0x57)) + for i in range(total)] + scenarios[name] = Scenario( + name, + f"SINGLE transit relay, {size} B payload, 3-hop path", + interface, frames, DEFAULT_PACKETS[name], runs, + off_i, off_d, size, + completion_counter=lambda: RNS.Transport.tx_packets, + ) + + # --- SINGLE transit relay, final hop (header strip) -------------------- + name = "transit_single_final" + total, inline_n, drainer_n, off_i, off_d = _slices(DEFAULT_PACKETS[name]) + dst_h = RNS.Cryptography.hkdf(length=16, derive_from=b"TFdst", + salt=b"bench", context=None) + next_hop = RNS.Cryptography.hkdf(length=16, derive_from=b"TFnh", + salt=b"bench", context=None) + now = time.time() + with RNS.Transport.path_table_lock: + RNS.Transport.path_table[dst_h] = [ + now, next_hop, 1, now + 3600, [], interface, bytes(32) + ] + frames = [_transit_single_frame(interface, dst_h, + _payload(i, 135, 0x46)) + for i in range(total)] + scenarios[name] = Scenario( + name, + "SINGLE transit relay, final hop, transport header strip, 135 B", + interface, frames, DEFAULT_PACKETS[name], runs, + off_i, off_d, 135, + completion_counter=lambda: RNS.Transport.tx_packets, + ) + + # --- LINK transit relay (cross-interface) ------------------------------ + sizes = [("135", 135), ("475", 475), ("1024", 1024), ("16384", 16384)] + for suffix, size in sizes: + name = f"transit_link_{suffix}" + + total, inline_n, drainer_n, off_i, off_d = _slices(DEFAULT_PACKETS[name]) + link_id = RNS.Cryptography.hkdf(length=16, derive_from=b"Llink", + salt=b"bench", context=None) + now = time.time() + with RNS.Transport.link_table_lock: + # timestamp, next-hop transport id, outbound iface, remaining hops, + # received-on iface, taken hops, original destination hash, + # validated, proof timeout + RNS.Transport.link_table[link_id] = [ + now, RNS.Transport.identity.hash, interface_b, 2, + interface, 2, os.urandom(16), True, now + 60 + ] + frames = [_transit_link_frame(interface, link_id, + _payload(i, size, 0x4C)) + for i in range(total)] + scenarios[name] = Scenario( + name, + f"LINK transit relay via link table, cross-interface, {size} B", + interface, frames, DEFAULT_PACKETS[name], runs, + off_i, off_d, size, + completion_counter=lambda: RNS.Transport.tx_packets, + ) + + # --- LINK terminus delivery -------------------------------------------- + name = "terminus_link" + total, inline_n, drainer_n, off_i, off_d = _slices(DEFAULT_PACKETS[name]) + link = _make_minimal_link(instance, interface) + _register_link(link) + frames = [_terminus_link_frame(link, _payload(i, 135, 0x6C)) + for i in range(total)] + scenarios[name] = Scenario( + name, + "LINK terminus delivery, token decrypt, no app callback, 135 B", + interface, frames, DEFAULT_PACKETS[name], runs, + off_i, off_d, 135, + completion_counter=lambda: link.rx, + ) + + # --- SINGLE local delivery --------------------------------------------- + name = "terminus_single" + total, inline_n, drainer_n, off_i, off_d = _slices(DEFAULT_PACKETS[name]) + identity = RNS.Identity() + # The Destination initialiser automatically registers IN destinations + # with the transport core. + destination = CountingDestination(identity, RNS.Destination.IN, + RNS.Destination.SINGLE, + "bench", "terminus") + frames = [] + for i in range(total): + packet = RNS.Packet(destination, _payload(i, 135, 0x53), + RNS.Packet.DATA, create_receipt=False) + packet.pack() + frames.append(packet.raw) + scenarios[name] = Scenario( + name, + "SINGLE local delivery, ephemeral-key decrypt, 135 B", + interface, frames, DEFAULT_PACKETS[name], runs, + off_i, off_d, 135, + completion_counter=lambda: destination.delivered, + ) + + # --- Announce ingress ---------------------------------------------------- + name = "announce_ingress" + total, inline_n, drainer_n, off_i, off_d = _slices(DEFAULT_PACKETS[name], + fresh=True) + name_hash = RNS.Identity.full_hash(b"rns.throughput.bench")[ + :RNS.Identity.NAME_HASH_LENGTH//8] + frames = []; tsize = 0 + for i in range(total): + ann_identity = RNS.Identity() + dst_h = RNS.Identity.full_hash(name_hash + ann_identity.hash)[ + :RNS.Reticulum.TRUNCATED_HASHLENGTH//8] + random_hash = os.urandom(5) + int(time.time() + i).to_bytes(5, "big") + signed_data = dst_h + ann_identity.get_public_key() + name_hash \ + + random_hash + b"" + signature = ann_identity.sign(signed_data) + announce_data = ann_identity.get_public_key() + name_hash \ + + random_hash + b"" + signature + flags = (RNS.Packet.HEADER_1 << 6) \ + | (RNS.Destination.SINGLE << 2) | RNS.Packet.ANNOUNCE + fbs = struct.pack("!B", flags) + struct.pack("!B", 0) + dst_h + bytes([RNS.Packet.NONE]) + announce_data + frames.append(fbs) + tsize += len(fbs) + + scenarios[name] = Scenario( + name, + "Announce ingress, fresh destinations, validation + path insert", + interface, frames, DEFAULT_PACKETS[name], runs, + off_i, off_d, int(tsize/total), + completion_counter=lambda: len(RNS.Transport.path_table), + fresh_per_run=True, + ) + + # --- Outbound insertion into transport ---------------------------------- + name = "outbound_path" + packets = DEFAULT_PACKETS[name] + inline_n = packets.get("inline", 0) + total = inline_n # pool is re-used across runs + dst_h = RNS.Cryptography.hkdf(length=16, derive_from=b"Odst", + salt=b"bench", context=None) + next_hop = RNS.Cryptography.hkdf(length=16, derive_from=b"Onh", + salt=b"bench", context=None) + now = time.time() + with RNS.Transport.path_table_lock: + RNS.Transport.path_table[dst_h] = [ + now, next_hop, 3, now + 3600, [], interface, bytes(32) + ] + remote_id = RNS.Identity(create_keys=False) + remote_id.load_public_key(os.urandom(RNS.Identity.KEYSIZE//8)) + outbound_destination = RNS.Destination(remote_id, RNS.Destination.OUT, + RNS.Destination.SINGLE, + "bench", "outbound") + outbound_destination.hash = dst_h + outbound_destination.hexhash = dst_h.hex() + frames = [] + for i in range(total): + packet = RNS.Packet(outbound_destination, _payload(i, 135, 0x4F), + RNS.Packet.DATA, create_receipt=False) + packet.pack() + frames.append(packet) + outbound = Scenario( + name, + "Outbound insertion into transport, known 3-hop path, 135 B", + interface, frames, packets, runs, + 0, 0, 135, + completion_counter=None, + ) + outbound.kind = "outbound" + scenarios[name] = outbound + + if requested is not None: + missing = [s for s in requested if s not in scenarios] + if missing: + raise KeyError(f"Unknown scenario(s): {', '.join(missing)}. " + f"Available: {', '.join(sorted(scenarios))}") + scenarios = {name: scenarios[name] for name in requested} + + return scenarios + + +def _reset_transport_state(): + RNS.Transport.packet_hashlist = set() + RNS.Transport.packet_hashlist_prev = set() + RNS.Transport.reverse_table = {} + + +def _percentile(sorted_values, p): + if not sorted_values: + return 0.0 + index = min(len(sorted_values) - 1, int(len(sorted_values) * p)) + return sorted_values[index] + + +def _feed(frame, interface): + RNS.Transport.preprocess_inbound(frame, interface) + +def _bench_inline(interface, frames, n, runs, offset, fresh_per_run=False): + """Synchronous benchmark: process each frame in the calling thread, + bypassing the inbound queue. Returns (run_means_us, samples_us).""" + run_means = [] + all_samples = [] + + # Bypass the inbound queue so processing happens entirely in the + # calling thread; restore the previous setting afterwards. + previous_queue_state = RNS.Transport.USE_INBOUND_QUEUE + RNS.Transport.USE_INBOUND_QUEUE = False + try: + for r in range(runs): + _reset_transport_state() + gc.collect() + gc.disable() + + base = offset + (r * n if fresh_per_run else 0) + t0 = time.perf_counter() + prev = None + samples = [] + step = INLINE_SAMPLE_STEP + next_sample = step + for i in range(n): + _feed(frames[base + i], interface) + if i == next_sample: + now = time.perf_counter() + if prev is not None: + samples.append((now - prev) / step) + prev = now + next_sample += step + dt = time.perf_counter() - t0 + gc.enable() + + run_means.append(dt / n * 1e6) + all_samples.extend(samples) + finally: + RNS.Transport.USE_INBOUND_QUEUE = previous_queue_state + + return run_means, [s * 1e6 for s in all_samples] + + +def _bench_drainer(scenario, n, runs): + """Feed frames through the inbound queue from the calling thread + while the drainer thread processes them. Backpressure is applied + via the scenario completion counter, so no frames are dropped and + the pipeline stays saturated.""" + run_means = [] + + # Ensure the inbound queue and drainer are used; restore afterwards. + previous_queue_state = RNS.Transport.USE_INBOUND_QUEUE + RNS.Transport.USE_INBOUND_QUEUE = True + try: + for r in range(runs): + _reset_transport_state() + gc.collect() + gc.disable() + + base = scenario.drainer_offset \ + + (r * n if scenario.fresh_per_run else 0) + baseline = scenario.completion_counter() + fed = 0 + t0 = time.perf_counter() + chunk = DRAINER_CHUNK + while fed < n: + batch = min(chunk, n - fed) + for i in range(fed, fed + batch): + _feed(scenario.frames[base + i], scenario.interface) + fed += batch + target = baseline + fed + while scenario.completion_counter() < target: + time.sleep(0.0002) + dt = time.perf_counter() - t0 + gc.enable() + + run_means.append(dt / n * 1e6) + finally: + RNS.Transport.USE_INBOUND_QUEUE = previous_queue_state + + return run_means + + +def _bench_outbound(packets, n, runs, offset): + """Synchronous benchmark of the outbound path: insert each packet + into transport for a known path via Transport.outbound(). The packet + pool is re-used across runs.""" + run_means = [] + for r in range(runs): + gc.collect() + gc.disable() + t0 = time.perf_counter() + for i in range(n): + RNS.Transport.outbound(packets[offset + i]) + dt = time.perf_counter() - t0 + gc.enable() + run_means.append(dt / n * 1e6) + return run_means + + +# --------------------------------------------------------------------------- +# Output helpers +# --------------------------------------------------------------------------- + +def _fmt_us(value): + return f"{value:>10.2f}" + + +def _fmt_pps(value): + return f"{value:>12,.0f}" + + +def print_environment_header(): + tree_fastpath = "present" if hasattr(RNS.Transport, "_fastpath") else "absent" + print("=" * 40) + print("Reticulum Transport Throughput Benchmark") + print("=" * 40) + print(f" RNS version : {RNS.__version__}") + print(f" Mode : {'compiled' if RNS.compiled else 'interpreted'}") + print(f" crypto : {RNS.Cryptography.backend()}") + print(f" transport : enabled") + print(f" fast path : {tree_fastpath} (informational)") + print(f" python : {platform.python_version()}") + print(f" platform : {platform.platform()}") + print(f" machine : {platform.machine()}, cpus: {os.cpu_count()}") + print() + + +def print_scenario_table(scenario, results): + """results: dict mode -> dict(runs, mean_us, pps, stdev, p50, p95).""" + print(f"Scenario: {scenario.name} - {scenario.description}") + print(f" {'mode':<9}{'n':>8}{'runs':>6}{'mean µs':>13}{'pps':>16}" + f"{'p50':>11}{'p95':>11}") + print(" " + "-" * 75) + for mode in ("inline", "drainer"): + if mode not in results: + continue + r = results[mode] + n = scenario.n_for_mode(mode) + p50 = f"{r['p50']:>9.2f} µs" if r["p50"] is not None else "-" + p95 = f"{r['p95']:>9.2f} µs" if r["p95"] is not None else "-" + spread = "" + if r.get("stdev") is not None: + spread = f" ±{r['stdev']:.2f}" + if mode == "inline": mode = "direct" + print(f" {mode:<9}{n:>8}{r['runs']:>6}{_fmt_us(r['mean_us']):>13}" + f"{_fmt_pps(r['pps']):>16}{p50:>11}{p95:>11} " + f"({r['runs']} runs, median{spread})") + + print() + for mode in ("inline", "drainer"): + if mode not in results: + continue + + r = results[mode] + tp = r['pps']*scenario.frame_size*8 + print(f"{mode:<8} : {RNS.prettyspeed(tp)}") + + print() + + +def print_pps_matrix(rows): + """rows: list of (scenario_name, inline_pps, drainer_pps, fsize)""" + if not rows: + return + print("-" * 72) + print("Transport Core Throughput - PPS matrix (median of runs)") + print(f" {'scenario':<22}{'direct':>14}{'drainer':>14}") + print(" " + "-" * 70) + for name, inline_pps, drainer_pps, fsize in rows: + i = f"{inline_pps:>12,.0f}" if inline_pps else "-" + d = f"{drainer_pps:>12,.0f}" if drainer_pps else "-" + itp = RNS.prettyspeed(inline_pps*fsize*8) if inline_pps else "-" + dtp = RNS.prettyspeed(drainer_pps*fsize*8) if drainer_pps else "-" + print(f" {name:<22}{i:>14}{d:>14}{itp:>16} / {dtp}") + print() + + +# --------------------------------------------------------------------------- +# Test suite +# --------------------------------------------------------------------------- + +_instance = None +_interfaces = None +_scenarios = None +_config_dir = None + + +def _start_transport(): + global _instance, _interfaces, _scenarios, _config_dir + + _config_dir = tempfile.mkdtemp(prefix="rns-transport-bench-") + config_dir = _config_dir + with open(os.path.join(config_dir, "config"), "w") as fh: + fh.write( + "[reticulum]\n" + " enable_transport = yes\n" + " share_instance = No\n" + " panic_on_interface_error = No\n" + "\n" + "[logging]\n" + " loglevel = 0\n" + "\n" + "[interfaces]\n" + ) + + _instance = RNS.Reticulum(configdir=config_dir, + loglevel=RNS.LOG_CRITICAL, + logdest=lambda *a, **k: None) + + while not RNS.Transport.ready: + time.sleep(0.05) + + interface = BenchmarkInterface("line-a") + interface_b = BenchmarkInterface("line-b") + RNS.Transport.add_interface(interface) + RNS.Transport.add_interface(interface_b) + RNS.Transport.prioritize_interfaces() + _interfaces = (interface, interface_b) + + requested = BENCHMARK_CONFIG["scenarios"] + _scenarios = build_scenarios(_instance, interface, interface_b, + requested=requested) + + +class TestTransportThroughput(unittest.TestCase): + + @classmethod + def setUpClass(cls): + if _scenarios is None: + _start_transport() + + @classmethod + def tearDownClass(cls): + if RNS.Transport._should_run: + RNS.Transport.exit_handler() + if _config_dir is not None: + import shutil + shutil.rmtree(_config_dir, ignore_errors=True) + + def _run_scenario_matrix(self, scenario_names): + rows = [] + # Only include scenarios selected via the command line + scenario_names = [name for name in scenario_names + if name in _scenarios] + for name in scenario_names: + scenario = _scenarios[name] + results = {} + + mode_filter = BENCHMARK_CONFIG["mode"] + runs = BENCHMARK_CONFIG["runs"] + + if scenario.kind == "outbound": + # The outbound path is synchronous; it only has an + # inline-equivalent measurement, and follows the mode + # filter's "inline" selector. + if mode_filter in ("inline", "both"): + run_means = _bench_outbound(scenario.frames, + scenario.packets_inline, + runs, 0) + mean_us = statistics.median(run_means) + results["inline"] = { + "runs": runs, + "mean_us": mean_us, + "pps": 1e6 / mean_us, + "stdev": statistics.stdev(run_means) if len(run_means) > 2 else None, + "p50": None, + "p95": None, + } + else: + if mode_filter in ("inline", "both") and scenario.packets_inline: + n = scenario.packets_inline + run_means, samples = _bench_inline(scenario.interface, + scenario.frames, n, runs, + scenario.inline_offset, + scenario.fresh_per_run) + mean_us = statistics.median(run_means) + stdev = statistics.stdev(run_means) if len(run_means) > 2 else None + sorted_samples = sorted(samples) + results["inline"] = { + "runs": runs, + "mean_us": mean_us, + "pps": 1e6 / mean_us, + "stdev": stdev, + "p50": _percentile(sorted_samples, 0.50), + "p95": _percentile(sorted_samples, 0.95), + } + + if mode_filter in ("drainer", "both") and scenario.can_drainer(): + n = scenario.packets_drainer + run_means = _bench_drainer(scenario, n, runs) + mean_us = statistics.median(run_means) + stdev = statistics.stdev(run_means) if len(run_means) > 2 else None + results["drainer"] = { + "runs": runs, + "mean_us": mean_us, + "pps": 1e6 / mean_us, + "stdev": stdev, + "p50": None, + "p95": None, + } + + if results: + print_scenario_table(scenario, results) + rows.append((name, + results.get("inline", {}).get("pps"), + results.get("drainer", {}).get("pps"), + scenario.frame_size)) + + print_pps_matrix(rows) + + def test_01_transit_throughput(self): + print("") + print_environment_header() + self._run_scenario_matrix([ + "transit_single_135", + "transit_single_475", + "transit_single_1024", + "transit_single_16384", + "transit_single_final", + "transit_link_135", + "transit_link_475", + "transit_link_1024", + "transit_link_16384", + ]) + + def test_02_delivery_throughput(self): + print("") + self._run_scenario_matrix([ + "terminus_link_135", + "terminus_link_475", + "terminus_link_1024", + "terminus_link_16384", + "terminus_single", + "announce_ingress", + "outbound_path", + ]) + + +def _usage(): + return ( + "\nUsage: python3 tests/transport_throughput.py [options]\n" + "\n" + "Options:\n" + " -s, --scenario NAME Run only the named scenario (repeatable)\n" + " --mode MODE Measurement mode: inline, drainer or both\n" + " (default: both)\n" + " --runs N Measurement runs per scenario/mode, median\n" + " is reported (default: 3)\n" + " --list-scenarios List available scenarios and exit\n" + " -h, --help Show this help and exit\n" + ) + + +if __name__ == "__main__": + argv = sys.argv[1:] + rest = [] + i = 0 + while i < len(argv): + arg = argv[i] + if arg in ("-s", "--scenario"): + BENCHMARK_CONFIG["scenarios"] = BENCHMARK_CONFIG["scenarios"] or [] + BENCHMARK_CONFIG["scenarios"].append(argv[i + 1]) + i += 2 + elif arg == "--mode": + mode = argv[i + 1] + if mode not in ("inline", "drainer", "both"): + raise SystemExit(f"Invalid mode '{mode}'" + _usage()) + BENCHMARK_CONFIG["mode"] = mode + i += 2 + elif arg == "--runs": + BENCHMARK_CONFIG["runs"] = max(1, int(argv[i + 1])) + i += 2 + elif arg == "--list-scenarios": + print("\nAvailable scenarios:") + for name in sorted(SCENARIO_DESCRIPTIONS): + print(f" {name:<22} {SCENARIO_DESCRIPTIONS[name]}") + raise SystemExit(0) + elif arg in ("-h", "--help"): + raise SystemExit(_usage()) + else: + rest.append(arg) + i += 1 + + unittest.main(argv=[sys.argv[0]] + rest, verbosity=2) + From 77f763258441fb9ce71db084703b64608559211d Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Mon, 24 Aug 2026 20:52:23 +0200 Subject: [PATCH 05/16] Avoid extra epoll modifies when EPOLLOUT already set --- RNS/Interfaces/BackboneInterface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RNS/Interfaces/BackboneInterface.py b/RNS/Interfaces/BackboneInterface.py index 27e22bb7..d503eea4 100644 --- a/RNS/Interfaces/BackboneInterface.py +++ b/RNS/Interfaces/BackboneInterface.py @@ -797,8 +797,9 @@ class BackboneClientInterface(Interface): def process_outgoing(self, data): if self.online and not self.detached: try: + buffer_was_empty = len(self.transmit_buffer) == 0 self.transmit_buffer += bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG]) - BackboneInterface.tx_ready(self) + if buffer_was_empty: BackboneInterface.tx_ready(self) except Exception as e: RNS.log("Exception occurred while transmitting via "+str(self)+", tearing down interface", RNS.LOG_ERROR) From 7e197542e52fe6af7cf4ac25bac0304b9710247b Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Mon, 24 Aug 2026 21:11:22 +0200 Subject: [PATCH 06/16] Updated througput bench --- tests/throughput.py | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/tests/throughput.py b/tests/throughput.py index 7112eaf0..ac183e81 100644 --- a/tests/throughput.py +++ b/tests/throughput.py @@ -651,6 +651,7 @@ def print_scenario_table(scenario, results): r = results[mode] tp = r['pps']*scenario.frame_size*8 + if mode == "inline": mode = "direct" print(f"{mode:<8} : {RNS.prettyspeed(tp)}") print() @@ -661,7 +662,7 @@ def print_pps_matrix(rows): if not rows: return print("-" * 72) - print("Transport Core Throughput - PPS matrix (median of runs)") + print("Transport Throughput - PPS matrix (median of runs)") print(f" {'scenario':<22}{'direct':>14}{'drainer':>14}") print(" " + "-" * 70) for name, inline_pps, drainer_pps, fsize in rows: @@ -881,3 +882,42 @@ if __name__ == "__main__": unittest.main(argv=[sys.argv[0]] + rest, verbosity=2) +# No Fastpath: + +# Transport Core Throughput - PPS matrix (median of runs) +# scenario direct drainer +# ---------------------------------------------------------------------- +# transit_single_135 247,667 162,090 267.48 Mbps / 175.06 Mbps +# transit_single_475 233,514 159,717 887.35 Mbps / 606.92 Mbps +# transit_single_1024 211,443 175,994 1.73 Gbps / 1.44 Gbps +# transit_single_16384 78,963 72,532 10.35 Gbps / 9.51 Gbps +# transit_single_final 240,855 112,702 260.12 Mbps / 121.72 Mbps +# transit_link_135 266,614 220,571 287.94 Mbps / 238.22 Mbps +# transit_link_475 256,347 152,410 974.12 Mbps / 579.16 Mbps +# transit_link_1024 228,080 194,444 1.87 Gbps / 1.59 Gbps +# transit_link_16384 82,149 73,720 10.77 Gbps / 9.66 Gbps + +# terminus_single 29,764 28,308 32.15 Mbps / 30.57 Mbps +# announce_ingress 7,394 7,150 9.88 Mbps / 9.55 Mbps +# outbound_path 872,747 - 942.57 Mbps / - +# ---------------------------------------------------------------------- + + +# Fastpath: + +# Transport Core Throughput - PPS matrix (median of runs) +# scenario direct drainer +# ---------------------------------------------------------------------- +# transit_single_135 407,561 411,426 440.17 Mbps / 444.34 Mbps +# transit_single_475 378,867 377,964 1.44 Gbps / 1.44 Gbps +# transit_single_1024 320,878 325,910 2.63 Gbps / 2.67 Gbps +# transit_single_16384 94,265 94,357 12.36 Gbps / 12.37 Gbps +# transit_single_final 395,245 399,247 426.86 Mbps / 431.19 Mbps +# transit_link_135 453,216 451,863 489.47 Mbps / 488.01 Mbps +# transit_link_475 417,013 417,294 1.58 Gbps / 1.59 Gbps +# transit_link_1024 347,548 353,783 2.85 Gbps / 2.90 Gbps +# transit_link_16384 96,682 96,871 12.67 Gbps / 12.70 Gbps + +# terminus_single 29,451 25,862 31.81 Mbps / 27.93 Mbps +# announce_ingress 7,495 6,996 10.01 Mbps / 9.35 Mbps +# outbound_path 840,738 - 908.00 Mbps / - \ No newline at end of file From 516cb106c1dbd6b25475d19907a8e7435da35027 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Mon, 24 Aug 2026 21:14:58 +0200 Subject: [PATCH 07/16] Updated througput benchmarker --- tests/throughput.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/tests/throughput.py b/tests/throughput.py index ac183e81..b5a3bb92 100644 --- a/tests/throughput.py +++ b/tests/throughput.py @@ -882,8 +882,27 @@ if __name__ == "__main__": unittest.main(argv=[sys.argv[0]] + rest, verbosity=2) +# Pre: +# +# Transport Throughput - PPS matrix (median of runs) +# scenario direct drainer +# ---------------------------------------------------------------------- +# transit_single_135 208,550 184,075 225.23 Mbps / 198.80 Mbps +# transit_single_475 195,652 130,808 743.48 Mbps / 497.07 Mbps +# transit_single_1024 169,828 111,446 1.39 Gbps / 912.97 Mbps +# transit_single_16384 49,618 74,101 6.50 Gbps / 9.71 Gbps +# transit_single_final 204,604 112,925 220.97 Mbps / 121.96 Mbps +# transit_link_135 272,813 234,430 294.64 Mbps / 253.18 Mbps +# transit_link_475 260,003 227,895 988.01 Mbps / 866.00 Mbps +# transit_link_1024 232,763 145,872 1.91 Gbps / 1.19 Gbps +# transit_link_16384 84,028 73,313 11.01 Gbps / 9.61 Gbps +# +# terminus_single 29,813 25,663 32.20 Mbps / 27.72 Mbps +# announce_ingress 7,561 7,242 10.10 Mbps / 9.68 Mbps +# outbound_path 696,106 - 751.79 Mbps / - +# # No Fastpath: - +# # Transport Core Throughput - PPS matrix (median of runs) # scenario direct drainer # ---------------------------------------------------------------------- @@ -896,15 +915,15 @@ if __name__ == "__main__": # transit_link_475 256,347 152,410 974.12 Mbps / 579.16 Mbps # transit_link_1024 228,080 194,444 1.87 Gbps / 1.59 Gbps # transit_link_16384 82,149 73,720 10.77 Gbps / 9.66 Gbps - +# # terminus_single 29,764 28,308 32.15 Mbps / 30.57 Mbps # announce_ingress 7,394 7,150 9.88 Mbps / 9.55 Mbps # outbound_path 872,747 - 942.57 Mbps / - # ---------------------------------------------------------------------- - - +# +# # Fastpath: - +# # Transport Core Throughput - PPS matrix (median of runs) # scenario direct drainer # ---------------------------------------------------------------------- @@ -917,7 +936,7 @@ if __name__ == "__main__": # transit_link_475 417,013 417,294 1.58 Gbps / 1.59 Gbps # transit_link_1024 347,548 353,783 2.85 Gbps / 2.90 Gbps # transit_link_16384 96,682 96,871 12.67 Gbps / 12.70 Gbps - +# # terminus_single 29,451 25,862 31.81 Mbps / 27.93 Mbps # announce_ingress 7,495 6,996 10.01 Mbps / 9.35 Mbps # outbound_path 840,738 - 908.00 Mbps / - \ No newline at end of file From d044db29317d2a6490e21cdad5163161508a6537 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Mon, 24 Aug 2026 22:12:04 +0200 Subject: [PATCH 08/16] Cache announce signature validation --- RNS/Identity.py | 3 ++- RNS/Packet.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/RNS/Identity.py b/RNS/Identity.py index a2512efe..988d3a43 100644 --- a/RNS/Identity.py +++ b/RNS/Identity.py @@ -556,7 +556,8 @@ class Identity: if signal_blackholed: return "blackholed" else: return False - if announced_identity.pub != None and announced_identity.validate(signature, signed_data): + if announced_identity.pub != None and (packet.announce_signature_validated or announced_identity.validate(signature, signed_data)): + packet.announce_signature_validated = True if only_validate_signature: del announced_identity return True diff --git a/RNS/Packet.py b/RNS/Packet.py index 6ff842f4..9357adc0 100755 --- a/RNS/Packet.py +++ b/RNS/Packet.py @@ -117,7 +117,8 @@ class Packet: __slots__ = "hops", "header", "header_type", "packet_type", "transport_type", "context", "context_flag", "destination" __slots__ += "transport_id", "data", "flags", "raw", "packed", "sent", "create_receipt", "receipt", "fromPacked", "MTU" __slots__ += "sent_at", "packet_hash", "ratchet_id", "attached_interface", "receiving_interface", "rssi", "snr", "q" - __slots__ += "ciphertext", "plaintext", "destination_hash", "destination_type", "link", "map_hash", "is_outbound_pr", "traffic_class" + __slots__ += "ciphertext", "plaintext", "destination_hash", "destination_type", "link", "map_hash", "is_outbound_pr" + __slots__ += "traffic_class", "announce_signature_validated" def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST, header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True, context_flag=FLAG_UNSET): @@ -160,6 +161,7 @@ class Packet: self.ratchet_id = None self.traffic_class = None + self.announce_signature_validated = None self.attached_interface = attached_interface self.receiving_interface = None self.is_outbound_pr = False From 2d2167140dda3052c9ab468f8b38cbecc3566c94 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Tue, 25 Aug 2026 01:41:02 +0200 Subject: [PATCH 09/16] FP cache experiment --- RNS/Transport.py | 87 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 22 deletions(-) diff --git a/RNS/Transport.py b/RNS/Transport.py index 749291fc..7fd1e357 100755 --- a/RNS/Transport.py +++ b/RNS/Transport.py @@ -134,6 +134,7 @@ class Transport: PATH_REQUEST_RG = 1.5 # Extra grace time for roaming-mode interfaces to allow more suitable peers to respond first PATH_REQUEST_MI = 20 # Minimum interval in seconds for automated path requests + USE_FP_CACHE = True USE_INBOUND_QUEUE = True USE_OUTBOUND_QUEUE = False INBOUND_DA_QUEUE_LENGTH = 4096 @@ -173,6 +174,7 @@ class Transport: # Notes on memory usage: 1 megabyte of memory can store approximately # 55.100 path table entries or approximately 22.300 link table entries. + link_fp_cache = {} announce_table = {} # A table for storing announces currently waiting to be retransmitted path_table = {} # A lookup table containing the next hop to a given destination reverse_table = {} # A lookup table for storing packet hashes used to return proofs and replies @@ -1795,6 +1797,34 @@ class Transport: packet.receiving_interface = interface packet.hops += 1 + if Transport.USE_FP_CACHE: + fp_entry = Transport.link_fp_cache.get(packet.destination_hash, None) + if fp_entry: + fp_hops = packet.hops + if len(Transport.local_client_interfaces) > 0: + if Transport.is_local_client_interface(packet.receiving_interface): fp_hops -= 1 + elif Transport.interface_to_shared_instance(packet.receiving_interface): fp_hops -= 1 + + if fp_entry[0] and (fp_hops == fp_entry[1] or fp_hops == fp_entry[2]): outbound_interface = fp_entry[3] + elif packet.receiving_interface == fp_entry[3] and fp_hops == fp_entry[1]: outbound_interface = fp_entry[4] + elif packet.receiving_interface == fp_entry[4] and fp_hops == fp_entry[2]: outbound_interface = fp_entry[3] + else: outbound_interface = None + + if outbound_interface: + nhops = fp_hops if not fp_entry[5] or fp_entry[5] != outbound_interface else Transport.local_hops_delta + # RNS.log(f"{packet.receiving_interface} -> {outbound_interface} {nhops}") + Transport.add_packet_hash(packet.packet_hash) + new_raw = packet.raw[0:1] + new_raw += struct.pack("!B", nhops) + new_raw += packet.raw[2:] + Transport.transmit(outbound_interface, new_raw) + Transport.link_table[packet.destination_hash][IDX_LT_TIMESTAMP] = time.time() + # RNS.log(f"FP CACHE HIT", RNS.LOG_CRITICAL) + return + # else: RNS.log(f"FP CACHE MISS", RNS.LOG_CRITICAL) + + # RNS.log(f"FP CACHE OUTBOUND MISS {packet.hops} {packet.receiving_interface}\n{fp_entry}") + # Ingress limit announces early if packet.packet_type == RNS.Packet.ANNOUNCE: if not tc: traffic_class = Transport.TC_ANNOUNCE @@ -2128,11 +2158,13 @@ class Transport: # the same for this link, direction doesn't # matter, and we simply repeat the packet. outbound_interface = None + same_iface = False if link_entry[IDX_LT_NH_IF] == link_entry[IDX_LT_RCVD_IF]: # But check that taken hops matches one # of the expectede values. if packet.hops == link_entry[IDX_LT_REM_HOPS] or packet.hops == link_entry[IDX_LT_HOPS]: outbound_interface = link_entry[IDX_LT_NH_IF] + same_iface = True else: # If interfaces differ, we transmit on # the opposite interface of what the @@ -2158,6 +2190,18 @@ class Transport: Transport.transmit(outbound_interface, new_raw) Transport.link_table[packet.destination_hash][IDX_LT_TIMESTAMP] = time.time() + if Transport.USE_FP_CACHE and not packet.destination_hash in Transport.link_fp_cache: + if from_local_client and not instance_local_link and Transport.local_hops_delta != 0: mangle_to = outbound_interface + elif to_local_client and not instance_local_link and Transport.local_hops_delta != 0: mangle_to = link_entry[IDX_LT_NH_IF] if outbound_interface != link_entry[IDX_LT_NH_IF] else link_entry[IDX_LT_RCVD_IF] + else: mangle_to = None + + RNS.log(f"MANGLING TO: {mangle_to}") + + Transport.link_fp_cache[packet.destination_hash] = (same_iface, link_entry[IDX_LT_HOPS], link_entry[IDX_LT_REM_HOPS], + link_entry[IDX_LT_RCVD_IF], link_entry[IDX_LT_NH_IF], + mangle_to) + RNS.log(f"FP-CACHED {RNS.prettyhexrep(packet.destination_hash)}") + else: RNS.log(f"No-outbound return on link packet from {packet.receiving_interface}", RNS.LOG_WARNING) # TODO: Remove @@ -2577,30 +2621,29 @@ class Transport: # Handling for local data packets elif packet.packet_type == RNS.Packet.DATA: if packet.destination_type == RNS.Destination.LINK: - with Transport.active_links_lock: - link = Transport.active_links_map.get(packet.destination_hash) - if link != None: - if link.attached_interface == packet.receiving_interface: - packet.link = link - if packet.context == RNS.Packet.CACHE_REQUEST: - cached_packet = Transport.get_cached_packet(packet.data) - if cached_packet != None: - if not cached_packet.unpack(): return - RNS.Packet(destination=link, data=cached_packet.data, - packet_type=cached_packet.packet_type, context=cached_packet.context).send() + link = Transport.active_links_map.get(packet.destination_hash) + if link != None: + if link.attached_interface == packet.receiving_interface: + packet.link = link + if packet.context == RNS.Packet.CACHE_REQUEST: + cached_packet = Transport.get_cached_packet(packet.data) + if cached_packet != None: + if not cached_packet.unpack(): return + RNS.Packet(destination=link, data=cached_packet.data, + packet_type=cached_packet.packet_type, context=cached_packet.context).send() - else: link.receive(packet) + else: link.receive(packet) - else: - # In the strange and rare case that an interface - # is partly malfunctioning, and a link-associated - # packet is being received on an interface that - # has failed sending, and transport has failed over - # to another path, we remove this packet hash from - # the filter hashlist so the link can receive the - # packet when it finally arrives over another path. - while packet.packet_hash in Transport.packet_hashlist: Transport.packet_hashlist.remove(packet.packet_hash) - while packet.packet_hash in Transport.packet_hashlist_prev: Transport.packet_hashlist_prev.remove(packet.packet_hash) + else: + # In the strange and rare case that an interface + # is partly malfunctioning, and a link-associated + # packet is being received on an interface that + # has failed sending, and transport has failed over + # to another path, we remove this packet hash from + # the filter hashlist so the link can receive the + # packet when it finally arrives over another path. + while packet.packet_hash in Transport.packet_hashlist: Transport.packet_hashlist.remove(packet.packet_hash) + while packet.packet_hash in Transport.packet_hashlist_prev: Transport.packet_hashlist_prev.remove(packet.packet_hash) else: destination = None with Transport.destinations_map_lock: From f1117099021c357a1f9128ba8e22ef06591a46e2 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Tue, 25 Aug 2026 01:41:31 +0200 Subject: [PATCH 10/16] Updated througput benchmarker --- tests/throughput.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/throughput.py b/tests/throughput.py index b5a3bb92..ea5189d7 100644 --- a/tests/throughput.py +++ b/tests/throughput.py @@ -608,7 +608,7 @@ def _fmt_pps(value): def print_environment_header(): - tree_fastpath = "present" if hasattr(RNS.Transport, "_fastpath") else "absent" + tree_fastpath = "present" if hasattr(RNS.Transport, "USE_FP_CACHE") else "absent" print("=" * 40) print("Reticulum Transport Throughput Benchmark") print("=" * 40) @@ -616,7 +616,7 @@ def print_environment_header(): print(f" Mode : {'compiled' if RNS.compiled else 'interpreted'}") print(f" crypto : {RNS.Cryptography.backend()}") print(f" transport : enabled") - print(f" fast path : {tree_fastpath} (informational)") + print(f" fast path : {tree_fastpath}") print(f" python : {platform.python_version()}") print(f" platform : {platform.platform()}") print(f" machine : {platform.machine()}, cpus: {os.cpu_count()}") @@ -840,7 +840,7 @@ def _usage(): "\nUsage: python3 tests/transport_throughput.py [options]\n" "\n" "Options:\n" - " -s, --scenario NAME Run only the named scenario (repeatable)\n" + " -s, --scenario NAME Run only the named scenario\n" " --mode MODE Measurement mode: inline, drainer or both\n" " (default: both)\n" " --runs N Measurement runs per scenario/mode, median\n" From 17e980ff7982ee5e952f777488e70d11aea007e1 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Tue, 25 Aug 2026 01:44:50 +0200 Subject: [PATCH 11/16] Cleanup --- RNS/Transport.py | 43 ------------------------------------------- 1 file changed, 43 deletions(-) diff --git a/RNS/Transport.py b/RNS/Transport.py index 7fd1e357..c253c05e 100755 --- a/RNS/Transport.py +++ b/RNS/Transport.py @@ -134,7 +134,6 @@ class Transport: PATH_REQUEST_RG = 1.5 # Extra grace time for roaming-mode interfaces to allow more suitable peers to respond first PATH_REQUEST_MI = 20 # Minimum interval in seconds for automated path requests - USE_FP_CACHE = True USE_INBOUND_QUEUE = True USE_OUTBOUND_QUEUE = False INBOUND_DA_QUEUE_LENGTH = 4096 @@ -1797,34 +1796,6 @@ class Transport: packet.receiving_interface = interface packet.hops += 1 - if Transport.USE_FP_CACHE: - fp_entry = Transport.link_fp_cache.get(packet.destination_hash, None) - if fp_entry: - fp_hops = packet.hops - if len(Transport.local_client_interfaces) > 0: - if Transport.is_local_client_interface(packet.receiving_interface): fp_hops -= 1 - elif Transport.interface_to_shared_instance(packet.receiving_interface): fp_hops -= 1 - - if fp_entry[0] and (fp_hops == fp_entry[1] or fp_hops == fp_entry[2]): outbound_interface = fp_entry[3] - elif packet.receiving_interface == fp_entry[3] and fp_hops == fp_entry[1]: outbound_interface = fp_entry[4] - elif packet.receiving_interface == fp_entry[4] and fp_hops == fp_entry[2]: outbound_interface = fp_entry[3] - else: outbound_interface = None - - if outbound_interface: - nhops = fp_hops if not fp_entry[5] or fp_entry[5] != outbound_interface else Transport.local_hops_delta - # RNS.log(f"{packet.receiving_interface} -> {outbound_interface} {nhops}") - Transport.add_packet_hash(packet.packet_hash) - new_raw = packet.raw[0:1] - new_raw += struct.pack("!B", nhops) - new_raw += packet.raw[2:] - Transport.transmit(outbound_interface, new_raw) - Transport.link_table[packet.destination_hash][IDX_LT_TIMESTAMP] = time.time() - # RNS.log(f"FP CACHE HIT", RNS.LOG_CRITICAL) - return - # else: RNS.log(f"FP CACHE MISS", RNS.LOG_CRITICAL) - - # RNS.log(f"FP CACHE OUTBOUND MISS {packet.hops} {packet.receiving_interface}\n{fp_entry}") - # Ingress limit announces early if packet.packet_type == RNS.Packet.ANNOUNCE: if not tc: traffic_class = Transport.TC_ANNOUNCE @@ -2158,13 +2129,11 @@ class Transport: # the same for this link, direction doesn't # matter, and we simply repeat the packet. outbound_interface = None - same_iface = False if link_entry[IDX_LT_NH_IF] == link_entry[IDX_LT_RCVD_IF]: # But check that taken hops matches one # of the expectede values. if packet.hops == link_entry[IDX_LT_REM_HOPS] or packet.hops == link_entry[IDX_LT_HOPS]: outbound_interface = link_entry[IDX_LT_NH_IF] - same_iface = True else: # If interfaces differ, we transmit on # the opposite interface of what the @@ -2190,18 +2159,6 @@ class Transport: Transport.transmit(outbound_interface, new_raw) Transport.link_table[packet.destination_hash][IDX_LT_TIMESTAMP] = time.time() - if Transport.USE_FP_CACHE and not packet.destination_hash in Transport.link_fp_cache: - if from_local_client and not instance_local_link and Transport.local_hops_delta != 0: mangle_to = outbound_interface - elif to_local_client and not instance_local_link and Transport.local_hops_delta != 0: mangle_to = link_entry[IDX_LT_NH_IF] if outbound_interface != link_entry[IDX_LT_NH_IF] else link_entry[IDX_LT_RCVD_IF] - else: mangle_to = None - - RNS.log(f"MANGLING TO: {mangle_to}") - - Transport.link_fp_cache[packet.destination_hash] = (same_iface, link_entry[IDX_LT_HOPS], link_entry[IDX_LT_REM_HOPS], - link_entry[IDX_LT_RCVD_IF], link_entry[IDX_LT_NH_IF], - mangle_to) - RNS.log(f"FP-CACHED {RNS.prettyhexrep(packet.destination_hash)}") - else: RNS.log(f"No-outbound return on link packet from {packet.receiving_interface}", RNS.LOG_WARNING) # TODO: Remove From 38e9d1cdd48c83acb115bb166694409d919f2358 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Tue, 25 Aug 2026 01:45:34 +0200 Subject: [PATCH 12/16] Cleanup --- RNS/Transport.py | 69 ++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/RNS/Transport.py b/RNS/Transport.py index c253c05e..a65d889c 100755 --- a/RNS/Transport.py +++ b/RNS/Transport.py @@ -2690,46 +2690,45 @@ class Transport: with Transport.pending_links_lock: link = Transport.pending_links_map.get(packet.destination_hash) if link != None: - # TODO: Cleanup indentation - if packet.hops != link.expected_hops and link.status == RNS.Link.PENDING and Transport.ALLOW_LINK_PATH_REBALANCE: - RNS.log(f"Unbalanced link path ({packet.hops}/{link.expected_hops}) detected on link {link}, validating signature for re-balancing...", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None - try: - if len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2 or len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE: - packet_data = packet.data - signalling_bytes = b"" - confirmed_mtu = None - mode = RNS.Link.mode_from_lp_packet(packet) - if mode != link.mode: raise TypeError(f"Invalid link mode {mode} in link request proof") - if len(packet_data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE: - confirmed_mtu = RNS.Link.mtu_from_lp_packet(packet) - signalling_bytes = RNS.Link.signalling_bytes(confirmed_mtu, mode) - packet_data = packet_data[:RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2] + if packet.hops != link.expected_hops and link.status == RNS.Link.PENDING and Transport.ALLOW_LINK_PATH_REBALANCE: + RNS.log(f"Unbalanced link path ({packet.hops}/{link.expected_hops}) detected on link {link}, validating signature for re-balancing...", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None + try: + if len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2 or len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE: + packet_data = packet.data + signalling_bytes = b"" + confirmed_mtu = None + mode = RNS.Link.mode_from_lp_packet(packet) + if mode != link.mode: raise TypeError(f"Invalid link mode {mode} in link request proof") + if len(packet_data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE: + confirmed_mtu = RNS.Link.mtu_from_lp_packet(packet) + signalling_bytes = RNS.Link.signalling_bytes(confirmed_mtu, mode) + packet_data = packet_data[:RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2] - peer_pub_bytes = packet_data[RNS.Identity.SIGLENGTH//8:RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2] - peer_sig_pub_bytes = link.destination.identity.get_public_key()[RNS.Link.ECPUBSIZE//2:RNS.Link.ECPUBSIZE] + peer_pub_bytes = packet_data[RNS.Identity.SIGLENGTH//8:RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2] + peer_sig_pub_bytes = link.destination.identity.get_public_key()[RNS.Link.ECPUBSIZE//2:RNS.Link.ECPUBSIZE] - signed_data = link.link_id+peer_pub_bytes+peer_sig_pub_bytes+signalling_bytes - signature = packet_data[:RNS.Identity.SIGLENGTH//8] + signed_data = link.link_id+peer_pub_bytes+peer_sig_pub_bytes+signalling_bytes + signature = packet_data[:RNS.Identity.SIGLENGTH//8] - if link.destination.identity.validate(signature, signed_data): - if not link.rebalanced: - RNS.log(f"Re-balancing path to {RNS.prettyhexrep(link.destination.hash)} at link terminus ({link.expected_hops}->{packet.hops})", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None - link.rebalanced = time.time() - link.expected_hops = packet.hops - path_entry = Transport.path_table.get(link.destination.hash) - if path_entry: - path_entry[IDX_PT_HOPS] = packet.hops - RNS.log(f"Path table re-balanced for {RNS.prettyhexrep(link.destination.hash)}", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None + if link.destination.identity.validate(signature, signed_data): + if not link.rebalanced: + RNS.log(f"Re-balancing path to {RNS.prettyhexrep(link.destination.hash)} at link terminus ({link.expected_hops}->{packet.hops})", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None + link.rebalanced = time.time() + link.expected_hops = packet.hops + path_entry = Transport.path_table.get(link.destination.hash) + if path_entry: + path_entry[IDX_PT_HOPS] = packet.hops + RNS.log(f"Path table re-balanced for {RNS.prettyhexrep(link.destination.hash)}", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None - else: RNS.log(f"Aborting path re-balancing at link terminus for {RNS.prettyhexrep(link.destination.hash)} on link {link} due to invalid signature", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None - except Exception as e: RNS.log("Error while validating link request proof for path re-balancing at link terminus. The contained exception was: "+str(e), REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None + else: RNS.log(f"Aborting path re-balancing at link terminus for {RNS.prettyhexrep(link.destination.hash)} on link {link} due to invalid signature", REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None + except Exception as e: RNS.log("Error while validating link request proof for path re-balancing at link terminus. The contained exception was: "+str(e), REBALANCE_LOGLEVEL) if RNS.sl(REBALANCE_LOGLEVEL) else None - if packet.hops == link.expected_hops: - # Add this packet to the filter hashlist if we - # have determined that it's actually destined - # for this system, and then validate the proof - Transport.add_packet_hash(packet.packet_hash) - pending_link = link + if packet.hops == link.expected_hops: + # Add this packet to the filter hashlist if we + # have determined that it's actually destined + # for this system, and then validate the proof + Transport.add_packet_hash(packet.packet_hash) + pending_link = link if pending_link: pending_link.validate_proof(packet) From 8221f82dc0439cea4009470b4a1133dd5272ca6e Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Tue, 25 Aug 2026 01:52:05 +0200 Subject: [PATCH 13/16] Cleanup --- RNS/Transport.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/RNS/Transport.py b/RNS/Transport.py index a65d889c..8331f290 100755 --- a/RNS/Transport.py +++ b/RNS/Transport.py @@ -2159,8 +2159,7 @@ class Transport: Transport.transmit(outbound_interface, new_raw) Transport.link_table[packet.destination_hash][IDX_LT_TIMESTAMP] = time.time() - else: - RNS.log(f"No-outbound return on link packet from {packet.receiving_interface}", RNS.LOG_WARNING) # TODO: Remove + else: RNS.log(f"No-outbound return on link packet from {packet.receiving_interface}", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None # TODO: Can we return safely here? Test and possibly enable this at some point. return @@ -2602,11 +2601,7 @@ class Transport: while packet.packet_hash in Transport.packet_hashlist: Transport.packet_hashlist.remove(packet.packet_hash) while packet.packet_hash in Transport.packet_hashlist_prev: Transport.packet_hashlist_prev.remove(packet.packet_hash) else: - destination = None - with Transport.destinations_map_lock: - if packet.destination_hash in Transport.destinations_map: - destination = Transport.destinations_map[packet.destination_hash] - + destination = Transport.destinations_map.get(packet.destination_hash) if destination and destination.type == packet.destination_type: packet.destination = destination if destination.receive(packet): From aba8d606dd0d4b1ff3be11b5b9c7d62ff25a49e5 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Tue, 25 Aug 2026 01:52:53 +0200 Subject: [PATCH 14/16] Cleanup --- RNS/Transport.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/RNS/Transport.py b/RNS/Transport.py index 8331f290..0887ab79 100755 --- a/RNS/Transport.py +++ b/RNS/Transport.py @@ -2160,10 +2160,8 @@ class Transport: Transport.link_table[packet.destination_hash][IDX_LT_TIMESTAMP] = time.time() else: RNS.log(f"No-outbound return on link packet from {packet.receiving_interface}", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None - - # TODO: Can we return safely here? Test and possibly enable this at some point. - return + return # Announce handling. Handles logic related to incoming # announces, queueing rebroadcasts of these, and removal From dea0124c5759185c60c5545601e72a9a5970f28c Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Tue, 25 Aug 2026 02:06:34 +0200 Subject: [PATCH 15/16] Reduced lock acquisition --- RNS/Transport.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/RNS/Transport.py b/RNS/Transport.py index 0887ab79..c54d61bd 100755 --- a/RNS/Transport.py +++ b/RNS/Transport.py @@ -2167,14 +2167,10 @@ class Transport: # announces, queueing rebroadcasts of these, and removal # of queued announce rebroadcasts once handed to the next node. if packet.packet_type == RNS.Packet.ANNOUNCE: - local_destination = None - with Transport.destinations_map_lock: - if packet.destination_hash in Transport.destinations_map: - local_destination = Transport.destinations_map[packet.destination_hash] - announce_valid = RNS.Identity.validate_announce(packet) if not announce_valid: return packet.receiving_interface.protocol_violation("Invalid announce") if packet.receiving_interface else None + local_destination = Transport.destinations_map.get(packet.destination_hash) if local_destination == None and announce_valid: if packet.transport_id != None: received_from = packet.transport_id @@ -2541,11 +2537,7 @@ class Transport: # Handling for link requests to local destinations elif packet.packet_type == RNS.Packet.LINKREQUEST and not link_request_handled: if packet.transport_id == None or packet.transport_id == Transport.identity.hash: - destination = None - with Transport.destinations_map_lock: - if packet.destination_hash in Transport.destinations_map: - destination = Transport.destinations_map[packet.destination_hash] - + destination = Transport.destinations_map.get(packet.destination_hash) if destination and destination.type == packet.destination_type: path_mtu = RNS.Link.mtu_from_lr_packet(packet) mode = RNS.Link.mode_from_lr_packet(packet) From d38a8de571421f4091b2e977c5864931bce4c01b Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Tue, 25 Aug 2026 02:18:23 +0200 Subject: [PATCH 16/16] Fixed f-strings for old snakes --- RNS/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/RNS/__init__.py b/RNS/__init__.py index dff1471a..1c63b3b8 100755 --- a/RNS/__init__.py +++ b/RNS/__init__.py @@ -588,18 +588,18 @@ class Profiler: name = tag["name"] stats_all = tag["stats_all"]; stats_1m = tag["stats_1m"]; stats_5m = tag["stats_5m"]; stats_30m = tag["stats_30m"]; stats_60m = tag["stats_60m"] results_str = f" {ind}{name}\n" - results_str += f" {ind} Samples : {stats_all["count"]} from {stats_all["threads"]} thread{'s' if stats_all["threads"] > 1 else ''}\n" + results_str += f" {ind} Samples : {stats_all['count']} from {stats_all['threads']} thread{'s' if stats_all['threads'] > 1 else ''}\n" if stats_all != None: results_str += f" {ind} {'Mean':^15} | {'Median':^15} | {'Min':^15} | {'Max':^15} | {'St. dev':^15} | {'Total':^15}\n" - results_str += f" {ind} Stats : ({pst(stats_all["mean"]):^15} | {pst(stats_all["median"]):^15} | {pst(stats_all["min"]):^15} | {pst(stats_all["max"]):^15} | {pst(stats_all["stdev"]):^15} | {pst(stats_all["sum"]):^15})\n" + results_str += f" {ind} Stats : ({pst(stats_all['mean']):^15} | {pst(stats_all['median']):^15} | {pst(stats_all['min']):^15} | {pst(stats_all['max']):^15} | {pst(stats_all['stdev']):^15} | {pst(stats_all['sum']):^15})\n" if stats_1m != None: - results_str += f" {ind} 0-1m : ({pst(stats_1m["mean"]):^15} | {pst(stats_1m["median"]):^15} | {pst(stats_1m["min"]):^15} | {pst(stats_1m["max"]):^15} | {pst(stats_1m["stdev"]):^15} | {pst(stats_1m["sum"]):^15})\n" + results_str += f" {ind} 0-1m : ({pst(stats_1m['mean']):^15} | {pst(stats_1m['median']):^15} | {pst(stats_1m['min']):^15} | {pst(stats_1m['max']):^15} | {pst(stats_1m['stdev']):^15} | {pst(stats_1m['sum']):^15})\n" if stats_5m != None: - results_str += f" {ind} 1-5m : ({pst(stats_5m["mean"]):^15} | {pst(stats_5m["median"]):^15} | {pst(stats_5m["min"]):^15} | {pst(stats_5m["max"]):^15} | {pst(stats_5m["stdev"]):^15} | {pst(stats_5m["sum"]):^15})\n" + results_str += f" {ind} 1-5m : ({pst(stats_5m['mean']):^15} | {pst(stats_5m['median']):^15} | {pst(stats_5m['min']):^15} | {pst(stats_5m['max']):^15} | {pst(stats_5m['stdev']):^15} | {pst(stats_5m['sum']):^15})\n" if stats_30m != None: - results_str += f" {ind} 5-30m : ({pst(stats_30m["mean"]):^15} | {pst(stats_30m["median"]):^15} | {pst(stats_30m["min"]):^15} | {pst(stats_30m["max"]):^15} | {pst(stats_30m["stdev"]):^15} | {pst(stats_30m["sum"]):^15})\n" + results_str += f" {ind} 5-30m : ({pst(stats_30m['mean']):^15} | {pst(stats_30m['median']):^15} | {pst(stats_30m['min']):^15} | {pst(stats_30m['max']):^15} | {pst(stats_30m['stdev']):^15} | {pst(stats_30m['sum']):^15})\n" if stats_60m != None: - results_str += f" {ind} 30-60m : ({pst(stats_60m["mean"]):^15} | {pst(stats_60m["median"]):^15} | {pst(stats_60m["min"]):^15} | {pst(stats_60m["max"]):^15} | {pst(stats_60m["stdev"]):^15} | {pst(stats_60m["sum"]):^15})\n" + results_str += f" {ind} 30-60m : ({pst(stats_60m['mean']):^15} | {pst(stats_60m['median']):^15} | {pst(stats_60m['min']):^15} | {pst(stats_60m['max']):^15} | {pst(stats_60m['stdev']):^15} | {pst(stats_60m['sum']):^15})\n" return results_str results_str = ""