Document microReticulum random_hash interop bug (§4.1 callout + §9.10)

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) <noreply@anthropic.com>
This commit is contained in:
Rob 2026-05-03 11:27:03 -04:00
commit fa014d21e6
2 changed files with 37 additions and 0 deletions

24
SPEC.md
View file

@ -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 <size>B H<1|2> <PT> dest=<hex> ctx=0x<hex> hops=<n>
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

13
todo.md
View file

@ -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