reticiulum-specification/flows/send-announce.md
Rob cfd0d8249b Re-anchor against RNS 1.2.4 / LXMF 0.9.7 + track upstream distribution shift
Upstream RNS 1.2.4 (2026-05-07) announces it is "probably the last
release that is also published to GitHub" — pip continues until rnpkg
is complete and RNS is self-hosting. All 13 verifiers pass against
1.2.4 / 0.9.7; no wire-format, signing, or protocol behavior changed
between 1.2.0 and 1.2.4, so the changes here are purely currency:

- Pin tools/requirements.txt to rns==1.2.4 / lxmf==0.9.7 so the
  verifier stays reproducible if upstream stops mirroring to PyPI
  before the migration is ready.
- Add an "Upstream distribution shift" watch-list to todo.md (local
  Reticulum node, repo destination hash, rnpkg install/upgrade
  commands, rsg signature verification, mirroring source citations).
- Bump SPEC.md frontmatter and re-anchor ~50 line citations across
  Identity.py, Transport.py, Resource.py, Link.py, Reticulum.py,
  Packet.py, and LXMF/* (Identity.py drift was the heaviest at +13
  to +31 lines; Transport.py was variable). Fix one numeric
  (MAX_RANDOM_BLOBS = 32 → 64) and one semantic (§6.6.3 LRPROOF MTU
  clamp citation pointed at the wrong location — corrected to point
  at the transit-relay clamp at Transport.py:1539-1556).
- Update §10.4 decompression-bomb hazard to note upstream's 1.1.9 cap
  adoption, with citations to Resource.py:686-691 and Buffer.py:95-97
  plus a "do not use one-shot bz2.decompress()" warning.
- Re-anchor 11 flows/ files (version pins + ~30 line citations).
- Bump version labels in tools/README.md, test-vectors/README.md, and
  4 verifier docstrings + 2 hardcoded print strings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 07:42:25 -04:00

6.1 KiB

Flow: send an announce

What happens chronologically when a node emits an announce — the periodic broadcast that lets the rest of the mesh discover or refresh a path to this destination. Pinned against RNS 1.2.4.

Out of scope: the relay-side rebroadcast (forward-announce.md — see ../SPEC.md §12.3) and path-response announces (already covered in path-discovery.md).


Sequence

1. Caller invokes Destination.announce(app_data=...)

RNS/Destination.py:243-318. Triggers:

  • Periodic re-announce loop (every 5-15 minutes per §7.5; LXMF runs this from LXMRouter.jobs).
  • Application-explicit announce (e.g. user clicks "announce now" in Sideband).
  • Path-response branch — see path-discovery.md step 6.

2. Build random_hash

random_hash = RNS.Identity.get_random_hash()[:5] + int(time.time()).to_bytes(5, "big")

5 random bytes + 5 bytes big-endian uint40 unix-seconds timestamp per ../SPEC.md §4.1. The timestamp is used by transit relays for path-table replay-ordering decisions per §4.5 step 6.3.

3. Optional ratchet rotation

If the destination has ratchets enabled (destination.ratchets != None), rotate_ratchets() runs (Destination.py:227-235):

if now > self.latest_ratchet_time + self.ratchet_interval:
    new_ratchet = Identity._generate_ratchet()             # X25519 keypair, 32B priv
    self.ratchets.insert(0, new_ratchet)                   # most-recent-first
    self.latest_ratchet_time = now
    self._clean_ratchets()                                 # cap at RATCHET_COUNT = 512
    self._persist_ratchets()                               # storagepath/ratchets/<hex>

The new ratchet's public key is what gets included in this announce. RATCHET_INTERVAL = 30*60s so a ratchet rotates at most every 30 minutes — back-to-back announces within that window reuse the current ratchet (per §7.3 the relays would dedup them on (dest_hash, ratchet_pub) if even the random_hash collided, but the 10 random bytes prevent that).

4. Build signed_data

Per §4.2:

signed_data = self.hash + self.identity.get_public_key() + self.name_hash + random_hash + ratchet
if app_data is not None:
    signed_data += app_data

self.hash is the destination_hash (which appears in the outer Reticulum header on the wire, but is also signed). ratchet is b"" when no ratchet is included.

5. Sign and pack the announce body

signature = self.identity.sign(signed_data)                # Ed25519 long-term key
announce_data = self.identity.get_public_key() + self.name_hash + random_hash + ratchet + signature
if app_data is not None:
    announce_data += app_data

The dest_hash is not in announce_data even though it's in signed_data — the receiver gets dest_hash from the outer packet header per §4.1.

6. Cache the body for path-response replay

Destination.py:303-309: store self.path_responses[tag] = [time.time(), announce_data] if a tag was supplied (path-response branch). The cache TTL is PR_TAG_WINDOW = 30s per §7.2.4 — same wire bytes served to multiple racing relays for dedup convergence.

7. Construct and emit the Reticulum ANNOUNCE packet

context_flag = FLAG_SET if ratchet else FLAG_UNSET
announce_context = PATH_RESPONSE if path_response else NONE
announce_packet = RNS.Packet(self, announce_data,
                             RNS.Packet.ANNOUNCE,
                             context = announce_context,
                             attached_interface = attached_interface,
                             context_flag = context_flag)
announce_packet.send()

Wire form per §4.1:

  • packet_type = ANNOUNCE (1), transport_type = BROADCAST (0), destination_type = SINGLE (0)
  • header_type = HEADER_1 (the ratchet rotation announce is always broadcast — no transport_id at this stage)
  • context = NONE (0x00) for periodic re-announces, PATH_RESPONSE (0x0B) for path-response announces
  • context_flag = 1 if ratchet present (signals the optional ratchet_pub slot in the body)

Announce packets are NOT encrypted — Packet.pack (RNS/Packet.py:188-191) special-cases ANNOUNCE to skip encryption. The body is signed but plaintext, so anyone in earshot can validate the signature and decode the public key.

8. Transport.outbound broadcasts on every OUT interface

Same broadcast branch as a path? request (flows/path-discovery.md step 2) — the dest_hash isn't in path_table (it's our own destination, not a remote one), so the broadcast branch at RNS/Transport.py:1122+ fires, emitting on every interface where interface.OUT == True. Per §7.5 the announce is rate-limited by ANNOUNCE_CAP = 2.0 (2% airtime) on each interface.

9. Periodic re-announce loop

LXMF runs this via LXMRouter.jobs calling LXMRouter.announce_propagation_node and LXMRouter.announce_destination (for delivery destinations) at the configured cadence. Default re-announce interval is 5-15 minutes per §7.5. Without it, transit-relay path tables age out within minutes and peers can't message you.


Wire-byte summary

[ 1B flags: HEADER_1 | context_flag | BROADCAST | SINGLE | ANNOUNCE ]
[ 1B hops=0 ]
[ 16B dest_hash ]
[ 1B context: 0x00 normal / 0x0B PATH_RESPONSE ]
[ 64B public_key (X25519 || Ed25519) ]
[ 10B name_hash ]
[ 10B random_hash (5B random || 5B big-endian uint40 unix_seconds) ]
[ 32B ratchet_pub ]   ← present iff context_flag bit set
[ 64B Ed25519 signature ]
[ N B  app_data ]     ← may be empty

Source map

Step File Function / line
1 RNS/Destination.py announce, line 243
2 RNS/Destination.py random_hash construction, line 282
3 RNS/Destination.py rotate_ratchets, line 227
4 RNS/Destination.py signed_data assembly, line 297
5 RNS/Destination.py sign + pack, line 300-303
6 RNS/Destination.py path_responses cache, line 305
7 RNS/Destination.py Packet construction, line 313
7 RNS/Packet.py ANNOUNCE-skips-encryption, line 189-191
8 RNS/Transport.py outbound broadcast branch, line 1119
9 LXMF/LXMRouter.py jobs / re-announce cadence