reticiulum-specification/tools/verify_announce_app_data.py
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

123 lines
4.9 KiB
Python

"""
Verifier for SPEC.md S4.3 (announce app_data format for LXMF delivery
destinations).
Exercises:
- Upstream LXMF.LXMRouter.get_announce_app_data emits a 2-element msgpack
array [display_name_bytes, stamp_cost] in LXMF 0.9.7. The dead-code
supported_functionality line at LXMF/LXMRouter.py:998 is computed but
never appended.
- The wire-byte form for display_name="Reticulum5", stamp_cost=None matches
the hex documented in SPEC.md S4.3:
92 c4 0a 52 65 74 69 63 75 6c 75 6d 35 c0
- The parsers in LXMF/LXMF.py tolerate:
* raw UTF-8 ("original announce format")
* 1-element msgpack array
* 2-element [name, stamp_cost]
* 3-element [name, stamp_cost, [capability_flags]]
and that the 3-element variant is what flips
compression_support_from_app_data based on SF_COMPRESSION presence.
Exit code 0 on PASS, non-zero on FAIL.
"""
from __future__ import annotations
import sys
import RNS
import LXMF
from LXMF import LXMF as LXMF_helpers
import RNS.vendor.umsgpack as umsgpack
def fail(msg: str) -> None:
print(f"FAIL: {msg}")
sys.exit(1)
def verify_two_element_wire_bytes():
# SPEC.md S4.3 hex example: display_name="Reticulum5", stamp_cost=None.
# Spec layout:
# 92 # fixarray, 2 elements
# c4 0a # bin8, length 10
# 52 65 74 69 63 75 6c 75 6d 35 # "Reticulum5"
# c0 # nil (stamp_cost)
expected = bytes.fromhex("92" + "c40a" + "5265746963756c756d35" + "c0")
name = b"Reticulum5"
peer_data = [name, None]
actual = umsgpack.packb(peer_data)
if actual != expected:
fail(f"S4.3 2-element wire bytes mismatch:\n"
f" got: {actual.hex()}\n"
f" want: {expected.hex()}")
print("PASS S4.3 2-element wire bytes (umsgpack.packb([b'Reticulum5', None]))")
def verify_producer_is_two_element_in_this_lxmf():
# Read the LXMRouter source and confirm it appends 2 elements (not 3) in
# the LXMF 0.9.7 producer. We do this by inspecting the function source so
# the verifier breaks loudly if upstream restores the 3-element variant.
import inspect
from LXMF.LXMRouter import LXMRouter
src = inspect.getsource(LXMRouter.get_announce_app_data)
if "peer_data = [display_name, stamp_cost]" not in src:
fail(f"S4.3 producer line not found in LXMRouter.get_announce_app_data. "
f"Upstream may have restored the 3-element variant. Source:\n{src}")
if "peer_data.append(supported_functionality)" in src:
# If upstream re-enables this, the spec needs the 3-element variant
# marked as "current upstream" instead of "parser-tolerated only".
fail("S4.3 producer NOW appends supported_functionality. "
"Update SPEC.md S4.3 to describe the 3-element variant as live.")
print(f"PASS S4.3 LXMF {LXMF.__version__} producer emits 2-element form only "
"(supported_functionality is dead code at LXMF/LXMRouter.py:998)")
def verify_parser_tolerance():
cases = [
# (label, app_data bytes, expected display_name, expected stamp_cost,
# expected compression_support)
("raw UTF-8 (original format)",
"Alice".encode("utf-8"),
"Alice", None, True),
("1-element fixarray [name]",
umsgpack.packb([b"Bob"]),
"Bob", None, True),
("2-element fixarray [name, stamp_cost=int]",
umsgpack.packb([b"Carol", 12]),
"Carol", 12, True),
("2-element fixarray [name, nil]",
umsgpack.packb([b"Dan", None]),
"Dan", None, True),
("3-element fixarray [name, nil, [SF_COMPRESSION]]",
umsgpack.packb([b"Eve", None, [LXMF_helpers.SF_COMPRESSION]]),
"Eve", None, True),
("3-element fixarray [name, nil, []] (no SF flags set)",
umsgpack.packb([b"Faye", None, []]),
"Faye", None, False),
]
for label, blob, want_name, want_cost, want_compress in cases:
got_name = LXMF_helpers.display_name_from_app_data(blob)
got_cost = LXMF_helpers.stamp_cost_from_app_data(blob)
got_compress = LXMF_helpers.compression_support_from_app_data(blob)
if got_name != want_name:
fail(f"S4.3 parser ({label}): display_name got {got_name!r} want {want_name!r}")
if got_cost != want_cost:
fail(f"S4.3 parser ({label}): stamp_cost got {got_cost!r} want {want_cost!r}")
if got_compress != want_compress:
fail(f"S4.3 parser ({label}): compression got {got_compress!r} want {want_compress!r}")
print("PASS S4.3 parser tolerance (raw UTF-8, 1/2/3-element msgpack array)")
def main():
print(f"verify_announce_app_data.py against RNS {RNS.__version__} / LXMF {LXMF.__version__}")
verify_two_element_wire_bytes()
verify_producer_is_two_element_in_this_lxmf()
verify_parser_tolerance()
print("ALL PASS")
if __name__ == "__main__":
main()