//! Mesh announce protocol for self-organizing network discovery. //! //! Nodes periodically broadcast signed [`MeshAnnounce`] packets. These propagate //! through the mesh, building each node's [`RoutingTable`](crate::routing_table::RoutingTable). use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::time::{SystemTime, UNIX_EPOCH}; use crate::identity::MeshIdentity; /// Capability flag: node can relay messages for others. pub const CAP_RELAY: u16 = 0x0001; /// Capability flag: node has store-and-forward. pub const CAP_STORE: u16 = 0x0002; /// Capability flag: node is connected to Internet/server. pub const CAP_GATEWAY: u16 = 0x0004; /// Capability flag: node is on a low-bandwidth transport only. pub const CAP_CONSTRAINED: u16 = 0x0008; /// A signed mesh node announcement. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct MeshAnnounce { /// Ed25519 public key of the announcing node (32 bytes). pub identity_key: Vec, /// Truncated address: SHA-256(identity_key)[0..16] — used for routing. pub address: [u8; 16], /// Capability bitfield. pub capabilities: u16, /// Monotonically increasing sequence number (per node). pub sequence: u64, /// Unix timestamp of creation. pub timestamp: u64, /// Transports this node is reachable on: Vec<(transport_name, serialized_addr)>. pub reachable_via: Vec<(String, Vec)>, /// Current hop count (incremented on re-broadcast). pub hop_count: u8, /// Maximum propagation hops. pub max_hops: u8, /// Ed25519 signature over all fields except signature and hop_count. pub signature: Vec, } /// Compute the 16-byte mesh address from an Ed25519 public key. /// /// The address is the first 16 bytes of SHA-256(identity_key). pub fn compute_address(identity_key: &[u8]) -> [u8; 16] { let hash = Sha256::digest(identity_key); let mut addr = [0u8; 16]; addr.copy_from_slice(&hash[..16]); addr } impl MeshAnnounce { /// Create and sign a new mesh announcement. pub fn new( identity: &MeshIdentity, capabilities: u16, reachable_via: Vec<(String, Vec)>, max_hops: u8, ) -> Self { let identity_key = identity.public_key().to_vec(); let address = compute_address(&identity_key); let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs(); let mut announce = Self { identity_key, address, capabilities, sequence: 0, timestamp, reachable_via, hop_count: 0, max_hops, signature: Vec::new(), }; let signable = announce.signable_bytes(); announce.signature = identity.sign(&signable).to_vec(); announce } /// Create and sign with a specific sequence number. pub fn with_sequence( identity: &MeshIdentity, capabilities: u16, reachable_via: Vec<(String, Vec)>, max_hops: u8, sequence: u64, ) -> Self { let mut announce = Self::new(identity, capabilities, reachable_via, max_hops); announce.sequence = sequence; // Re-sign with the correct sequence number. let signable = announce.signable_bytes(); announce.signature = identity.sign(&signable).to_vec(); announce } /// Assemble the byte string that is signed / verified. /// /// `hop_count` and `signature` are excluded: forwarding nodes increment /// hop_count without re-signing (same design as [`MeshEnvelope`]). fn signable_bytes(&self) -> Vec { let mut buf = Vec::with_capacity( self.identity_key.len() + 16 + 2 + 8 + 8 + self.reachable_via.len() * 32 + 1, ); buf.extend_from_slice(&self.identity_key); buf.extend_from_slice(&self.address); buf.extend_from_slice(&self.capabilities.to_le_bytes()); buf.extend_from_slice(&self.sequence.to_le_bytes()); buf.extend_from_slice(&self.timestamp.to_le_bytes()); for (name, addr) in &self.reachable_via { buf.extend_from_slice(name.as_bytes()); buf.extend_from_slice(addr); } buf.push(self.max_hops); buf } /// Verify the Ed25519 signature on this announcement. pub fn verify(&self) -> bool { let identity_key: [u8; 32] = match self.identity_key.as_slice().try_into() { Ok(k) => k, Err(_) => return false, }; let sig: [u8; 64] = match self.signature.as_slice().try_into() { Ok(s) => s, Err(_) => return false, }; let signable = self.signable_bytes(); quicprochat_core::IdentityKeypair::verify_raw(&identity_key, &signable, &sig).is_ok() } /// Check whether this announce has expired relative to a maximum age. pub fn is_expired(&self, max_age_secs: u64) -> bool { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs(); now.saturating_sub(self.timestamp) > max_age_secs } /// Create a forwarded copy with `hop_count` incremented by one. /// /// The signature remains the original — forwarding nodes do not re-sign. pub fn forwarded(&self) -> Self { let mut copy = self.clone(); copy.hop_count = copy.hop_count.saturating_add(1); copy } /// Whether this announce can still propagate (under hop limit and not expired). /// /// Uses a generous default max age of 1800 seconds (30 minutes) for the /// expiry check. Callers that need a different max age should check /// [`is_expired`](Self::is_expired) separately. pub fn can_propagate(&self) -> bool { self.hop_count < self.max_hops && !self.is_expired(1800) } /// Serialize to compact CBOR binary format (for wire transmission). pub fn to_wire(&self) -> Vec { 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 { let announce: Self = ciborium::from_reader(bytes)?; Ok(announce) } } #[cfg(test)] mod tests { use super::*; fn test_identity() -> MeshIdentity { MeshIdentity::generate() } #[test] fn create_and_verify() { let id = test_identity(); let announce = MeshAnnounce::new( &id, CAP_RELAY | CAP_STORE, vec![("tcp".into(), b"127.0.0.1:9000".to_vec())], 8, ); assert!(announce.verify(), "freshly created announce must verify"); assert_eq!(announce.hop_count, 0); assert_eq!(announce.identity_key, id.public_key().to_vec()); assert_eq!(announce.capabilities, CAP_RELAY | CAP_STORE); assert_eq!(announce.max_hops, 8); } #[test] fn tampered_fails_verify() { let id = test_identity(); let mut announce = MeshAnnounce::new(&id, CAP_RELAY, vec![], 4); announce.capabilities = CAP_GATEWAY; // tamper assert!( !announce.verify(), "tampered announce must fail verification" ); } #[test] fn forwarded_still_verifies() { let id = test_identity(); let announce = MeshAnnounce::new(&id, CAP_RELAY, vec![], 8); assert!(announce.verify()); let fwd = announce.forwarded(); assert_eq!(fwd.hop_count, 1); assert!( fwd.verify(), "forwarded announce must still verify (hop_count excluded from signature)" ); let fwd2 = fwd.forwarded(); assert_eq!(fwd2.hop_count, 2); assert!(fwd2.verify(), "double-forwarded must still verify"); } #[test] fn expired_announce() { let id = test_identity(); let mut announce = MeshAnnounce::new(&id, 0, vec![], 4); // Set timestamp far in the past. announce.timestamp = 0; assert!(announce.is_expired(60), "announce from epoch should be expired with 60s max age"); } #[test] fn address_from_key_deterministic() { let key = [42u8; 32]; let addr1 = compute_address(&key); let addr2 = compute_address(&key); assert_eq!(addr1, addr2, "same key must produce same address"); // Different key produces different address. let other_key = [99u8; 32]; let other_addr = compute_address(&other_key); assert_ne!(addr1, other_addr); } #[test] fn cbor_roundtrip() { let id = test_identity(); let announce = MeshAnnounce::new( &id, CAP_RELAY | CAP_GATEWAY, vec![ ("tcp".into(), b"127.0.0.1:9000".to_vec()), ("lora".into(), vec![0x01, 0x02, 0x03, 0x04]), ], 6, ); let wire = announce.to_wire(); let restored = MeshAnnounce::from_wire(&wire).expect("CBOR deserialize"); assert_eq!(announce.identity_key, restored.identity_key); assert_eq!(announce.address, restored.address); assert_eq!(announce.capabilities, restored.capabilities); assert_eq!(announce.sequence, restored.sequence); assert_eq!(announce.timestamp, restored.timestamp); assert_eq!(announce.reachable_via, restored.reachable_via); assert_eq!(announce.hop_count, restored.hop_count); assert_eq!(announce.max_hops, restored.max_hops); assert_eq!(announce.signature, restored.signature); assert!(restored.verify()); } }