From db3033d3b01b4df6d9494b8d60bb62dd9f1b16fc Mon Sep 17 00:00:00 2001 From: Torlando <281092095+torlando-agent[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:14:44 +0000 Subject: [PATCH] fix(propagation): gate endpoint hot-path path-table reads (microReticulum bump) Bumps microReticulum pin to e2c9d4d (diag/path-get-caller on cd0338e): Transport::inbound() and path_request() no longer perform a full microStore get() (flash write + read) for every inbound packet / path request on endpoint-only nodes. The read is gated on the exact conditions where destination_entry is consumed, and the local-destination path-request answer uses the in-memory _destinations table, so the device stays discoverable. The build still carries the temporary [PG] counters for the live before/after capture; counters are stripped in a follow-up before anything merges. --- platformio.ini | 2 +- ...est_endpoint_hotpath_read_gate_contract.py | 114 ++++++++++++++++++ .../test_release_build_contract.py | 2 +- tests/native/nomadnet_x86_flow/CMakeLists.txt | 2 +- tests/native/nomadnet_x86_flow/run_flow.py | 2 +- tools/audit_release_build.py | 2 +- 6 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 tests/build_scripts/test_endpoint_hotpath_read_gate_contract.py diff --git a/platformio.ini b/platformio.ini index 409dc9fa..d0d09cbe 100644 --- a/platformio.ini +++ b/platformio.ini @@ -105,7 +105,7 @@ lib_deps = ; tables so the explicit 1 MiB RNS container pool cannot grow without limit. ; Immutable published pin for bounded transport tables and sensitive ; packet-request envelope wiping. - https://github.com/torlando-tech/microReticulum.git#ef071871421b244e4209f1c2b5b2a838b22bd756 + https://github.com/torlando-tech/microReticulum.git#e2c9d4d17a510e40821a0b0651de07469f78d2cc ; microLXMF: chore/microreticulum-0.4.1-layout — includes namespaced to ; for the 0.4.x src/microReticulum/ layout. ; 3cdde79: faster load_message_metadata — single LittleFS open (read_file diff --git a/tests/build_scripts/test_endpoint_hotpath_read_gate_contract.py b/tests/build_scripts/test_endpoint_hotpath_read_gate_contract.py new file mode 100644 index 00000000..54b1b868 --- /dev/null +++ b/tests/build_scripts/test_endpoint_hotpath_read_gate_contract.py @@ -0,0 +1,114 @@ +"""Source-level contract for the endpoint-node hot-path read gate. + +In the CBA microStore fork, Transport::inbound() and Transport::path_request() +each perform a full _new_path_table.get() (flush_buffer flash write + index +scan + segment open) for every non-announce inbound packet / path request, +BEFORE consulting whether the node can act on the result. On an endpoint-only +node (transport disabled, no attached local clients) that result is never +used, so each air packet costs a multi-second flash operation on the 2MB +partition that also holds message storage. Measured on a T-Deck idle: 792 +such reads in 600s, all from these two sites, driving a 840s display stall. + +The fix gates each read on the exact condition where its destination_entry is +actually consumed: + - inbound(): for_local_client is only actionable when relaying, so gate on + (transport_enabled || any local client attached). + - path_request(): destination_entry is only read in the + (transport_enabled || is_from_local_client) answer branch; the + local-destination answer for THIS node uses the in-memory + _destinations table and must remain reachable. + +These checks lock the gates in place and prove the local-destination answer +path is preserved (an endpoint must still be discoverable). +""" + +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +TRANSPORT_CPP = ROOT / ".pio/libdeps/tdeck/microReticulum/src/microReticulum/Transport.cpp" + + +def require_source() -> str: + if not TRANSPORT_CPP.is_file(): + pytest.skip("pinned microReticulum libdeps not populated (run: pio run -e tdeck)") + return TRANSPORT_CPP.read_text() + + +def function_body(source: str, signature: str, next_signature: str) -> str: + start = source.index(signature) + nxt = source.index(next_signature, start + len(signature)) + return source[start:nxt] + + +def test_inbound_for_local_client_read_gated(): + source = require_source() + body = function_body( + source, + "void Transport::inbound(const Bytes& raw", + "void Transport::synthesize_tunnel(", + ) + # The for_local_client determination is the hot read we gate. + marker = "If packet is anything besides ANNOUNCE then determine" + assert marker in body, "inbound() for_local_client block not found" + block = body[body.index(marker):] + + # The get() must now be guarded by the endpoint condition. + gate = block.index("Reticulum::transport_enabled() || _local_client_interfaces.size() > 0") + get = block.index("_new_path_table.get(packet.destination_hash()", gate) + assert gate < get, ( + "inbound() _new_path_table.get() for for_local_client must be gated " + "behind (transport_enabled || local clients attached); an endpoint-only " + "node must not pay a flash read for a value it never uses" + ) + # The hops==0 for_local_client assignment must still follow the (skippable) read. + assert "for_local_client = true" in block[get:], ( + "inbound() must still set for_local_client on a hops==0 hit" + ) + + +def test_path_request_read_gated_but_local_answer_preserved(): + source = require_source() + body = function_body( + source, + "void Transport::path_request(const Bytes& destination_hash", + "bool Transport::from_local_client(", + ) + # destination_entry is only consumed in the answer branch; the read must be + # gated on the same condition. + gate = body.index("Reticulum::transport_enabled() || is_from_local_client") + get = body.index("_new_path_table.get(destination_hash", gate) + assert gate < get, ( + "path_request() _new_path_table.get() must be gated behind " + "(transport_enabled || is_from_local_client); an endpoint-only node " + "must not pay a flash read to answer a path request" + ) + # The local-destination answer (in-memory _destinations) must remain and be + # reachable AFTER the (skippable) read so an endpoint stays discoverable. + local_lookup = body.index("_destinations.find(destination_hash)") + assert local_lookup > get, ( + "path_request() must still consult the in-memory _destinations table " + "to answer path requests for THIS node (local-destination answer)" + ) + + +def test_fork_pin_matches_audit_tool(): + # The gate lives in the pinned microReticulum fork; the pin in + # platformio.ini must agree with tools/audit_release_build.py so a bump is + # atomic (all 5 sites moved together). + ini = (ROOT / "platformio.ini").read_text() + audit = (ROOT / "tools/audit_release_build.py").read_text() + ini_pins = { + line.split("microReticulum.git#")[1].strip() + for line in ini.splitlines() + if "microReticulum.git#" in line + } + audit_pins = { + line.split('"')[3] + for line in audit.splitlines() + if line.strip().startswith('"microReticulum"') + } + assert ini_pins and ini_pins == audit_pins, ( + f"platformio.ini microReticulum pin {ini_pins} != audit tool pin {audit_pins}" + ) diff --git a/tests/build_scripts/test_release_build_contract.py b/tests/build_scripts/test_release_build_contract.py index 7c7be906..7b70f609 100644 --- a/tests/build_scripts/test_release_build_contract.py +++ b/tests/build_scripts/test_release_build_contract.py @@ -3,7 +3,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[2] MICROSTORE_PIN = "https://github.com/torlando-tech/microStore.git#2762f7606800ffb23f4a593947d4f58e259cda7a" -MICRORETICULUM_PIN = "https://github.com/torlando-tech/microReticulum.git#ef071871421b244e4209f1c2b5b2a838b22bd756" +MICRORETICULUM_PIN = "https://github.com/torlando-tech/microReticulum.git#e2c9d4d17a510e40821a0b0651de07469f78d2cc" MAX_RNS_PSRAM_POOL_BYTES = 1024 * 1024 diff --git a/tests/native/nomadnet_x86_flow/CMakeLists.txt b/tests/native/nomadnet_x86_flow/CMakeLists.txt index 73c5b506..429010af 100644 --- a/tests/native/nomadnet_x86_flow/CMakeLists.txt +++ b/tests/native/nomadnet_x86_flow/CMakeLists.txt @@ -29,7 +29,7 @@ set(PYXIS_NOMADNET_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../lib/tdeck_ui/UI/LXMF" set(PYXIS_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../..") set(PYXIS_MANIFEST_BASE "51e4b586c4c3867ae399f573557edda6f3b48a44") set(PYXIS_MANIFEST_BRANCH "feat/nomadnet-partials-core") -set(PYXIS_MANIFEST_MICRORETICULUM "ef071871421b244e4209f1c2b5b2a838b22bd756") +set(PYXIS_MANIFEST_MICRORETICULUM "e2c9d4d17a510e40821a0b0651de07469f78d2cc") execute_process(COMMAND git rev-parse HEAD WORKING_DIRECTORY "${RNS_SOURCE}" OUTPUT_VARIABLE _rns_commit OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE _rns_git_result) diff --git a/tests/native/nomadnet_x86_flow/run_flow.py b/tests/native/nomadnet_x86_flow/run_flow.py index 453c8434..a5f8c761 100644 --- a/tests/native/nomadnet_x86_flow/run_flow.py +++ b/tests/native/nomadnet_x86_flow/run_flow.py @@ -45,7 +45,7 @@ if os.environ.get("PYXIS_FLOW_SCENARIOS"): MANIFEST_BASE = "51e4b586c4c3867ae399f573557edda6f3b48a44" MANIFEST_BRANCH = "feat/nomadnet-partials-core" -MANIFEST_MICRORETICULUM = "ef071871421b244e4209f1c2b5b2a838b22bd756" +MANIFEST_MICRORETICULUM = "e2c9d4d17a510e40821a0b0651de07469f78d2cc" MANIFEST_FILES = ( "tests/native/nomadnet_x86_flow/CMakeLists.txt", "tests/native/nomadnet_x86_flow/BuildManifest.h.in", diff --git a/tools/audit_release_build.py b/tools/audit_release_build.py index b202fcc0..2ffce5d2 100644 --- a/tools/audit_release_build.py +++ b/tools/audit_release_build.py @@ -45,7 +45,7 @@ EXCLUDED_SYMBOLS = ( "MapTileHttpArduino::", ) PINNED_DEPENDENCIES = { - "microReticulum": "ef071871421b244e4209f1c2b5b2a838b22bd756", + "microReticulum": "e2c9d4d17a510e40821a0b0651de07469f78d2cc", "microLXMF": "82d2e54d721d9e4ce3ffc0049d9996aa5632390a", "microStore": "2762f7606800ffb23f4a593947d4f58e259cda7a", }