Add four more verifiers + receive-propagated flow + frontmatter version
Verifiers:
tools/verify_proof_packet.py — locks in §6.5. Toggles
Reticulum.__use_implicit_proof to test both modes; confirms
Identity.prove emits 64B (implicit) or 96B (explicit) proof
body; PacketReceipt.validate_proof accepts both lengths and
rejects an 80B body.
tools/verify_link_handshake.py — locks in §6.1, §6.2, §6.3, §6.6.
Most importantly verifies the previously-corrected §6.2 LRPROOF
body order (signature(64) || responder_X25519_pub(32) ||
[signalling]) and §6.3 link_id offsets (N=2 for HEADER_1) by
actually building a Link initiator-side, capturing the
LINKREQUEST raw bytes, computing link_id by the spec recipe,
running validate_request inline (since the upstream wrapper
swallows exceptions), and confirming the responder's LRPROOF
bytes match the spec layout. This was the single most
interop-critical correction we made.
tools/verify_rnode_split.py — locks in §8.3. Pure-function
re-implementation of the canonical TX and RX state machines
from RNode_Firmware.ino:359-446 + 716-742; tests header-byte
layout, single-frame TX, split-frame TX (300B → 254+46 with
shared header byte), all four RX state-machine cases (a/b/c/d
from the spec table), and end-to-end TX/RX round-trip at
sizes 50, 254, 255, 300, 508.
tools/verify_msgpack_quirk.py — locks in §9.3. Confirms umsgpack
distinguishes str (fixstr/0xa5) from bytes (bin8/0xc4); confirms
LXMF.display_name_from_app_data parses bytes-encoded display
names correctly and silently returns None (not crash) on
str-encoded ones, matching the bug-tolerance documented in §9.3.
All 11 verifiers pass against RNS 1.2.0 / LXMF 0.9.6.
Plus:
- SPEC.md frontmatter: 'Last verified against' line per agent.md §7.
- flows/receive-propagated-lxmf.md: closing half of the propagated
LXMF lifecycle. /get listing query, fetch query, ack-and-purge
via the have_ids slot, message-bundle unpack and dispatch
through lxmf_delivery.
- tools/README.md status table refreshed; flows/README.md flips
receive-propagated-lxmf.md to ✅.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
75169b0631
commit
abf66b9cef
8 changed files with 990 additions and 6 deletions
119
tools/verify_msgpack_quirk.py
Normal file
119
tools/verify_msgpack_quirk.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
"""
|
||||
Verifier for SPEC.md S9.3 (RNS bundles `umsgpack` — encode display
|
||||
names as bytes, not str).
|
||||
|
||||
The bundled `RNS.vendor.umsgpack` distinguishes between Python `str`
|
||||
(encoded as msgpack `fixstr/str8/str16/str32` types `0xa0..0xbf`,
|
||||
`0xd9`, `0xda`, `0xdb`) and Python `bytes` (encoded as `bin8/bin16/
|
||||
bin32` types `0xc4`, `0xc5`, `0xc6`). The downstream LXMF parser at
|
||||
`LXMF/LXMF.py:131` does `dn.decode("utf-8")` on the unpacked first
|
||||
element — this only works when the producer used `bytes` (so umsgpack
|
||||
unpacks as Python `bytes` which has `.decode`).
|
||||
|
||||
If a producer uses `str` instead, umsgpack unpacks back to a Python
|
||||
`str` which has no `.decode("utf-8")` method, the LXMF parser raises
|
||||
AttributeError, and the message's display name is silently lost.
|
||||
|
||||
This verifier confirms:
|
||||
|
||||
1. umsgpack.packb on a `str` produces fixstr/str8/str16/str32 prefix
|
||||
bytes; on `bytes` produces bin8/bin16/bin32.
|
||||
2. umsgpack.unpackb round-trips a `bytes` value back to Python `bytes`,
|
||||
and a `str` value back to Python `str`.
|
||||
3. LXMF.display_name_from_app_data returns the decoded display name
|
||||
for a bytes-encoded producer and returns None (with no crash) for
|
||||
a str-encoded producer — i.e. the gotcha is real but contained.
|
||||
|
||||
Exit code 0 on PASS, non-zero on FAIL.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
import RNS
|
||||
import RNS.vendor.umsgpack as umsgpack
|
||||
import LXMF
|
||||
from LXMF import LXMF as LXMF_helpers
|
||||
|
||||
|
||||
def fail(msg: str) -> None:
|
||||
print(f"FAIL: {msg}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def verify_pack_str_uses_str_prefix():
|
||||
# 5-character str fits in fixstr (0xa0..0xbf with the length in the low 5 bits)
|
||||
blob = umsgpack.packb("hello")
|
||||
if blob[0] not in (0xa5, 0xd9, 0xda, 0xdb):
|
||||
# 0xa5 = fixstr length 5 (0xa0 | 5)
|
||||
fail(f"umsgpack.packb('hello') produced unexpected first byte 0x{blob[0]:02x}; "
|
||||
f"want fixstr 0xa5 or str8/16/32")
|
||||
if blob[0] != 0xa5:
|
||||
fail(f"umsgpack.packb('hello') did not use fixstr; got 0x{blob[0]:02x}")
|
||||
print(f"PASS S9.3 packb('hello') -> 0x{blob[0]:02x}: fixstr (str family)")
|
||||
|
||||
|
||||
def verify_pack_bytes_uses_bin_prefix():
|
||||
# 5-byte bytes uses bin8 (0xc4 NN ...) — there's no fixbin
|
||||
blob = umsgpack.packb(b"hello")
|
||||
if blob[0] not in (0xc4, 0xc5, 0xc6):
|
||||
fail(f"umsgpack.packb(b'hello') produced unexpected first byte 0x{blob[0]:02x}; "
|
||||
f"want bin8/16/32")
|
||||
if blob[0] != 0xc4:
|
||||
fail(f"umsgpack.packb(b'hello') did not use bin8; got 0x{blob[0]:02x}")
|
||||
if blob[1] != 5:
|
||||
fail(f"bin8 length byte != 5: 0x{blob[1]:02x}")
|
||||
print(f"PASS S9.3 packb(b'hello') -> 0xc4 05: bin8 (bin family)")
|
||||
|
||||
|
||||
def verify_unpack_round_trip_str_vs_bytes():
|
||||
# str round-trips to str
|
||||
s = umsgpack.unpackb(umsgpack.packb("hello"))
|
||||
if not isinstance(s, str):
|
||||
fail(f"umsgpack.unpackb(packb('hello')) returned {type(s).__name__}, want str")
|
||||
|
||||
# bytes round-trips to bytes
|
||||
b = umsgpack.unpackb(umsgpack.packb(b"hello"))
|
||||
if not isinstance(b, bytes):
|
||||
fail(f"umsgpack.unpackb(packb(b'hello')) returned {type(b).__name__}, want bytes")
|
||||
|
||||
print("PASS S9.3 round-trip preserves str/bytes distinction")
|
||||
|
||||
|
||||
def verify_lxmf_display_name_parser_quirk():
|
||||
"""LXMF.display_name_from_app_data only works when the producer used bytes."""
|
||||
# 1. Correct producer: bytes-encoded display name in a 2-element array
|
||||
correct_blob = umsgpack.packb([b"AliceTest", None])
|
||||
name = LXMF_helpers.display_name_from_app_data(correct_blob)
|
||||
if name != "AliceTest":
|
||||
fail(f"S9.3 correct (bytes-encoded) display name parsed wrong: {name!r}")
|
||||
print("PASS S9.3 correct producer: msgpack([bytes, None]) -> 'AliceTest'")
|
||||
|
||||
# 2. Wrong producer: str-encoded display name. LXMF should NOT crash —
|
||||
# it should return None. (The except in display_name_from_app_data
|
||||
# at LXMF.py:133-135 logs the error and returns None.)
|
||||
wrong_blob = umsgpack.packb(["AliceTest", None])
|
||||
try:
|
||||
result = LXMF_helpers.display_name_from_app_data(wrong_blob)
|
||||
except Exception as e:
|
||||
fail(f"S9.3 LXMF parser crashed on str-encoded producer: {e}")
|
||||
|
||||
if result is not None:
|
||||
fail(f"S9.3 LXMF parser silently accepted str-encoded producer "
|
||||
f"(returned {result!r}); spec says this should fail to None")
|
||||
print("PASS S9.3 wrong producer: msgpack([str, None]) -> None "
|
||||
"(LXMF.py:131 .decode raises AttributeError, parser returns None)")
|
||||
|
||||
|
||||
def main():
|
||||
print(f"verify_msgpack_quirk.py against RNS {RNS.__version__} / LXMF {LXMF.__version__}")
|
||||
verify_pack_str_uses_str_prefix()
|
||||
verify_pack_bytes_uses_bin_prefix()
|
||||
verify_unpack_round_trip_str_vs_bytes()
|
||||
verify_lxmf_display_name_parser_quirk()
|
||||
print("ALL PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue