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.
This commit is contained in:
Torlando
2026-09-06 19:14:44 +00:00
parent 8a0eabf432
commit db3033d3b0
6 changed files with 119 additions and 5 deletions
+1 -1
View File
@@ -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
; <microReticulum/...> for the 0.4.x src/microReticulum/ layout.
; 3cdde79: faster load_message_metadata — single LittleFS open (read_file
@@ -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}"
)
@@ -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
@@ -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)
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -45,7 +45,7 @@ EXCLUDED_SYMBOLS = (
"MapTileHttpArduino::",
)
PINNED_DEPENDENCIES = {
"microReticulum": "ef071871421b244e4209f1c2b5b2a838b22bd756",
"microReticulum": "e2c9d4d17a510e40821a0b0651de07469f78d2cc",
"microLXMF": "82d2e54d721d9e4ce3ffc0049d9996aa5632390a",
"microStore": "2762f7606800ffb23f4a593947d4f58e259cda7a",
}