fix(mesh): bind Reticulum peer identity directly, not via name-matching
Two MeshPeer rows were being created for one physical Reticulum node: a radio twin (keyed by the RNS dest_hash, arch_pubkey_hex always None) and a pseudo-federation twin (keyed by the archy ed25519 pubkey, created via the generic identity-broadcast path meant for Meshcore/Meshtastic). The generic path relies on bind_federation_twins matching both twins' advert_name, but the Reticulum radio twin's display_name is deliberately never the identity text — so the two rows could never merge, and the generic send path (keyed off whichever twin the caller resolves) ended up looking up the archy pubkey's prefix in ReticulumLink's `prefix_to_hash` map, which is only ever populated with RNS dest_hash prefixes. Every send failed with "Unknown Reticulum prefix ... peer hasn't announced yet", confirmed live between two real nodes (archy-x250-exp / archy-x250-pa) that could see each other's adverts but never exchange a message. Unlike Meshcore/Meshtastic, Reticulum's ARCHY identity blob arrives in the same announce event as the destination hash, so there's no ambiguity about which peer it belongs to — bind it directly onto the RNS-hash-keyed radio peer instead of relying on name-matching. Threaded through a new `ParsedContact::arch_pubkey_hex` (None for Meshcore/Meshtastic, unchanged behavior there) so `refresh_contacts` can set it on the correct peer row without touching `bind_federation_twins`/`group_peer_twins`, which already know how to collapse twins once they share an arch_pubkey_hex. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
893508b700
commit
458b9fbbb7
@ -679,11 +679,19 @@ async fn refresh_contacts(device: &mut MeshRadioDevice, state: &Arc<MeshState>)
|
|||||||
advert_name: contact.advert_name.clone(),
|
advert_name: contact.advert_name.clone(),
|
||||||
did: existing.and_then(|p| p.did.clone()),
|
did: existing.and_then(|p| p.did.clone()),
|
||||||
pubkey_hex: Some(contact.public_key_hex.clone()),
|
pubkey_hex: Some(contact.public_key_hex.clone()),
|
||||||
// Preserve any archipelago identity bound by an earlier
|
// Reticulum carries the archipelago identity in-band with
|
||||||
// identity advert — NEVER overwrite it with the firmware
|
// the contact snapshot itself (see `ParsedContact::
|
||||||
// contact key, or a signed `!ai` query from this peer would
|
// arch_pubkey_hex`'s doc comment) — prefer it when this
|
||||||
// fail authentication after the next contact refresh.
|
// refresh's snapshot has one. Otherwise preserve whatever
|
||||||
arch_pubkey_hex: existing.and_then(|p| p.arch_pubkey_hex.clone()),
|
// was bound by an earlier identity advert (Meshcore/
|
||||||
|
// Meshtastic's `bind_federation_twins` path): NEVER
|
||||||
|
// overwrite a known identity with the firmware contact
|
||||||
|
// key, or a signed `!ai` query from this peer would fail
|
||||||
|
// authentication after the next contact refresh.
|
||||||
|
arch_pubkey_hex: contact
|
||||||
|
.arch_pubkey_hex
|
||||||
|
.clone()
|
||||||
|
.or_else(|| existing.and_then(|p| p.arch_pubkey_hex.clone())),
|
||||||
x25519_pubkey: existing.and_then(|p| p.x25519_pubkey),
|
x25519_pubkey: existing.and_then(|p| p.x25519_pubkey),
|
||||||
// Meshtastic-only today (see ParsedContact) — falls back to
|
// Meshtastic-only today (see ParsedContact) — falls back to
|
||||||
// whatever was already known if this refresh's contact
|
// whatever was already known if this refresh's contact
|
||||||
|
|||||||
@ -982,6 +982,7 @@ impl MeshtasticDevice {
|
|||||||
snr,
|
snr,
|
||||||
lat,
|
lat,
|
||||||
lon,
|
lon,
|
||||||
|
arch_pubkey_hex: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1054,6 +1055,7 @@ fn packet_to_inbound_frame(
|
|||||||
snr: None,
|
snr: None,
|
||||||
lat: None,
|
lat: None,
|
||||||
lon: None,
|
lon: None,
|
||||||
|
arch_pubkey_hex: None,
|
||||||
});
|
});
|
||||||
if packet.rx_rssi.is_some() {
|
if packet.rx_rssi.is_some() {
|
||||||
contact.rssi = packet.rx_rssi.map(|v| v as i16);
|
contact.rssi = packet.rx_rssi.map(|v| v as i16);
|
||||||
|
|||||||
@ -406,6 +406,15 @@ pub struct ParsedContact {
|
|||||||
/// contact has shared one.
|
/// contact has shared one.
|
||||||
pub lat: Option<f64>,
|
pub lat: Option<f64>,
|
||||||
pub lon: Option<f64>,
|
pub lon: Option<f64>,
|
||||||
|
/// Archipelago ed25519 identity hex, when this transport carried it
|
||||||
|
/// in-band with the contact announce itself (Reticulum only today — the
|
||||||
|
/// RNS announce's app_data can embed an `ARCHY:n:` identity blob
|
||||||
|
/// alongside the destination hash in the same event, so there's no
|
||||||
|
/// ambiguity about which physical peer it belongs to). Meshcore/
|
||||||
|
/// Meshtastic identity adverts go out on a separate channel and are
|
||||||
|
/// correlated after the fact by `bind_federation_twins`'s advert_name
|
||||||
|
/// matching instead, so they always leave this `None`.
|
||||||
|
pub arch_pubkey_hex: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse RESP_CONTACT (0x03) response.
|
/// Parse RESP_CONTACT (0x03) response.
|
||||||
@ -457,6 +466,7 @@ pub fn parse_contact(data: &[u8]) -> Result<ParsedContact> {
|
|||||||
snr: None,
|
snr: None,
|
||||||
lat: None,
|
lat: None,
|
||||||
lon: None,
|
lon: None,
|
||||||
|
arch_pubkey_hex: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -169,9 +169,12 @@ fn daemon_command(
|
|||||||
struct ReticulumPeer {
|
struct ReticulumPeer {
|
||||||
dest_hash: [u8; 16],
|
dest_hash: [u8; 16],
|
||||||
display_name: String,
|
display_name: String,
|
||||||
/// Archy ed25519 identity hex, once carried in a verified announce
|
/// Archy ed25519 identity hex, once carried in this peer's own announce
|
||||||
/// app-data blob. Not yet wired (TODO, lands with the signed-announce
|
/// app-data blob (`ARCHY:n:...`) — see `handle_event`'s "announce" arm.
|
||||||
/// work) — present so `get_contacts` has a stable shape to extend into.
|
/// Unlike Meshcore/Meshtastic, this arrives in-band with the same event
|
||||||
|
/// as the destination hash, so it can be bound directly onto this
|
||||||
|
/// RNS-hash-keyed peer with no name-matching ambiguity (contrast
|
||||||
|
/// `bind_federation_twins`, which those two transports rely on instead).
|
||||||
arch_pubkey_hex: Option<String>,
|
arch_pubkey_hex: Option<String>,
|
||||||
reachable: bool,
|
reachable: bool,
|
||||||
}
|
}
|
||||||
@ -645,6 +648,7 @@ impl ReticulumLink {
|
|||||||
snr: None,
|
snr: None,
|
||||||
lat: None,
|
lat: None,
|
||||||
lon: None,
|
lon: None,
|
||||||
|
arch_pubkey_hex: p.arch_pubkey_hex.clone(),
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
@ -723,17 +727,21 @@ impl ReticulumLink {
|
|||||||
.filter(|s| !s.is_empty());
|
.filter(|s| !s.is_empty());
|
||||||
|
|
||||||
// If the announce app_data is an ARCHY:n: identity blob (see
|
// If the announce app_data is an ARCHY:n: identity blob (see
|
||||||
// daemon_command's doc comment), bind it onto this peer AND
|
// daemon_command's doc comment), bind the ed25519 hex directly
|
||||||
// surface it through the SAME channel-text path
|
// onto this RNS-hash-keyed peer below (unambiguous — it came
|
||||||
// meshcore/Meshtastic identity adverts use
|
// in the same event as `hash`) AND surface it through the same
|
||||||
|
// channel-text path meshcore/Meshtastic identity adverts use
|
||||||
// (frames::handle_channel_payload -> parse_identity_broadcast
|
// (frames::handle_channel_payload -> parse_identity_broadcast
|
||||||
// -> handle_identity_received -> bind_federation_twins), so a
|
// -> handle_identity_received), so it also lands on that
|
||||||
// Reticulum-carried identity merges into the same conversation
|
// transport's federation-twin peer for UI/contact purposes.
|
||||||
// as that node's other-transport twins — zero new bind logic.
|
// `group_peer_twins` can then collapse the two rows since both
|
||||||
let is_identity_blob = app_data_text
|
// now carry the same `arch_pubkey_hex`, instead of relying on
|
||||||
|
// `bind_federation_twins`'s advert_name matching, which never
|
||||||
|
// matches here — see `display_name` below.
|
||||||
|
let parsed_identity = app_data_text
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(|t| protocol::parse_identity_broadcast(t).is_some())
|
.and_then(protocol::parse_identity_broadcast);
|
||||||
.unwrap_or(false);
|
let is_identity_blob = parsed_identity.is_some();
|
||||||
if is_identity_blob {
|
if is_identity_blob {
|
||||||
let text = app_data_text.clone().unwrap();
|
let text = app_data_text.clone().unwrap();
|
||||||
let mut data = Vec::with_capacity(7 + text.len());
|
let mut data = Vec::with_capacity(7 + text.len());
|
||||||
@ -746,6 +754,7 @@ impl ReticulumLink {
|
|||||||
bytes_consumed: 0,
|
bytes_consumed: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
let arch_pubkey_hex = parsed_identity.map(|(_did, ed_pubkey, _x25519)| ed_pubkey);
|
||||||
|
|
||||||
let display_name = app_data_text
|
let display_name = app_data_text
|
||||||
.filter(|_| !is_identity_blob)
|
.filter(|_| !is_identity_blob)
|
||||||
@ -755,11 +764,14 @@ impl ReticulumLink {
|
|||||||
.and_modify(|p| {
|
.and_modify(|p| {
|
||||||
p.display_name = display_name.clone();
|
p.display_name = display_name.clone();
|
||||||
p.reachable = true;
|
p.reachable = true;
|
||||||
|
if arch_pubkey_hex.is_some() {
|
||||||
|
p.arch_pubkey_hex = arch_pubkey_hex.clone();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.or_insert(ReticulumPeer {
|
.or_insert(ReticulumPeer {
|
||||||
dest_hash: hash,
|
dest_hash: hash,
|
||||||
display_name,
|
display_name,
|
||||||
arch_pubkey_hex: None,
|
arch_pubkey_hex,
|
||||||
reachable: true,
|
reachable: true,
|
||||||
});
|
});
|
||||||
self.persist_peers();
|
self.persist_peers();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user