From a2400d823f2e8a45e197c760b03f79d12dd72a21 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Wed, 8 Jul 2026 19:00:25 +0000 Subject: [PATCH] proto/stp: extract ParentDeclaration and relocate the coordinate type to a shared proto/coord module Two behavior-neutral relocations, wire bytes unchanged: - Move the ParentDeclaration type and its impls out of state.rs into a dedicated declaration.rs, re-exported at the same path, and relocate the inline wire.rs test module into tests/wire.rs alongside the other stp unit tests (reusing the shared test helpers). wire.rs drops to non-test code only. - Move TreeCoordinate, CoordEntry, and the coordinate wire codec out of proto/stp into a shared proto/coord module (a peer of proto/link), re-exported from proto::stp so every existing import path keeps resolving unchanged. Coordinate is a shared addressing primitive with many non-stp consumers, so it no longer belongs under the spanning-tree subsystem. The codec carries a documented temporary dependency on stp::TreeError until a dedicated CoordError is introduced. --- src/proto/{stp/coordinate.rs => coord.rs} | 103 ++++- src/proto/mod.rs | 1 + src/proto/stp/declaration.rs | 144 +++++++ src/proto/stp/mod.rs | 16 +- src/proto/stp/state.rs | 141 +------ src/proto/stp/tests/mod.rs | 1 + src/proto/stp/tests/wire.rs | 336 +++++++++++++++++ src/proto/stp/wire.rs | 437 ---------------------- 8 files changed, 593 insertions(+), 586 deletions(-) rename src/proto/{stp/coordinate.rs => coord.rs} (65%) create mode 100644 src/proto/stp/declaration.rs create mode 100644 src/proto/stp/tests/wire.rs diff --git a/src/proto/stp/coordinate.rs b/src/proto/coord.rs similarity index 65% rename from src/proto/stp/coordinate.rs rename to src/proto/coord.rs index c293dd4..64e04cf 100644 --- a/src/proto/stp/coordinate.rs +++ b/src/proto/coord.rs @@ -1,9 +1,15 @@ -//! Tree coordinates and distance calculations. +//! Tree coordinate addressing primitive: the `TreeCoordinate` path type, its +//! `CoordEntry` elements, and their wire codec. A shared `proto` primitive +//! (peer of `link.rs`), re-exported from `proto::stp` for source continuity. use core::fmt; -use super::TreeError; use crate::NodeAddr; +use crate::proto::Error; +// TEMPORARY: coord depends upward on stp's TreeError here. This inversion is +// intentional and resolved in the next pass when a no_std-clean CoordError is +// created and TreeCoordinate is cut over to it. +use crate::proto::stp::TreeError; /// Metadata for a single node in a tree coordinate path. /// @@ -223,3 +229,96 @@ impl fmt::Debug for TreeCoordinate { write!(f, "])") } } + +// ============================================================================ +// Coordinate Wire Format Helpers +// ============================================================================ + +/// Wire size of a TreeCoordinate in address-only format: 2 + entries × 16. +pub(crate) fn coords_wire_size(coords: &TreeCoordinate) -> usize { + 2 + coords.entries().len() * 16 +} + +/// Encode a TreeCoordinate as address-only wire format: count(u16 LE) + addrs(16 × n). +/// +/// Session-layer messages serialize coordinates as NodeAddr arrays (16 bytes each), +/// without the sequence/timestamp metadata used by the tree gossip protocol. +pub(crate) fn encode_coords(coords: &TreeCoordinate, buf: &mut Vec) { + let addrs: Vec<&NodeAddr> = coords.node_addrs().collect(); + let count = addrs.len() as u16; + buf.extend_from_slice(&count.to_le_bytes()); + for addr in addrs { + buf.extend_from_slice(addr.as_bytes()); + } +} + +/// Decode a TreeCoordinate from address-only wire format. +/// +/// Returns the decoded coordinate and the number of bytes consumed. +pub(crate) fn decode_coords(data: &[u8]) -> Result<(TreeCoordinate, usize), Error> { + if data.len() < 2 { + return Err(Error::MessageTooShort { + expected: 2, + got: data.len(), + }); + } + let count = u16::from_le_bytes([data[0], data[1]]) as usize; + let needed = 2 + count * 16; + if data.len() < needed { + return Err(Error::MessageTooShort { + expected: needed, + got: data.len(), + }); + } + if count == 0 { + return Err(Error::Malformed("coordinate with zero entries".into())); + } + let mut addrs = Vec::with_capacity(count); + for i in 0..count { + let offset = 2 + i * 16; + let mut bytes = [0u8; 16]; + bytes.copy_from_slice(&data[offset..offset + 16]); + addrs.push(NodeAddr::from_bytes(bytes)); + } + let coord = TreeCoordinate::from_addrs(addrs).map_err(|e| Error::Malformed(e.to_string()))?; + Ok((coord, needed)) +} + +/// Decode an optional coordinate field (count may be 0). +/// +/// Returns None if count is 0, Some(coord) otherwise, plus bytes consumed. +pub(crate) fn decode_optional_coords( + data: &[u8], +) -> Result<(Option, usize), Error> { + if data.len() < 2 { + return Err(Error::MessageTooShort { + expected: 2, + got: data.len(), + }); + } + let count = u16::from_le_bytes([data[0], data[1]]) as usize; + let needed = 2 + count * 16; + if data.len() < needed { + return Err(Error::MessageTooShort { + expected: needed, + got: data.len(), + }); + } + if count == 0 { + return Ok((None, 2)); + } + let mut addrs = Vec::with_capacity(count); + for i in 0..count { + let offset = 2 + i * 16; + let mut bytes = [0u8; 16]; + bytes.copy_from_slice(&data[offset..offset + 16]); + addrs.push(NodeAddr::from_bytes(bytes)); + } + let coord = TreeCoordinate::from_addrs(addrs).map_err(|e| Error::Malformed(e.to_string()))?; + Ok((Some(coord), needed)) +} + +/// Encode a count of zero (for empty/absent coordinate fields). +pub(crate) fn encode_empty_coords(buf: &mut Vec) { + buf.extend_from_slice(&0u16.to_le_bytes()); +} diff --git a/src/proto/mod.rs b/src/proto/mod.rs index 7b0953b..6cb6281 100644 --- a/src/proto/mod.rs +++ b/src/proto/mod.rs @@ -7,6 +7,7 @@ mod error; pub use error::Error; pub(crate) mod bloom; +pub(crate) mod coord; pub(crate) mod discovery; pub(crate) mod fmp; pub(crate) mod fsp; diff --git a/src/proto/stp/declaration.rs b/src/proto/stp/declaration.rs new file mode 100644 index 0000000..9cb49d9 --- /dev/null +++ b/src/proto/stp/declaration.rs @@ -0,0 +1,144 @@ +//! Parent declaration: a node's signed announcement of its spanning-tree parent. + +use core::fmt; + +use crate::NodeAddr; + +/// A node's declaration of its parent in the spanning tree. +/// +/// Each node periodically announces its parent selection. The declaration +/// includes a monotonic sequence number for freshness and a signature +/// for authenticity. When `parent_id == node_addr`, the node declares itself +/// as a root candidate. +#[derive(Clone)] +pub struct ParentDeclaration { + /// The node making this declaration. + node_addr: NodeAddr, + /// The selected parent (equals node_addr if self-declaring as root). + parent_id: NodeAddr, + /// Monotonically increasing sequence number. + sequence: u64, + /// Timestamp when this declaration was created (Unix seconds). + timestamp: u64, + /// Raw 64-byte Schnorr signature over the declaration fields. Stored as + /// opaque bytes so the in-core type carries no signature-crypto dependency; + /// the shell computes/verifies it over `signing_bytes()` (§6). + signature: Option<[u8; 64]>, +} + +impl ParentDeclaration { + /// Create a new unsigned parent declaration. + /// + /// The declaration must be signed before transmission using `set_signature()`. + pub fn new(node_addr: NodeAddr, parent_id: NodeAddr, sequence: u64, timestamp: u64) -> Self { + Self { + node_addr, + parent_id, + sequence, + timestamp, + signature: None, + } + } + + /// Create a self-declaration (node is root candidate). + pub fn self_root(node_addr: NodeAddr, sequence: u64, timestamp: u64) -> Self { + Self::new(node_addr, node_addr, sequence, timestamp) + } + + /// Create a declaration with a pre-computed signature. + pub fn with_signature( + node_addr: NodeAddr, + parent_id: NodeAddr, + sequence: u64, + timestamp: u64, + signature: [u8; 64], + ) -> Self { + Self { + node_addr, + parent_id, + sequence, + timestamp, + signature: Some(signature), + } + } + + /// Get the declaring node's ID. + pub fn node_addr(&self) -> &NodeAddr { + &self.node_addr + } + + /// Get the parent node's ID. + pub fn parent_id(&self) -> &NodeAddr { + &self.parent_id + } + + /// Get the sequence number. + pub fn sequence(&self) -> u64 { + self.sequence + } + + /// Get the timestamp. + pub fn timestamp(&self) -> u64 { + self.timestamp + } + + /// Get the raw 64-byte signature, if set. + pub fn signature(&self) -> Option<&[u8; 64]> { + self.signature.as_ref() + } + + /// Set the raw 64-byte signature after signing. + pub fn set_signature(&mut self, signature: [u8; 64]) { + self.signature = Some(signature); + } + + /// Check if this is a root declaration (parent == self). + pub fn is_root(&self) -> bool { + self.node_addr == self.parent_id + } + + /// Check if this declaration is signed. + pub fn is_signed(&self) -> bool { + self.signature.is_some() + } + + /// Get the bytes that should be signed. + /// + /// Format: node_addr (16) || parent_id (16) || sequence (8) || timestamp (8) + pub fn signing_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(48); + bytes.extend_from_slice(self.node_addr.as_bytes()); + bytes.extend_from_slice(self.parent_id.as_bytes()); + bytes.extend_from_slice(&self.sequence.to_le_bytes()); + bytes.extend_from_slice(&self.timestamp.to_le_bytes()); + bytes + } + + /// Check if this declaration is fresher than another. + pub fn is_fresher_than(&self, other: &ParentDeclaration) -> bool { + self.sequence > other.sequence + } +} + +impl fmt::Debug for ParentDeclaration { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ParentDeclaration") + .field("node_addr", &self.node_addr) + .field("parent_id", &self.parent_id) + .field("sequence", &self.sequence) + .field("is_root", &self.is_root()) + .field("signed", &self.is_signed()) + .finish() + } +} + +impl PartialEq for ParentDeclaration { + fn eq(&self, other: &Self) -> bool { + self.node_addr == other.node_addr + && self.parent_id == other.parent_id + && self.sequence == other.sequence + && self.timestamp == other.timestamp + } +} + +impl Eq for ParentDeclaration {} diff --git a/src/proto/stp/mod.rs b/src/proto/stp/mod.rs index d0fb51d..70d4af5 100644 --- a/src/proto/stp/mod.rs +++ b/src/proto/stp/mod.rs @@ -13,13 +13,14 @@ //! - `state.rs` — `TreeState` + `ParentDeclaration` data + the `&self` //! ranking/election methods (`evaluate_parent`, `should_be_root`, //! `find_next_hop`). -//! - `coordinate.rs` — `TreeCoordinate` / `CoordEntry`. +//! - `TreeCoordinate` / `CoordEntry` now live in the shared +//! [`crate::proto::coord`] primitive and are re-exported here for continuity. //! - `limits.rs` — the flap-dampening / hold-down state machine. //! - `wire.rs` — `TreeAnnounce` + `validate_semantics` (the one std-tethered //! file). -mod coordinate; mod core; +mod declaration; mod limits; mod state; mod wire; @@ -31,13 +32,14 @@ use thiserror::Error; use crate::{IdentityError, NodeAddr}; -pub use coordinate::{CoordEntry, TreeCoordinate}; -pub(crate) use core::{ParentEval, Stp, TreeDecision}; -pub use state::{ParentDeclaration, TreeState}; -pub use wire::TreeAnnounce; -pub(crate) use wire::{ +pub use crate::proto::coord::{CoordEntry, TreeCoordinate}; +pub(crate) use crate::proto::coord::{ coords_wire_size, decode_coords, decode_optional_coords, encode_coords, encode_empty_coords, }; +pub(crate) use core::{ParentEval, Stp, TreeDecision}; +pub use declaration::ParentDeclaration; +pub use state::TreeState; +pub use wire::TreeAnnounce; /// Errors related to spanning tree operations. #[derive(Debug, Error)] diff --git a/src/proto/stp/state.rs b/src/proto/stp/state.rs index 9d6aeba..fad3481 100644 --- a/src/proto/stp/state.rs +++ b/src/proto/stp/state.rs @@ -5,7 +5,7 @@ use core::fmt; use super::core::ParentEval; use super::limits::FlapDampener; -use super::{CoordEntry, TreeCoordinate}; +use super::{CoordEntry, ParentDeclaration, TreeCoordinate}; use crate::NodeAddr; /// Local spanning tree state for a node. @@ -547,142 +547,3 @@ impl fmt::Debug for TreeState { .finish() } } - -/// A node's declaration of its parent in the spanning tree. -/// -/// Each node periodically announces its parent selection. The declaration -/// includes a monotonic sequence number for freshness and a signature -/// for authenticity. When `parent_id == node_addr`, the node declares itself -/// as a root candidate. -#[derive(Clone)] -pub struct ParentDeclaration { - /// The node making this declaration. - node_addr: NodeAddr, - /// The selected parent (equals node_addr if self-declaring as root). - parent_id: NodeAddr, - /// Monotonically increasing sequence number. - sequence: u64, - /// Timestamp when this declaration was created (Unix seconds). - timestamp: u64, - /// Raw 64-byte Schnorr signature over the declaration fields. Stored as - /// opaque bytes so the in-core type carries no signature-crypto dependency; - /// the shell computes/verifies it over `signing_bytes()` (§6). - signature: Option<[u8; 64]>, -} - -impl ParentDeclaration { - /// Create a new unsigned parent declaration. - /// - /// The declaration must be signed before transmission using `set_signature()`. - pub fn new(node_addr: NodeAddr, parent_id: NodeAddr, sequence: u64, timestamp: u64) -> Self { - Self { - node_addr, - parent_id, - sequence, - timestamp, - signature: None, - } - } - - /// Create a self-declaration (node is root candidate). - pub fn self_root(node_addr: NodeAddr, sequence: u64, timestamp: u64) -> Self { - Self::new(node_addr, node_addr, sequence, timestamp) - } - - /// Create a declaration with a pre-computed signature. - pub fn with_signature( - node_addr: NodeAddr, - parent_id: NodeAddr, - sequence: u64, - timestamp: u64, - signature: [u8; 64], - ) -> Self { - Self { - node_addr, - parent_id, - sequence, - timestamp, - signature: Some(signature), - } - } - - /// Get the declaring node's ID. - pub fn node_addr(&self) -> &NodeAddr { - &self.node_addr - } - - /// Get the parent node's ID. - pub fn parent_id(&self) -> &NodeAddr { - &self.parent_id - } - - /// Get the sequence number. - pub fn sequence(&self) -> u64 { - self.sequence - } - - /// Get the timestamp. - pub fn timestamp(&self) -> u64 { - self.timestamp - } - - /// Get the raw 64-byte signature, if set. - pub fn signature(&self) -> Option<&[u8; 64]> { - self.signature.as_ref() - } - - /// Set the raw 64-byte signature after signing. - pub fn set_signature(&mut self, signature: [u8; 64]) { - self.signature = Some(signature); - } - - /// Check if this is a root declaration (parent == self). - pub fn is_root(&self) -> bool { - self.node_addr == self.parent_id - } - - /// Check if this declaration is signed. - pub fn is_signed(&self) -> bool { - self.signature.is_some() - } - - /// Get the bytes that should be signed. - /// - /// Format: node_addr (16) || parent_id (16) || sequence (8) || timestamp (8) - pub fn signing_bytes(&self) -> Vec { - let mut bytes = Vec::with_capacity(48); - bytes.extend_from_slice(self.node_addr.as_bytes()); - bytes.extend_from_slice(self.parent_id.as_bytes()); - bytes.extend_from_slice(&self.sequence.to_le_bytes()); - bytes.extend_from_slice(&self.timestamp.to_le_bytes()); - bytes - } - - /// Check if this declaration is fresher than another. - pub fn is_fresher_than(&self, other: &ParentDeclaration) -> bool { - self.sequence > other.sequence - } -} - -impl fmt::Debug for ParentDeclaration { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ParentDeclaration") - .field("node_addr", &self.node_addr) - .field("parent_id", &self.parent_id) - .field("sequence", &self.sequence) - .field("is_root", &self.is_root()) - .field("signed", &self.is_signed()) - .finish() - } -} - -impl PartialEq for ParentDeclaration { - fn eq(&self, other: &Self) -> bool { - self.node_addr == other.node_addr - && self.parent_id == other.parent_id - && self.sequence == other.sequence - && self.timestamp == other.timestamp - } -} - -impl Eq for ParentDeclaration {} diff --git a/src/proto/stp/tests/mod.rs b/src/proto/stp/tests/mod.rs index 15de4ca..7119999 100644 --- a/src/proto/stp/tests/mod.rs +++ b/src/proto/stp/tests/mod.rs @@ -4,3 +4,4 @@ mod coordinate; mod limits; mod state; mod util; +mod wire; diff --git a/src/proto/stp/tests/wire.rs b/src/proto/stp/tests/wire.rs new file mode 100644 index 0000000..559e9f5 --- /dev/null +++ b/src/proto/stp/tests/wire.rs @@ -0,0 +1,336 @@ +//! TreeAnnounce wire codec unit tests. + +use super::util::{make_coords, make_node_addr}; +use crate::identity::Identity; +use crate::proto::Error; +use crate::proto::stp::wire::TreeAnnounce; +use crate::proto::stp::{CoordEntry, ParentDeclaration, TreeCoordinate, TreeError}; + +/// Sign a declaration in place. In production the shell owns the key-crypto +/// (§6); this test-local helper keeps the sign/verify boundary out of the +/// in-core `state.rs` while letting the codec tests build signed messages. +fn sign_decl(decl: &mut ParentDeclaration, identity: &Identity) { + let sig = identity.sign(&decl.signing_bytes()); + decl.set_signature(sig.to_byte_array()); +} + +#[test] +fn test_tree_announce() { + let node = make_node_addr(1); + let parent = make_node_addr(2); + let decl = ParentDeclaration::new(node, parent, 1, 1000); + let ancestry = make_coords(&[1, 2, 0]); + + let announce = TreeAnnounce::new(decl, ancestry); + + assert_eq!(announce.declaration.node_addr(), &node); + assert_eq!(announce.ancestry.depth(), 2); +} + +#[test] +fn test_tree_announce_encode_decode_root() { + use crate::identity::Identity; + + let identity = Identity::generate(); + let node_addr = *identity.node_addr(); + + // Root declaration: parent == self + let mut decl = ParentDeclaration::new(node_addr, node_addr, 1, 5000); + sign_decl(&mut decl, &identity); + + // Root ancestry: just the root itself + let ancestry = TreeCoordinate::new(vec![CoordEntry::new(node_addr, 1, 5000)]).unwrap(); + + let announce = TreeAnnounce::new(decl, ancestry); + let encoded = announce.encode().unwrap(); + + // msg_type (1) + version (1) + seq (8) + ts (8) + parent (16) + count (2) + 1 entry (32) + sig (64) = 132 + assert_eq!(encoded.len(), 132); + assert_eq!(encoded[0], 0x10); // LinkMessageType::TreeAnnounce + + // Decode strips msg_type byte (as dispatcher does) + let decoded = TreeAnnounce::decode(&encoded[1..]).unwrap(); + + assert_eq!(decoded.declaration.node_addr(), &node_addr); + assert_eq!(decoded.declaration.parent_id(), &node_addr); + assert_eq!(decoded.declaration.sequence(), 1); + assert_eq!(decoded.declaration.timestamp(), 5000); + assert!(decoded.declaration.is_root()); + assert!(decoded.declaration.is_signed()); + assert_eq!(decoded.ancestry.depth(), 0); // root has depth 0 + assert_eq!(decoded.ancestry.entries().len(), 1); + assert_eq!(decoded.ancestry.entries()[0].node_addr, node_addr); + assert_eq!(decoded.ancestry.entries()[0].sequence, 1); + assert_eq!(decoded.ancestry.entries()[0].timestamp, 5000); +} + +#[test] +fn test_tree_announce_encode_decode_depth3() { + use crate::identity::Identity; + + let identity = Identity::generate(); + let node_addr = *identity.node_addr(); + let parent = make_node_addr(2); + let grandparent = make_node_addr(3); + let root = make_node_addr(4); + + let mut decl = ParentDeclaration::new(node_addr, parent, 5, 10000); + sign_decl(&mut decl, &identity); + + let ancestry = TreeCoordinate::new(vec![ + CoordEntry::new(node_addr, 5, 10000), + CoordEntry::new(parent, 4, 9000), + CoordEntry::new(grandparent, 3, 8000), + CoordEntry::new(root, 2, 7000), + ]) + .unwrap(); + + let announce = TreeAnnounce::new(decl, ancestry); + let encoded = announce.encode().unwrap(); + + // 1 + 99 + 4*32 = 228 + assert_eq!(encoded.len(), 228); + + let decoded = TreeAnnounce::decode(&encoded[1..]).unwrap(); + + assert_eq!(decoded.declaration.node_addr(), &node_addr); + assert_eq!(decoded.declaration.parent_id(), &parent); + assert_eq!(decoded.declaration.sequence(), 5); + assert_eq!(decoded.declaration.timestamp(), 10000); + assert!(!decoded.declaration.is_root()); + assert_eq!(decoded.ancestry.depth(), 3); + assert_eq!(decoded.ancestry.entries().len(), 4); + + // Verify all entries preserved + let entries = decoded.ancestry.entries(); + assert_eq!(entries[0].node_addr, node_addr); + assert_eq!(entries[0].sequence, 5); + assert_eq!(entries[1].node_addr, parent); + assert_eq!(entries[1].sequence, 4); + assert_eq!(entries[2].node_addr, grandparent); + assert_eq!(entries[2].timestamp, 8000); + assert_eq!(entries[3].node_addr, root); + assert_eq!(entries[3].timestamp, 7000); + + // Root ID is last entry + assert_eq!(decoded.ancestry.root_id(), &root); +} + +#[test] +fn test_tree_announce_decode_unsupported_version() { + use crate::identity::Identity; + + let identity = Identity::generate(); + let node_addr = *identity.node_addr(); + + let mut decl = ParentDeclaration::new(node_addr, node_addr, 1, 1000); + sign_decl(&mut decl, &identity); + + let ancestry = TreeCoordinate::new(vec![CoordEntry::new(node_addr, 1, 1000)]).unwrap(); + let announce = TreeAnnounce::new(decl, ancestry); + let mut encoded = announce.encode().unwrap(); + + // Corrupt version byte (byte index 1, after msg_type) + encoded[1] = 0xFF; + + let result = TreeAnnounce::decode(&encoded[1..]); + assert!(matches!(result, Err(Error::UnsupportedVersion(0xFF)))); +} + +#[test] +fn test_tree_announce_decode_truncated() { + // Way too short + let result = TreeAnnounce::decode(&[0x01]); + assert!(matches!( + result, + Err(Error::MessageTooShort { expected: 99, .. }) + )); + + // Just under minimum (98 bytes) + let short = vec![0u8; 98]; + let result = TreeAnnounce::decode(&short); + assert!(matches!( + result, + Err(Error::MessageTooShort { expected: 99, .. }) + )); +} + +#[test] +fn test_tree_announce_decode_ancestry_count_mismatch() { + use crate::identity::Identity; + + let identity = Identity::generate(); + let node_addr = *identity.node_addr(); + + let mut decl = ParentDeclaration::new(node_addr, node_addr, 1, 1000); + sign_decl(&mut decl, &identity); + + let ancestry = TreeCoordinate::new(vec![CoordEntry::new(node_addr, 1, 1000)]).unwrap(); + let announce = TreeAnnounce::new(decl, ancestry); + let mut encoded = announce.encode().unwrap(); + + // The ancestry_count is at offset: 1 (msg_type) + 1 (version) + 8 (seq) + 8 (ts) + 16 (parent) = 34 + // Set ancestry_count to 5 but we only have 1 entry's worth of data + encoded[34] = 5; + encoded[35] = 0; + + let result = TreeAnnounce::decode(&encoded[1..]); + assert!(matches!(result, Err(Error::MessageTooShort { .. }))); +} + +#[test] +fn test_tree_announce_encode_unsigned_fails() { + let node = make_node_addr(1); + let decl = ParentDeclaration::new(node, node, 1, 1000); + let ancestry = make_coords(&[1, 0]); + + let announce = TreeAnnounce::new(decl, ancestry); + let result = announce.encode(); + assert!(matches!(result, Err(Error::InvalidSignature))); +} + +/// Tests that a well-formed non-root ancestry is accepted. +#[test] +fn test_tree_announce_validate_semantics_accepts_valid_non_root() { + use crate::identity::Identity; + + // Regenerate until the random identity's node_addr is numerically + // larger than both fixed parent (02:..) and root (01:..), so the + // root-minimum invariant holds deterministically. + let identity = loop { + let id = Identity::generate(); + if id.node_addr().as_bytes()[0] > 1 { + break id; + } + }; + let node_addr = *identity.node_addr(); + let parent = make_node_addr(2); + let root = make_node_addr(1); + + let mut decl = ParentDeclaration::new(node_addr, parent, 5, 1000); + sign_decl(&mut decl, &identity); + + let ancestry = TreeCoordinate::new(vec![ + CoordEntry::new(node_addr, 5, 1000), + CoordEntry::new(parent, 4, 900), + CoordEntry::new(root, 3, 800), + ]) + .unwrap(); + + let announce = TreeAnnounce::new(decl, ancestry); + assert!(announce.validate_semantics().is_ok()); +} + +/// Tests that an ancestry is rejected if the final node_addr is not the smallest entry in the path. +#[test] +fn test_tree_announce_validate_semantics_rejects_non_minimal_root() { + use crate::identity::Identity; + + let identity = Identity::generate(); + let node_addr = *identity.node_addr(); + let smaller = make_node_addr(0); + let advertised_root = make_node_addr(1); + + let mut decl = ParentDeclaration::new(node_addr, smaller, 5, 1000); + sign_decl(&mut decl, &identity); + + let ancestry = TreeCoordinate::new(vec![ + CoordEntry::new(node_addr, 5, 1000), + CoordEntry::new(smaller, 4, 900), + CoordEntry::new(advertised_root, 3, 800), + ]) + .unwrap(); + + let announce = TreeAnnounce::new(decl, ancestry); + assert!(matches!( + announce.validate_semantics(), + Err(TreeError::AncestryRootNotMinimum { + advertised, + minimum, + }) if advertised == advertised_root && minimum == smaller + )); +} + +/// Tests that an ancestry is rejected if the first ancestry hop does not match the signed parent_id. +#[test] +fn test_tree_announce_validate_semantics_rejects_parent_mismatch() { + use crate::identity::Identity; + + let identity = Identity::generate(); + let node_addr = *identity.node_addr(); + let declared_parent = make_node_addr(2); + let ancestry_parent = make_node_addr(3); + + let mut decl = ParentDeclaration::new(node_addr, declared_parent, 5, 1000); + sign_decl(&mut decl, &identity); + + let ancestry = TreeCoordinate::new(vec![ + CoordEntry::new(node_addr, 5, 1000), + CoordEntry::new(ancestry_parent, 4, 900), + CoordEntry::new(make_node_addr(1), 3, 800), + ]) + .unwrap(); + + let announce = TreeAnnounce::new(decl, ancestry); + assert!(matches!( + announce.validate_semantics(), + Err(TreeError::AncestryParentMismatch { + declared, + ancestry, + }) if declared == declared_parent && ancestry == ancestry_parent + )); +} + +/// Tests that an ancestry is rejected if the first path entry does not match the signed sender node_addr. +#[test] +fn test_tree_announce_validate_semantics_rejects_sender_mismatch() { + use crate::identity::Identity; + + let identity = Identity::generate(); + let node_addr = *identity.node_addr(); + let ancestry_sender = make_node_addr(9); + let parent = make_node_addr(2); + + let mut decl = ParentDeclaration::new(node_addr, parent, 5, 1000); + sign_decl(&mut decl, &identity); + + let ancestry = TreeCoordinate::new(vec![ + CoordEntry::new(ancestry_sender, 5, 1000), + CoordEntry::new(parent, 4, 900), + CoordEntry::new(make_node_addr(1), 3, 800), + ]) + .unwrap(); + + let announce = TreeAnnounce::new(decl, ancestry); + assert!(matches!( + announce.validate_semantics(), + Err(TreeError::AncestryNodeMismatch { + declared, + ancestry, + }) if declared == node_addr && ancestry == ancestry_sender + )); +} + +/// Tests that a self-root declaration is rejected if its ancestry contains extra ancestors. +#[test] +fn test_tree_announce_validate_semantics_rejects_root_with_ancestors() { + use crate::identity::Identity; + + let identity = Identity::generate(); + let node_addr = *identity.node_addr(); + + let mut decl = ParentDeclaration::self_root(node_addr, 5, 1000); + sign_decl(&mut decl, &identity); + + let ancestry = TreeCoordinate::new(vec![ + CoordEntry::new(node_addr, 5, 1000), + CoordEntry::new(make_node_addr(0), 4, 900), + ]) + .unwrap(); + + let announce = TreeAnnounce::new(decl, ancestry); + assert!(matches!( + announce.validate_semantics(), + Err(TreeError::RootDeclarationMismatch) + )); +} diff --git a/src/proto/stp/wire.rs b/src/proto/stp/wire.rs index 984b7df..fce4797 100644 --- a/src/proto/stp/wire.rs +++ b/src/proto/stp/wire.rs @@ -244,440 +244,3 @@ impl TreeAnnounce { }) } } -// ============================================================================ -// Coordinate Wire Format Helpers -// ============================================================================ - -/// Wire size of a TreeCoordinate in address-only format: 2 + entries × 16. -pub(crate) fn coords_wire_size(coords: &TreeCoordinate) -> usize { - 2 + coords.entries().len() * 16 -} - -/// Encode a TreeCoordinate as address-only wire format: count(u16 LE) + addrs(16 × n). -/// -/// Session-layer messages serialize coordinates as NodeAddr arrays (16 bytes each), -/// without the sequence/timestamp metadata used by the tree gossip protocol. -pub(crate) fn encode_coords(coords: &TreeCoordinate, buf: &mut Vec) { - let addrs: Vec<&NodeAddr> = coords.node_addrs().collect(); - let count = addrs.len() as u16; - buf.extend_from_slice(&count.to_le_bytes()); - for addr in addrs { - buf.extend_from_slice(addr.as_bytes()); - } -} - -/// Decode a TreeCoordinate from address-only wire format. -/// -/// Returns the decoded coordinate and the number of bytes consumed. -pub(crate) fn decode_coords(data: &[u8]) -> Result<(TreeCoordinate, usize), Error> { - if data.len() < 2 { - return Err(Error::MessageTooShort { - expected: 2, - got: data.len(), - }); - } - let count = u16::from_le_bytes([data[0], data[1]]) as usize; - let needed = 2 + count * 16; - if data.len() < needed { - return Err(Error::MessageTooShort { - expected: needed, - got: data.len(), - }); - } - if count == 0 { - return Err(Error::Malformed("coordinate with zero entries".into())); - } - let mut addrs = Vec::with_capacity(count); - for i in 0..count { - let offset = 2 + i * 16; - let mut bytes = [0u8; 16]; - bytes.copy_from_slice(&data[offset..offset + 16]); - addrs.push(NodeAddr::from_bytes(bytes)); - } - let coord = TreeCoordinate::from_addrs(addrs).map_err(|e| Error::Malformed(e.to_string()))?; - Ok((coord, needed)) -} - -/// Decode an optional coordinate field (count may be 0). -/// -/// Returns None if count is 0, Some(coord) otherwise, plus bytes consumed. -pub(crate) fn decode_optional_coords( - data: &[u8], -) -> Result<(Option, usize), Error> { - if data.len() < 2 { - return Err(Error::MessageTooShort { - expected: 2, - got: data.len(), - }); - } - let count = u16::from_le_bytes([data[0], data[1]]) as usize; - let needed = 2 + count * 16; - if data.len() < needed { - return Err(Error::MessageTooShort { - expected: needed, - got: data.len(), - }); - } - if count == 0 { - return Ok((None, 2)); - } - let mut addrs = Vec::with_capacity(count); - for i in 0..count { - let offset = 2 + i * 16; - let mut bytes = [0u8; 16]; - bytes.copy_from_slice(&data[offset..offset + 16]); - addrs.push(NodeAddr::from_bytes(bytes)); - } - let coord = TreeCoordinate::from_addrs(addrs).map_err(|e| Error::Malformed(e.to_string()))?; - Ok((Some(coord), needed)) -} - -/// Encode a count of zero (for empty/absent coordinate fields). -pub(crate) fn encode_empty_coords(buf: &mut Vec) { - buf.extend_from_slice(&0u16.to_le_bytes()); -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::identity::Identity; - - fn make_node_addr(val: u8) -> NodeAddr { - let mut bytes = [0u8; 16]; - bytes[0] = val; - NodeAddr::from_bytes(bytes) - } - - /// Sign a declaration in place. In production the shell owns the key-crypto - /// (§6); this test-local helper keeps the sign/verify boundary out of the - /// in-core `state.rs` while letting the codec tests build signed messages. - fn sign_decl(decl: &mut ParentDeclaration, identity: &Identity) { - let sig = identity.sign(&decl.signing_bytes()); - decl.set_signature(sig.to_byte_array()); - } - - fn make_coords(ids: &[u8]) -> TreeCoordinate { - TreeCoordinate::from_addrs(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap() - } - - #[test] - fn test_tree_announce() { - let node = make_node_addr(1); - let parent = make_node_addr(2); - let decl = ParentDeclaration::new(node, parent, 1, 1000); - let ancestry = make_coords(&[1, 2, 0]); - - let announce = TreeAnnounce::new(decl, ancestry); - - assert_eq!(announce.declaration.node_addr(), &node); - assert_eq!(announce.ancestry.depth(), 2); - } - - #[test] - fn test_tree_announce_encode_decode_root() { - use crate::identity::Identity; - - let identity = Identity::generate(); - let node_addr = *identity.node_addr(); - - // Root declaration: parent == self - let mut decl = ParentDeclaration::new(node_addr, node_addr, 1, 5000); - sign_decl(&mut decl, &identity); - - // Root ancestry: just the root itself - let ancestry = TreeCoordinate::new(vec![CoordEntry::new(node_addr, 1, 5000)]).unwrap(); - - let announce = TreeAnnounce::new(decl, ancestry); - let encoded = announce.encode().unwrap(); - - // msg_type (1) + version (1) + seq (8) + ts (8) + parent (16) + count (2) + 1 entry (32) + sig (64) = 132 - assert_eq!(encoded.len(), 132); - assert_eq!(encoded[0], 0x10); // LinkMessageType::TreeAnnounce - - // Decode strips msg_type byte (as dispatcher does) - let decoded = TreeAnnounce::decode(&encoded[1..]).unwrap(); - - assert_eq!(decoded.declaration.node_addr(), &node_addr); - assert_eq!(decoded.declaration.parent_id(), &node_addr); - assert_eq!(decoded.declaration.sequence(), 1); - assert_eq!(decoded.declaration.timestamp(), 5000); - assert!(decoded.declaration.is_root()); - assert!(decoded.declaration.is_signed()); - assert_eq!(decoded.ancestry.depth(), 0); // root has depth 0 - assert_eq!(decoded.ancestry.entries().len(), 1); - assert_eq!(decoded.ancestry.entries()[0].node_addr, node_addr); - assert_eq!(decoded.ancestry.entries()[0].sequence, 1); - assert_eq!(decoded.ancestry.entries()[0].timestamp, 5000); - } - - #[test] - fn test_tree_announce_encode_decode_depth3() { - use crate::identity::Identity; - - let identity = Identity::generate(); - let node_addr = *identity.node_addr(); - let parent = make_node_addr(2); - let grandparent = make_node_addr(3); - let root = make_node_addr(4); - - let mut decl = ParentDeclaration::new(node_addr, parent, 5, 10000); - sign_decl(&mut decl, &identity); - - let ancestry = TreeCoordinate::new(vec![ - CoordEntry::new(node_addr, 5, 10000), - CoordEntry::new(parent, 4, 9000), - CoordEntry::new(grandparent, 3, 8000), - CoordEntry::new(root, 2, 7000), - ]) - .unwrap(); - - let announce = TreeAnnounce::new(decl, ancestry); - let encoded = announce.encode().unwrap(); - - // 1 + 99 + 4*32 = 228 - assert_eq!(encoded.len(), 228); - - let decoded = TreeAnnounce::decode(&encoded[1..]).unwrap(); - - assert_eq!(decoded.declaration.node_addr(), &node_addr); - assert_eq!(decoded.declaration.parent_id(), &parent); - assert_eq!(decoded.declaration.sequence(), 5); - assert_eq!(decoded.declaration.timestamp(), 10000); - assert!(!decoded.declaration.is_root()); - assert_eq!(decoded.ancestry.depth(), 3); - assert_eq!(decoded.ancestry.entries().len(), 4); - - // Verify all entries preserved - let entries = decoded.ancestry.entries(); - assert_eq!(entries[0].node_addr, node_addr); - assert_eq!(entries[0].sequence, 5); - assert_eq!(entries[1].node_addr, parent); - assert_eq!(entries[1].sequence, 4); - assert_eq!(entries[2].node_addr, grandparent); - assert_eq!(entries[2].timestamp, 8000); - assert_eq!(entries[3].node_addr, root); - assert_eq!(entries[3].timestamp, 7000); - - // Root ID is last entry - assert_eq!(decoded.ancestry.root_id(), &root); - } - - #[test] - fn test_tree_announce_decode_unsupported_version() { - use crate::identity::Identity; - - let identity = Identity::generate(); - let node_addr = *identity.node_addr(); - - let mut decl = ParentDeclaration::new(node_addr, node_addr, 1, 1000); - sign_decl(&mut decl, &identity); - - let ancestry = TreeCoordinate::new(vec![CoordEntry::new(node_addr, 1, 1000)]).unwrap(); - let announce = TreeAnnounce::new(decl, ancestry); - let mut encoded = announce.encode().unwrap(); - - // Corrupt version byte (byte index 1, after msg_type) - encoded[1] = 0xFF; - - let result = TreeAnnounce::decode(&encoded[1..]); - assert!(matches!(result, Err(Error::UnsupportedVersion(0xFF)))); - } - - #[test] - fn test_tree_announce_decode_truncated() { - // Way too short - let result = TreeAnnounce::decode(&[0x01]); - assert!(matches!( - result, - Err(Error::MessageTooShort { expected: 99, .. }) - )); - - // Just under minimum (98 bytes) - let short = vec![0u8; 98]; - let result = TreeAnnounce::decode(&short); - assert!(matches!( - result, - Err(Error::MessageTooShort { expected: 99, .. }) - )); - } - - #[test] - fn test_tree_announce_decode_ancestry_count_mismatch() { - use crate::identity::Identity; - - let identity = Identity::generate(); - let node_addr = *identity.node_addr(); - - let mut decl = ParentDeclaration::new(node_addr, node_addr, 1, 1000); - sign_decl(&mut decl, &identity); - - let ancestry = TreeCoordinate::new(vec![CoordEntry::new(node_addr, 1, 1000)]).unwrap(); - let announce = TreeAnnounce::new(decl, ancestry); - let mut encoded = announce.encode().unwrap(); - - // The ancestry_count is at offset: 1 (msg_type) + 1 (version) + 8 (seq) + 8 (ts) + 16 (parent) = 34 - // Set ancestry_count to 5 but we only have 1 entry's worth of data - encoded[34] = 5; - encoded[35] = 0; - - let result = TreeAnnounce::decode(&encoded[1..]); - assert!(matches!(result, Err(Error::MessageTooShort { .. }))); - } - - #[test] - fn test_tree_announce_encode_unsigned_fails() { - let node = make_node_addr(1); - let decl = ParentDeclaration::new(node, node, 1, 1000); - let ancestry = make_coords(&[1, 0]); - - let announce = TreeAnnounce::new(decl, ancestry); - let result = announce.encode(); - assert!(matches!(result, Err(Error::InvalidSignature))); - } - - /// Tests that a well-formed non-root ancestry is accepted. - #[test] - fn test_tree_announce_validate_semantics_accepts_valid_non_root() { - use crate::identity::Identity; - - // Regenerate until the random identity's node_addr is numerically - // larger than both fixed parent (02:..) and root (01:..), so the - // root-minimum invariant holds deterministically. - let identity = loop { - let id = Identity::generate(); - if id.node_addr().as_bytes()[0] > 1 { - break id; - } - }; - let node_addr = *identity.node_addr(); - let parent = make_node_addr(2); - let root = make_node_addr(1); - - let mut decl = ParentDeclaration::new(node_addr, parent, 5, 1000); - sign_decl(&mut decl, &identity); - - let ancestry = TreeCoordinate::new(vec![ - CoordEntry::new(node_addr, 5, 1000), - CoordEntry::new(parent, 4, 900), - CoordEntry::new(root, 3, 800), - ]) - .unwrap(); - - let announce = TreeAnnounce::new(decl, ancestry); - assert!(announce.validate_semantics().is_ok()); - } - - /// Tests that an ancestry is rejected if the final node_addr is not the smallest entry in the path. - #[test] - fn test_tree_announce_validate_semantics_rejects_non_minimal_root() { - use crate::identity::Identity; - - let identity = Identity::generate(); - let node_addr = *identity.node_addr(); - let smaller = make_node_addr(0); - let advertised_root = make_node_addr(1); - - let mut decl = ParentDeclaration::new(node_addr, smaller, 5, 1000); - sign_decl(&mut decl, &identity); - - let ancestry = TreeCoordinate::new(vec![ - CoordEntry::new(node_addr, 5, 1000), - CoordEntry::new(smaller, 4, 900), - CoordEntry::new(advertised_root, 3, 800), - ]) - .unwrap(); - - let announce = TreeAnnounce::new(decl, ancestry); - assert!(matches!( - announce.validate_semantics(), - Err(TreeError::AncestryRootNotMinimum { - advertised, - minimum, - }) if advertised == advertised_root && minimum == smaller - )); - } - - /// Tests that an ancestry is rejected if the first ancestry hop does not match the signed parent_id. - #[test] - fn test_tree_announce_validate_semantics_rejects_parent_mismatch() { - use crate::identity::Identity; - - let identity = Identity::generate(); - let node_addr = *identity.node_addr(); - let declared_parent = make_node_addr(2); - let ancestry_parent = make_node_addr(3); - - let mut decl = ParentDeclaration::new(node_addr, declared_parent, 5, 1000); - sign_decl(&mut decl, &identity); - - let ancestry = TreeCoordinate::new(vec![ - CoordEntry::new(node_addr, 5, 1000), - CoordEntry::new(ancestry_parent, 4, 900), - CoordEntry::new(make_node_addr(1), 3, 800), - ]) - .unwrap(); - - let announce = TreeAnnounce::new(decl, ancestry); - assert!(matches!( - announce.validate_semantics(), - Err(TreeError::AncestryParentMismatch { - declared, - ancestry, - }) if declared == declared_parent && ancestry == ancestry_parent - )); - } - - /// Tests that an ancestry is rejected if the first path entry does not match the signed sender node_addr. - #[test] - fn test_tree_announce_validate_semantics_rejects_sender_mismatch() { - use crate::identity::Identity; - - let identity = Identity::generate(); - let node_addr = *identity.node_addr(); - let ancestry_sender = make_node_addr(9); - let parent = make_node_addr(2); - - let mut decl = ParentDeclaration::new(node_addr, parent, 5, 1000); - sign_decl(&mut decl, &identity); - - let ancestry = TreeCoordinate::new(vec![ - CoordEntry::new(ancestry_sender, 5, 1000), - CoordEntry::new(parent, 4, 900), - CoordEntry::new(make_node_addr(1), 3, 800), - ]) - .unwrap(); - - let announce = TreeAnnounce::new(decl, ancestry); - assert!(matches!( - announce.validate_semantics(), - Err(TreeError::AncestryNodeMismatch { - declared, - ancestry, - }) if declared == node_addr && ancestry == ancestry_sender - )); - } - - /// Tests that a self-root declaration is rejected if its ancestry contains extra ancestors. - #[test] - fn test_tree_announce_validate_semantics_rejects_root_with_ancestors() { - use crate::identity::Identity; - - let identity = Identity::generate(); - let node_addr = *identity.node_addr(); - - let mut decl = ParentDeclaration::self_root(node_addr, 5, 1000); - sign_decl(&mut decl, &identity); - - let ancestry = TreeCoordinate::new(vec![ - CoordEntry::new(node_addr, 5, 1000), - CoordEntry::new(make_node_addr(0), 4, 900), - ]) - .unwrap(); - - let announce = TreeAnnounce::new(decl, ancestry); - assert!(matches!( - announce.validate_semantics(), - Err(TreeError::RootDeclarationMismatch) - )); - } -}