From fa014d21e6324b77af6bdedf9c864b76d8cc0cb5 Mon Sep 17 00:00:00 2001 From: Rob Date: Sun, 3 May 2026 11:27:03 -0400 Subject: [PATCH] =?UTF-8?q?Document=20microReticulum=20random=5Fhash=20int?= =?UTF-8?q?erop=20bug=20(=C2=A74.1=20callout=20+=20=C2=A79.10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real interop bug found while checking what the thatSFguy/reticulum-lora-repeater stack does with the random_hash field. The repeater is a thin wrapper around attermann/microReticulum, which emits 10 fully-random bytes for random_hash rather than the upstream Python form of 5 random bytes + 5 bytes of big-endian uint40 unix_seconds. The Python form is preserved as a comment in microReticulum src/Destination.cpp:270-272, with a "CBA TODO add in time to random hash" next to the random-only implementation. Effect: Python RNS receivers parse random_hash[5:10] as an emission timestamp via Transport.timebase_from_random_blob (RNS/Transport.py: 3100-3101), and use it for path-table replacement decisions in the equal-or-greater-hop branch (RNS/Transport.py:1721-1745). A uniformly-random uint40 has median ~5.5e11 ≈ year 19403 AD, so microReticulum announces look "far-future" to Python receivers and permanently win replay-ordering comparisons until the path TTL expires. First-contact path-table population is unaffected — the bug only surfaces on path replacement, which makes it a quiet failure mode in mixed-vendor meshes (microReticulum repeater + Python rnsd). Symmetry: microReticulum receivers don't consult the timestamp half, so microReticulum-to-microReticulum traffic is unaffected. The asymmetry is what makes the symptom show up only when a Python relay is also in the mesh. The repeater's pre_build.py aggressively patches FIVE other microReticulum protocol bugs (ratchet announce parsing, identity hash length 16→32, validate_announce/announce diagnostics, DATA/ PROOF forwarding for transport-mode, path-table write dedup) — but not this one. Filed as an outreach todo to upstream the fix to attermann/microReticulum. SPEC.md §4.1 — adds an UNVERIFIED callout naming the deviation, citing the exact source location and explaining the propagation path through Python's path-table logic. SPEC.md §9.10 — gotcha entry making the bug findable from the gotchas list, with a suggested clean-room workaround (emit the timestamp half yourself, even just seconds-since-boot). todo.md — outreach entry to file an issue on attermann/microReticulum proposing the fix. Co-Authored-By: Claude Opus 4.7 (1M context) --- SPEC.md | 24 ++++++++++++++++++++++++ todo.md | 13 +++++++++++++ 2 files changed, 37 insertions(+) diff --git a/SPEC.md b/SPEC.md index 9e67d63..eeff105 100644 --- a/SPEC.md +++ b/SPEC.md @@ -189,6 +189,16 @@ random_hash = RNS.Identity.get_random_hash()[0:5] + int(time.time()).to_bytes(5, Transit relays read the timestamp portion via `Transport.timebase_from_random_blob(random_blob) = int.from_bytes(random_blob[5:10], "big")` (`RNS/Transport.py:3100-3101`) to make ordering decisions when an inbound announce carries a higher hop count than the cached path: only newer-emitted announces can refresh the path table (see §4.5). 5 bytes of seconds covers ~34,000 years, so wraparound is not a near-term concern. Implementations MUST emit this exact format, including a clock value that's monotonically non-decreasing across announces from the same destination — clockless sender devices (per §9.6) may end up locked out of long-range path table updates. +> ⚠️ **UNVERIFIED — Known deviation:** `attermann/microReticulum/src/Destination.cpp:270-272` (and therefore every project that uses microReticulum unmodified, including [`thatSFguy/reticulum-lora-repeater`](https://github.com/thatSFguy/reticulum-lora-repeater) and the Faketec sibling project) currently emits 10 fully-random bytes for `random_hash` — the timestamp half is a TODO that never landed: +> +> ```cpp +> //p random_hash = Identity::get_random_hash()[0:5] << int(time.time()).to_bytes(5, "big") +> // CBA TODO add in time to random hash +> Bytes random_hash = Cryptography::random(Type::Identity::RANDOM_HASH_LENGTH/8); +> ``` +> +> Python RNS receivers interpret `random_hash[5:10]` as a big-endian uint40 unix_seconds. A uniformly-random uint40 has median value ~5.5×10¹¹ ≈ year 19403 AD, so a microReticulum announce will (with overwhelming probability) appear "far-future" to a Python receiver. Effect: once one such announce populates `path_table[dest][IDX_PT_RANDBLOBS]`, the equal-or-greater-hop branch at `RNS/Transport.py:1721-1745` will reject any real-timestamped announce as "stale" until the path TTL expires. First-contact path-table population is unaffected; the bug only surfaces on path replacement under §4.5 step 6.3. The microReticulum receive side does NOT consult the timestamp half so microReticulum-to-microReticulum traffic is unaffected. The repeater repo's `pre_build.py` patches several microReticulum protocol bugs but not this one (as of [`thatSFguy/reticulum-lora-repeater@95823ad`-vintage upstream](https://github.com/thatSFguy/reticulum-lora-repeater)). Verifying by capture-and-decode against an actual mixed-vendor mesh is the work that would let this callout be removed. + The optional 32-byte `ratchet_pub` (an X25519 public key) is present iff the packet header's `context_flag` bit is 1. Indexing through this layout accordingly is mandatory; see `RNS/Identity.py::validate_announce` for the canonical parser. ### 4.2 Signed data @@ -822,6 +832,20 @@ rx B H<1|2> dest= ctx=0x hops= logged before any filtering converts hours of "messages aren't arriving" debugging to seconds. Without it, packets dropped by `if (dest != ours) return` vanish silently and look identical to "the bytes never arrived". Symmetric `tx` logging on outbound is similarly cheap insurance. +### 9.10 microReticulum `random_hash` lacks the timestamp half + +Real interop bug to plan around: `attermann/microReticulum`'s `Destination::announce` emits 10 fully-random bytes for the announce `random_hash` field rather than the upstream Python form of `5 random bytes || big-endian uint40 unix_seconds` (see §4.1). The Python form is preserved as a comment in the C++ source with a `TODO add in time to random hash` next to it; the timestamp half was never implemented. + +Effect on a mixed-vendor mesh: a Python RNS receiver parses `random_hash[5:10]` of a microReticulum announce as a far-future timestamp (median ~year 19403 AD because the random uint40 is uniformly distributed across `0..2^40-1`). The path-table replacement rule at `RNS/Transport.py:1721-1745` rejects subsequent real-timestamped announces from Python sources as "stale" until the path TTL expires. + +Symptom: a microReticulum repeater works fine when it's the only path; in a mesh that also has Python relays, paths "stick" to the microReticulum side even when shorter / fresher Python paths come up, until natural TTL expiry. First-contact path-table population is unaffected — the bug only surfaces on path replacement. + +Workarounds when building a clean-room implementation that talks to a microReticulum mesh: +- Emit the upstream form yourself (you have a clock — even seconds-since-boot is preferable to random bytes; the path-table comparison only cares about ordering, not absolute time). +- If you receive a uint40 timestamp that's more than, say, 24 hours in the future, treat it as suspect — but be cautious because legitimate Python senders with skewed clocks could trip this. + +The repeater repo's `pre_build.py` patches several other microReticulum protocol bugs (ratchet announce parsing, identity hash length 16→32, DATA/PROOF forwarding) but does not patch this one. Filing an upstream issue against `attermann/microReticulum` to land the original Python timestamp form is the durable fix. + --- ## 10. Resource fragmentation protocol diff --git a/todo.md b/todo.md index 4343b07..01ad297 100644 --- a/todo.md +++ b/todo.md @@ -10,6 +10,19 @@ Outstanding work for the spec repo. official Reticulum manual. Frame it as a complement to (not a replacement for) the existing operator-focused docs. +- [ ] **File a `random_hash` interop issue on `attermann/microReticulum`.** + `src/Destination.cpp:270-272` emits 10 fully-random bytes + where upstream Python emits 5 random + 5 BE-uint40 unix_seconds + (§4.1, §9.10). Effect: Python RNS path-table replacement + `RNS/Transport.py:1721-1745` rejects fresh announces from + Python sources as "stale" once a microReticulum announce has + populated the random_blob set, because the random tail is + interpreted as a far-future timestamp. Workaround documented + in §9.10; the durable fix is implementing the TODO comment in + the upstream source — even seconds-since-boot is preferable + to random bytes since path-table comparisons care about + ordering, not absolute time. + ## Test infrastructure - [x] **Bootstrap `test-vectors/identities.json`** — Alice + Bob