Merge main into archy-hwconfig — reconcile probe/dedup/name work
Both sides independently fixed the serial-alias dedup and the ESP32 boot-reset races; kept the branch's defer-to-auto-detect for unpinned preferred paths (single probe pass per cycle) on top of main's advert-name threading, Reticulum name propagation and radio-first routing. Modal keeps main's 'Set Recommended' naming + probe progress bar alongside the branch's in-app firmware flasher step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,12 +14,13 @@ Security posture (see the plan's "most secure way" section):
|
||||
RPC (one JSON object per line, both directions):
|
||||
in : {"cmd":"send","dest_hash":"<hex16>","content":"…","title":"…","method":"direct|opportunistic"}
|
||||
{"cmd":"announce"}
|
||||
{"cmd":"set_name","name":"…"}
|
||||
{"cmd":"status"}
|
||||
{"cmd":"send_resource","id":"<correlation>","dest_hash":"<hex16>","data_b64":"…"}
|
||||
{"cmd":"shutdown"}
|
||||
out: {"event":"ready","dest_hash":"<hex16>","display_name":"…"}
|
||||
{"event":"recv","source_hash":"<hex16>","content":"…","title":"…","fields":{…},"app_data":"<hex>","rssi":n,"snr":n,"stamp":t}
|
||||
{"event":"announce","dest_hash":"<hex16>","app_data":"<hex>"}
|
||||
{"event":"announce","dest_hash":"<hex16>","app_data":"<hex>","display_name":"…"|null,"archy_blob":"ARCHY:2:…"|null}
|
||||
{"event":"delivered","dest_hash":"<hex16>","state":"delivered|failed","id":"<hex>"}
|
||||
{"event":"status","connected":bool,"dest_hash":"<hex16>","interfaces":[…]}
|
||||
{"event":"resource_progress","id":"<correlation>","transferred":n,"total":n}
|
||||
@@ -227,31 +228,47 @@ class ReticulumDaemon:
|
||||
if self.delivery_destination is not None:
|
||||
self.delivery_destination.announce(app_data=self._announce_app_data())
|
||||
|
||||
def _announce_app_data(self) -> bytes:
|
||||
"""Carry the Archy identity so peers bind this RNS destination onto the
|
||||
existing contact, the same way a meshcore/Meshtastic identity advert does.
|
||||
|
||||
Reuses the exact ``ARCHY:2:{ed25519_hex}:{x25519_hex}`` wire format the
|
||||
Rust side already parses (``protocol::parse_identity_broadcast``) and
|
||||
binds via ``handle_identity_received``/``bind_federation_twins`` — so a
|
||||
Reticulum-carried identity merges into the SAME conversation as the
|
||||
meshcore/Meshtastic/federation twins of the same Archy node, satisfying
|
||||
cross-protocol DM convergence. The keys are the node's real Archipelago
|
||||
ed25519/x25519 pubkeys (passed in by the Rust side, which already has
|
||||
them) — NOT this daemon's internally-HKDF-derived RNS keys, which exist
|
||||
only to make the RNS destination hash deterministic and are never
|
||||
themselves treated as an Archy identity.
|
||||
|
||||
Falls back to a plain display-name string (undetected as an identity
|
||||
blob — no `ARCHY:2:` prefix) if the Archy pubkeys weren't supplied, e.g.
|
||||
a dev/selftest run with no `--archy-ed-pubkey-hex`.
|
||||
"""
|
||||
def _archy_identity_blob(self):
|
||||
"""The ``ARCHY:2:{ed25519_hex}:{x25519_hex}`` identity string the Rust
|
||||
side parses (``protocol::parse_identity_broadcast``) and binds via
|
||||
``handle_identity_received`` — so a Reticulum-carried identity merges
|
||||
into the SAME conversation as the meshcore/Meshtastic/federation twins
|
||||
of the same Archy node. The keys are the node's real Archipelago
|
||||
pubkeys (passed in by the Rust side) — NOT this daemon's
|
||||
internally-HKDF-derived RNS keys, which exist only to make the RNS
|
||||
destination hash deterministic. ``None`` when the pubkeys weren't
|
||||
supplied (dev/selftest run)."""
|
||||
if self.args.archy_ed_pubkey_hex and self.args.archy_x25519_pubkey_hex:
|
||||
return (
|
||||
f"ARCHY:2:{self.args.archy_ed_pubkey_hex}:"
|
||||
f"{self.args.archy_x25519_pubkey_hex}"
|
||||
).encode("ascii")
|
||||
return (self.args.display_name or "").encode("utf-8")
|
||||
return None
|
||||
|
||||
def _announce_app_data(self) -> bytes:
|
||||
"""LXMF-standard announce app_data — msgpack ``[display_name,
|
||||
stamp_cost, supported_functionality]`` via the router, so Sideband/
|
||||
NomadNet/MeshChat (and upgraded archy nodes) all see our real display
|
||||
name — with the Archy identity blob appended as an EXTRA list element.
|
||||
Stock clients only read the elements they know ([0]/[1]), so the blob
|
||||
rides along invisibly instead of replacing the name the way the old
|
||||
blob-only app_data did (which left every archy node nameless on RNS).
|
||||
"""
|
||||
import RNS.vendor.umsgpack as msgpack
|
||||
app_data = self.router.get_announce_app_data(self.delivery_destination.hash)
|
||||
blob = self._archy_identity_blob()
|
||||
if blob is None:
|
||||
return app_data
|
||||
try:
|
||||
peer_data = msgpack.unpackb(app_data)
|
||||
if not isinstance(peer_data, list):
|
||||
raise ValueError("unexpected announce app_data shape")
|
||||
peer_data.append(blob)
|
||||
return msgpack.packb(peer_data)
|
||||
except Exception:
|
||||
# Never let announce formatting kill announcing entirely — fall
|
||||
# back to the legacy blob-only format (identity binding > name).
|
||||
return blob
|
||||
|
||||
# ---- RNS-thread callbacks → asyncio ----
|
||||
def _on_lxmf_delivery(self, message):
|
||||
@@ -328,6 +345,15 @@ class ReticulumDaemon:
|
||||
self._send(req)
|
||||
elif cmd == "announce":
|
||||
self.announce()
|
||||
elif cmd == "set_name":
|
||||
# Live rename: update the LXMF delivery destination's display name
|
||||
# (what get_announce_app_data reads) and re-announce immediately so
|
||||
# peers learn the new name without waiting for the next advert tick.
|
||||
name = (req.get("name") or "").strip()
|
||||
if name and self.delivery_destination is not None:
|
||||
self.args.display_name = name
|
||||
self.delivery_destination.display_name = name
|
||||
self.announce()
|
||||
elif cmd == "status":
|
||||
self._broadcast(self._status())
|
||||
elif cmd == "send_resource":
|
||||
@@ -521,10 +547,41 @@ class _AnnounceHandler:
|
||||
self.receive_path_responses = True
|
||||
|
||||
def received_announce(self, destination_hash, announced_identity, app_data):
|
||||
# Decode what we can here (both the LXMF-standard display name and our
|
||||
# appended ARCHY identity blob — see _announce_app_data) so the Rust
|
||||
# side gets clean typed fields instead of re-implementing msgpack.
|
||||
display_name = None
|
||||
archy_blob = None
|
||||
raw = app_data or b""
|
||||
try:
|
||||
import LXMF
|
||||
display_name = LXMF.display_name_from_app_data(raw)
|
||||
# A legacy blob-only announce is plain ascii, so LXMF's decoder
|
||||
# returns the whole ARCHY identity blob as a "name" — drop it.
|
||||
if display_name and display_name.startswith("ARCHY:"):
|
||||
display_name = None
|
||||
except Exception:
|
||||
display_name = None
|
||||
try:
|
||||
if raw[:1] and ((0x90 <= raw[0] <= 0x9F) or raw[0] == 0xDC):
|
||||
import RNS.vendor.umsgpack as msgpack
|
||||
peer_data = msgpack.unpackb(raw)
|
||||
if isinstance(peer_data, list):
|
||||
for el in peer_data[3:]:
|
||||
if isinstance(el, bytes) and el.startswith(b"ARCHY:"):
|
||||
archy_blob = el.decode("ascii", "ignore")
|
||||
break
|
||||
elif raw.startswith(b"ARCHY:"):
|
||||
# Legacy (pre-upgrade archy node): app_data IS the blob.
|
||||
archy_blob = raw.decode("ascii", "ignore")
|
||||
except Exception:
|
||||
archy_blob = None
|
||||
self.daemon._emit_threadsafe({
|
||||
"event": "announce",
|
||||
"dest_hash": destination_hash.hex(),
|
||||
"app_data": (app_data or b"").hex(),
|
||||
"app_data": raw.hex(),
|
||||
"display_name": display_name,
|
||||
"archy_blob": archy_blob,
|
||||
})
|
||||
|
||||
|
||||
@@ -604,8 +661,30 @@ def main(argv=None) -> int:
|
||||
if args.selftest:
|
||||
args.no_radio = True
|
||||
daemon.bring_up()
|
||||
# Announce app_data round-trip: the LXMF-standard msgpack name must be
|
||||
# decodable by stock clients AND (with archy keys set) the appended
|
||||
# identity blob must survive as an extra list element — this is the
|
||||
# exact wire contract the Rust announce handler and Sideband both
|
||||
# depend on, so verify it here where there's a real router to build it.
|
||||
import LXMF as _LXMF
|
||||
import RNS.vendor.umsgpack as _msgpack
|
||||
args.archy_ed_pubkey_hex = args.archy_ed_pubkey_hex or "ab" * 32
|
||||
args.archy_x25519_pubkey_hex = args.archy_x25519_pubkey_hex or "cd" * 32
|
||||
app_data = daemon._announce_app_data()
|
||||
decoded_name = _LXMF.display_name_from_app_data(app_data)
|
||||
assert decoded_name == args.display_name, (
|
||||
f"announce name round-trip failed: {decoded_name!r} != {args.display_name!r}"
|
||||
)
|
||||
blob_elems = [e for e in _msgpack.unpackb(app_data)[3:]
|
||||
if isinstance(e, bytes) and e.startswith(b"ARCHY:")]
|
||||
assert blob_elems, "identity blob missing from announce app_data"
|
||||
# Live rename: set_name must change what the next announce carries.
|
||||
daemon.delivery_destination.display_name = "selftest-renamed"
|
||||
renamed = _LXMF.display_name_from_app_data(daemon._announce_app_data())
|
||||
assert renamed == "selftest-renamed", f"rename round-trip failed: {renamed!r}"
|
||||
print(f"selftest ok — dest_hash={daemon.dest_hash_hex} "
|
||||
f"display_name={args.display_name!r} lxmf_router=up")
|
||||
f"display_name={args.display_name!r} lxmf_router=up "
|
||||
f"announce_app_data=verified set_name=verified")
|
||||
return 0
|
||||
|
||||
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||
|
||||
Reference in New Issue
Block a user