mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-11 09:07:44 +00:00
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.
This commit is contained in:
@@ -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<u8>) {
|
||||
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<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 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<u8>) {
|
||||
buf.extend_from_slice(&0u16.to_le_bytes());
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<u8> {
|
||||
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 {}
|
||||
@@ -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)]
|
||||
|
||||
+1
-140
@@ -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<u8> {
|
||||
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 {}
|
||||
|
||||
@@ -4,3 +4,4 @@ mod coordinate;
|
||||
mod limits;
|
||||
mod state;
|
||||
mod util;
|
||||
mod wire;
|
||||
|
||||
@@ -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)
|
||||
));
|
||||
}
|
||||
@@ -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<u8>) {
|
||||
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<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 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<u8>) {
|
||||
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)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user