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:
135
crates/quicprochat-p2p/src/address.rs
Normal file
135
crates/quicprochat-p2p/src/address.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
//! Truncated mesh addresses for bandwidth-efficient routing.
|
||||
//!
|
||||
//! A [`MeshAddress`] is derived from an Ed25519 public key by taking the first
|
||||
//! 16 bytes of its SHA-256 hash. This provides globally unique addressing
|
||||
//! (birthday collision at ~2^64) while saving 16 bytes per packet compared to
|
||||
//! full 32-byte public keys.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fmt;
|
||||
|
||||
/// 16-byte truncated mesh address.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct MeshAddress([u8; 16]);
|
||||
|
||||
impl MeshAddress {
|
||||
/// Derive from a 32-byte Ed25519 public key.
|
||||
pub fn from_public_key(key: &[u8; 32]) -> Self {
|
||||
let hash = Sha256::digest(key);
|
||||
let mut addr = [0u8; 16];
|
||||
addr.copy_from_slice(&hash[..16]);
|
||||
Self(addr)
|
||||
}
|
||||
|
||||
/// Create from raw 16-byte array.
|
||||
pub fn from_bytes(bytes: [u8; 16]) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
/// Get the raw 16-byte address.
|
||||
pub fn as_bytes(&self) -> &[u8; 16] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Check if a 32-byte public key matches this address.
|
||||
pub fn matches_key(&self, key: &[u8; 32]) -> bool {
|
||||
Self::from_public_key(key) == *self
|
||||
}
|
||||
|
||||
/// The broadcast address (all zeros).
|
||||
pub const BROADCAST: Self = Self([0u8; 16]);
|
||||
|
||||
/// Check if this is the broadcast address.
|
||||
pub fn is_broadcast(&self) -> bool {
|
||||
self.0 == [0u8; 16]
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for MeshAddress {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "MeshAddress({})", hex::encode(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for MeshAddress {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", hex::encode(&self.0[..8]))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; 16]> for MeshAddress {
|
||||
fn from(bytes: [u8; 16]) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8; 16]> for MeshAddress {
|
||||
fn as_ref(&self) -> &[u8; 16] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_key_deterministic() {
|
||||
let key = [42u8; 32];
|
||||
let addr1 = MeshAddress::from_public_key(&key);
|
||||
let addr2 = MeshAddress::from_public_key(&key);
|
||||
assert_eq!(addr1, addr2, "same key must produce same address");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_keys_different_addresses() {
|
||||
let key_a = [1u8; 32];
|
||||
let key_b = [2u8; 32];
|
||||
let addr_a = MeshAddress::from_public_key(&key_a);
|
||||
let addr_b = MeshAddress::from_public_key(&key_b);
|
||||
assert_ne!(addr_a, addr_b, "different keys must produce different addresses");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_key_works() {
|
||||
let key = [99u8; 32];
|
||||
let addr = MeshAddress::from_public_key(&key);
|
||||
assert!(addr.matches_key(&key), "correct key must match");
|
||||
|
||||
let wrong_key = [100u8; 32];
|
||||
assert!(!addr.matches_key(&wrong_key), "wrong key must not match");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn broadcast_address() {
|
||||
assert_eq!(*MeshAddress::BROADCAST.as_bytes(), [0u8; 16]);
|
||||
assert!(MeshAddress::BROADCAST.is_broadcast());
|
||||
|
||||
let non_broadcast = MeshAddress::from_bytes([1u8; 16]);
|
||||
assert!(!non_broadcast.is_broadcast());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_formatting() {
|
||||
let key = [0xAB; 32];
|
||||
let addr = MeshAddress::from_public_key(&key);
|
||||
let display = format!("{addr}");
|
||||
// Display shows first 8 bytes as hex = 16 hex chars.
|
||||
assert_eq!(display.len(), 16, "display should show 8 bytes = 16 hex chars");
|
||||
|
||||
let debug = format!("{addr:?}");
|
||||
// Debug shows all 16 bytes as hex = 32 hex chars, plus wrapper.
|
||||
assert!(debug.starts_with("MeshAddress("));
|
||||
assert!(debug.ends_with(')'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_roundtrip() {
|
||||
let key = [77u8; 32];
|
||||
let addr = MeshAddress::from_public_key(&key);
|
||||
let json = serde_json::to_string(&addr).expect("serialize");
|
||||
let restored: MeshAddress = serde_json::from_str(&json).expect("deserialize");
|
||||
assert_eq!(addr, restored);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user