fix(mesh): Reticulum resource transfers actually deliver — 4 root causes
E2E-verified both directions dev-box<->x250 over real RF (5KB image ~60s):
1. Sender daemon never called link.identify() — receiver's
get_remote_identity() was None, so every arrived transfer carried an
empty source_hash and the Rust side dropped it (now also warns
instead of silently vanishing it).
2. Receiver treated resource.data as bytes, but RNS hands a concluded
Resource's data as a file-like BufferedReader — b64encode raised
TypeError and the transfer was lost even when attributed.
3. Radio twins of merged contacts carry the peer's Archipelago ed25519
key as pubkey_hex, not an RNS hash — prefix lookup could never match
('Unknown Reticulum prefix', observed live). resolve_dest_hash now
falls back to matching the announce-bound arch_pubkey_hex.
4. The daemon RPC socket kept asyncio's default 64KiB line limit; any
attachment >~48KB overflowed it and tore down the whole daemon
connection ('reticulum-daemon is gone'). Raised to 16MiB.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
79c3cc5947
commit
e62f911810
@@ -525,6 +525,28 @@ impl ReticulumLink {
|
||||
info!(count = self.peers.len(), "Loaded persisted Reticulum peers");
|
||||
}
|
||||
|
||||
/// Resolve a caller-supplied 6-byte routing prefix to a full 16-byte RNS
|
||||
/// destination hash. Two shapes arrive here depending on how the contact
|
||||
/// record was born (observed live 2026-07-28, image-to-merged-contact):
|
||||
/// 1. the peer's RNS dest-hash prefix (contacts created from an RNS
|
||||
/// announce) — direct `prefix_to_hash` hit;
|
||||
/// 2. the peer's Archipelago ed25519 pubkey prefix (radio twins bound
|
||||
/// via the ARCHY announce blob store the arch key as `pubkey_hex`) —
|
||||
/// no dest-hash match possible, so fall back to scanning peers whose
|
||||
/// announce-bound `arch_pubkey_hex` starts with the prefix.
|
||||
fn resolve_dest_hash(&self, prefix: &[u8; 6]) -> Option<[u8; 16]> {
|
||||
if let Some(hash) = self.prefix_to_hash.get(prefix) {
|
||||
return Some(*hash);
|
||||
}
|
||||
let hex_prefix = hex::encode(prefix);
|
||||
self.peers.values().find_map(|p| {
|
||||
p.arch_pubkey_hex
|
||||
.as_deref()
|
||||
.filter(|arch| arch.starts_with(&hex_prefix))
|
||||
.map(|_| p.dest_hash)
|
||||
})
|
||||
}
|
||||
|
||||
/// Best-effort sync write of the current peer table — called after any
|
||||
/// insert that adds/renames a peer. Infrequent (announces/first-contact,
|
||||
/// not per-message) so a blocking write here is a fine trade for keeping
|
||||
@@ -601,16 +623,12 @@ impl ReticulumLink {
|
||||
dest_pubkey_prefix: &[u8; 6],
|
||||
payload: &[u8],
|
||||
) -> Result<()> {
|
||||
let dest_hash = self
|
||||
.prefix_to_hash
|
||||
.get(dest_pubkey_prefix)
|
||||
.copied()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Unknown Reticulum prefix {} — peer hasn't announced yet",
|
||||
hex::encode(dest_pubkey_prefix)
|
||||
)
|
||||
})?;
|
||||
let dest_hash = self.resolve_dest_hash(dest_pubkey_prefix).with_context(|| {
|
||||
format!(
|
||||
"Unknown Reticulum prefix {} — peer hasn't announced yet",
|
||||
hex::encode(dest_pubkey_prefix)
|
||||
)
|
||||
})?;
|
||||
// Typed-envelope payloads (ReadReceipt/Reaction/etc. — anything small
|
||||
// enough for the single-frame path) are raw binary CBOR, not text.
|
||||
// `from_utf8_lossy` would irreversibly mangle them since `content`
|
||||
@@ -645,16 +663,12 @@ impl ReticulumLink {
|
||||
caption: Option<&str>,
|
||||
) -> Result<()> {
|
||||
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
|
||||
let dest_hash = self
|
||||
.prefix_to_hash
|
||||
.get(dest_pubkey_prefix)
|
||||
.copied()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Unknown Reticulum prefix {} — peer hasn't announced yet",
|
||||
hex::encode(dest_pubkey_prefix)
|
||||
)
|
||||
})?;
|
||||
let dest_hash = self.resolve_dest_hash(dest_pubkey_prefix).with_context(|| {
|
||||
format!(
|
||||
"Unknown Reticulum prefix {} — peer hasn't announced yet",
|
||||
hex::encode(dest_pubkey_prefix)
|
||||
)
|
||||
})?;
|
||||
self.send_rpc(serde_json::json!({
|
||||
"cmd": "send",
|
||||
"dest_hash": hex::encode(dest_hash),
|
||||
@@ -677,16 +691,12 @@ impl ReticulumLink {
|
||||
/// `handle_event`, not awaited here.
|
||||
pub async fn send_resource(&mut self, dest_pubkey_prefix: &[u8; 6], data: &[u8]) -> Result<()> {
|
||||
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
|
||||
let dest_hash = self
|
||||
.prefix_to_hash
|
||||
.get(dest_pubkey_prefix)
|
||||
.copied()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Unknown Reticulum prefix {} — peer hasn't announced yet",
|
||||
hex::encode(dest_pubkey_prefix)
|
||||
)
|
||||
})?;
|
||||
let dest_hash = self.resolve_dest_hash(dest_pubkey_prefix).with_context(|| {
|
||||
format!(
|
||||
"Unknown Reticulum prefix {} — peer hasn't announced yet",
|
||||
hex::encode(dest_pubkey_prefix)
|
||||
)
|
||||
})?;
|
||||
let req_id = self.next_resource_id();
|
||||
self.send_rpc(serde_json::json!({
|
||||
"cmd": "send_resource",
|
||||
@@ -1023,10 +1033,19 @@ impl ReticulumLink {
|
||||
.push_back(build_synthetic_frame(&prefix, &content));
|
||||
}
|
||||
Some("resource_recv") => {
|
||||
let Some(source_hex) = ev.get("source_hash").and_then(Value::as_str) else {
|
||||
return;
|
||||
};
|
||||
let source_hex = ev
|
||||
.get("source_hash")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let Ok(source_hash) = parse_hash16(source_hex) else {
|
||||
// An empty source_hash means the sender's link wasn't
|
||||
// identified (pre-identify daemon build on the far end) —
|
||||
// the blob is undeliverable without attribution, but say
|
||||
// so instead of vanishing it.
|
||||
warn!(
|
||||
source = source_hex,
|
||||
"Dropping inbound Reticulum resource without valid source identity"
|
||||
);
|
||||
return;
|
||||
};
|
||||
let prefix: [u8; 6] = source_hash[..6].try_into().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user