From edc9a172e95329b4b462d9cf8f7478a0c10852e3 Mon Sep 17 00:00:00 2001 From: archipelago Date: Tue, 4 Aug 2026 00:35:02 -0400 Subject: [PATCH] fix(mesh): federated peers are messageable without meeting over LoRa first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Peering a node was not enough to message it — you had to be in radio range once before chat worked, which defeats the point of federating. `send_message` chose its transport from the attached radio: let use_typed_envelope = archy && matches!(device_type, Meshcore | Reticulum); Only the typed path knows about FIPS/Tor. Everything else fell through to `peer_dest_prefix`, which resolves an over-the-air ROUTING key — so on a node running Meshtastic, or with no radio at all, sending to a federated peer failed. It only worked once a LoRa advert had created a radio twin for the same archipelago identity, which is precisely the "connect on LoRa first" the operator hit. Federation contacts are reachable off-radio by definition — that is what `upsert_federation_peer` records with `reachable: true` — so the transport choice must not depend on which radio is plugged in. A federation-synthetic contact id now always takes the typed path. This loses no radio-first behaviour: `send_typed_wire` already prefers a REACHABLE radio twin when the payload fits the frame, and only then falls back to FIPS and Tor. The fix routes federation contacts INTO that logic rather than around it. Test pins the predicate across every device type, including the two that failed (Meshtastic, Unknown), and asserts ordinary radio contacts and stock clients still route exactly as before. Co-Authored-By: Claude Opus 5 (1M context) --- core/archipelago/src/mesh/mod.rs | 63 +++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/core/archipelago/src/mesh/mod.rs b/core/archipelago/src/mesh/mod.rs index 0e475c6b..bf0b3e66 100644 --- a/core/archipelago/src/mesh/mod.rs +++ b/core/archipelago/src/mesh/mod.rs @@ -1901,8 +1901,23 @@ impl MeshService { // • Meshcore stock client → plain text (can't decode our envelope). // Rich typed messages (invoice/coordinate/reaction/…) always use the // typed-wire path via `send_typed_wire`; only plain Text is routed here. - let use_typed_envelope = - archy && matches!(device_type, DeviceType::Meshcore | DeviceType::Reticulum); + // A federation-synthetic contact ALWAYS takes the typed path, whatever + // radio (if any) is attached. `send_typed_wire` is the only routing + // that knows about FIPS/Tor, and it still prefers a reachable LoRa + // twin when the payload fits — so this loses no radio-first behaviour. + // + // Without this, a plain text message to a federated peer fell through + // to `peer_dest_prefix`, which resolves a RADIO routing key. On a node + // running Meshtastic — or with no radio at all — that fails, which is + // why peering a node was not enough to message it: you had to meet it + // over LoRa first so a radio twin existed to route through. Federation + // peers are reachable off-radio by definition (that is what + // `upsert_federation_peer` records with `reachable: true`), so the + // transport choice must not depend on which radio is plugged in. + let is_federation_contact = contact_id & 0x8000_0000 != 0; + let use_typed_envelope = archy + && (is_federation_contact + || matches!(device_type, DeviceType::Meshcore | DeviceType::Reticulum)); if use_typed_envelope { // Sign with our archipelago identity so the receiver can authenticate // us over LoRa (verifies against our bound `arch_pubkey_hex`). `with_seq` @@ -2360,6 +2375,50 @@ async fn bitcoin_rpc_getblockheader_by_height( #[cfg(test)] mod tests { + + /// Item 5: a federated/trusted peer must be messageable as soon as it is + /// peered — no LoRa meeting first. + /// + /// The routing predicate in `send_message` decides whether a plain text + /// message takes the federation-aware typed path (which knows FIPS/Tor and + /// still prefers a reachable radio twin) or the radio-only path, which + /// resolves an over-the-air routing key and cannot work for a peer we have + /// never heard on the radio. + /// + /// It previously keyed on the attached radio, so on a Meshtastic node — or + /// one with no radio at all — a federated peer fell to the radio path and + /// the send failed. Federation contacts are reachable off-radio by + /// definition, so the choice must not depend on which radio is plugged in. + #[test] + fn federation_contacts_take_the_off_radio_path_on_any_device() { + fn uses_typed_path(contact_id: u32, archy: bool, device: DeviceType) -> bool { + let is_federation_contact = contact_id & 0x8000_0000 != 0; + archy + && (is_federation_contact + || matches!(device, DeviceType::Meshcore | DeviceType::Reticulum)) + } + let fed = super::federation_peer_contact_id(&"ab".repeat(32)); + assert!(fed >= FEDERATION_CONTACT_ID_BASE); + + // The cases that used to fail: peered node, wrong radio or none. + for device in [ + DeviceType::Meshtastic, + DeviceType::Unknown, + DeviceType::Meshcore, + DeviceType::Reticulum, + ] { + assert!( + uses_typed_path(fed, true, device), + "federation peer must route off-radio on {device:?}" + ); + } + + // A plain radio contact on a stock-text device still takes the radio + // path — this fix must not reroute ordinary LoRa chats. + assert!(!uses_typed_path(42, true, DeviceType::Meshtastic)); + // And a stock (non-archy) client is never given a typed envelope. + assert!(!uses_typed_path(42, false, DeviceType::Meshcore)); + } use super::*; #[test]