feat(p2p): mesh stack, LoRa mock transport, and relay demo

Implement transport abstraction (TCP/iroh), announce and routing table,
multi-hop mesh router, truncated-address link layer, and LoRa mock
medium with fragmentation plus EU868-style duty-cycle accounting.
Add mesh_lora_relay_demo and scripts/mesh-demo.sh. Relax CBOR vs JSON
size assertion to match fixed-size cryptographic overhead. Extend
.gitignore for nested targets and node_modules.

Made-with: Cursor
This commit is contained in:
2026-03-30 21:19:12 +02:00
parent d469999c2a
commit f9ac921a0c
20 changed files with 4042 additions and 6 deletions

View File

@@ -176,13 +176,31 @@ impl MeshEnvelope {
copy
}
/// Serialize to bytes (JSON).
/// Serialize to compact CBOR binary format (for wire transmission).
pub fn to_wire(&self) -> Vec<u8> {
let mut buf = Vec::new();
ciborium::into_writer(self, &mut buf).expect("CBOR serialization should not fail");
buf
}
/// Deserialize from CBOR binary format.
pub fn from_wire(bytes: &[u8]) -> anyhow::Result<Self> {
let env: Self = ciborium::from_reader(bytes)?;
Ok(env)
}
/// Deserialize from wire format, trying CBOR first then JSON fallback.
pub fn from_wire_or_json(bytes: &[u8]) -> anyhow::Result<Self> {
Self::from_wire(bytes).or_else(|_| Self::from_bytes(bytes))
}
/// Serialize to bytes (JSON). Kept for backward compatibility and debugging.
pub fn to_bytes(&self) -> Vec<u8> {
// serde_json::to_vec should not fail on a well-formed envelope.
serde_json::to_vec(self).expect("envelope serialization should not fail")
}
/// Deserialize from bytes (JSON).
/// Deserialize from bytes (JSON). Kept for backward compatibility and debugging.
pub fn from_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
let env: Self = serde_json::from_slice(bytes)?;
Ok(env)
@@ -293,4 +311,68 @@ mod tests {
assert!(env.recipient_key.is_empty());
assert!(env.verify());
}
#[test]
fn cbor_roundtrip() {
let id = test_identity();
let recipient = [0xABu8; 32];
let env = MeshEnvelope::new(&id, &recipient, b"cbor roundtrip".to_vec(), 3600, 5);
let wire = env.to_wire();
let restored = MeshEnvelope::from_wire(&wire).expect("CBOR deserialize");
assert_eq!(env.id, restored.id);
assert_eq!(env.sender_key, restored.sender_key);
assert_eq!(env.recipient_key, restored.recipient_key);
assert_eq!(env.payload, restored.payload);
assert_eq!(env.ttl_secs, restored.ttl_secs);
assert_eq!(env.hop_count, restored.hop_count);
assert_eq!(env.max_hops, restored.max_hops);
assert_eq!(env.timestamp, restored.timestamp);
assert_eq!(env.signature, restored.signature);
assert!(restored.verify());
}
#[test]
fn cbor_smaller_than_json() {
let id = test_identity();
let recipient = [0xCCu8; 32];
let payload = b"a typical chat message for size comparison testing".to_vec();
let env = MeshEnvelope::new(&id, &recipient, payload, 3600, 5);
let wire_len = env.to_wire().len();
let json_len = env.to_bytes().len();
println!("CBOR wire size: {wire_len} bytes");
println!("JSON size: {json_len} bytes");
println!("Ratio: {:.1}x smaller", json_len as f64 / wire_len as f64);
assert!(
json_len * 2 > wire_len * 3,
"CBOR ({wire_len}B) should be materially smaller than JSON ({json_len}B)"
);
}
#[test]
fn cbor_backward_compat() {
let id = test_identity();
let env = MeshEnvelope::new(&id, &[0xDD; 32], b"json compat".to_vec(), 60, 3);
// Serialize as JSON (old format).
let json_bytes = env.to_bytes();
// from_wire_or_json should fall back to JSON parsing.
let restored = MeshEnvelope::from_wire_or_json(&json_bytes)
.expect("from_wire_or_json should handle JSON");
assert_eq!(env.id, restored.id);
assert_eq!(env.payload, restored.payload);
assert!(restored.verify());
}
#[test]
fn cbor_from_wire_rejects_garbage() {
let garbage = [0xFF, 0xFE, 0x00, 0x42, 0x99, 0x01, 0x02, 0x03];
let result = MeshEnvelope::from_wire(&garbage);
assert!(result.is_err(), "garbage input must return Err, not panic");
}
}