#![allow(missing_docs, clippy::indexing_slicing)]
use std::time::{SystemTime, UNIX_EPOCH};
use soma_som_core::crossing::CrossingRecord;
use soma_som_core::envelope::Envelope;
use soma_som_core::signing_provider::{CrossingSigner, SigningProviderError};
use soma_som_core::timing::{CycleTimer, TimingConfig, TimingError};
use soma_som_core::types::{CrossingType, UnitId};
use tracing::instrument;
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum BoundaryError {
#[error(
"Routing violation: {src_unit} is not the predecessor of {dst_unit} \
(expected {expected})"
)]
RoutingViolation {
src_unit: UnitId,
dst_unit: UnitId,
expected: UnitId,
},
#[error("Envelope validation failed: {reason}")]
EnvelopeInvalid { reason: String },
#[error("Source mismatch: envelope claims source={claimed}, expected={expected}")]
SourceMismatch { claimed: UnitId, expected: UnitId },
#[error(
"Cycle index mismatch: envelope has {envelope_cycle}, boundary expects {boundary_cycle}"
)]
CycleIndexMismatch {
envelope_cycle: u64,
boundary_cycle: u64,
},
#[error(transparent)]
Timing(#[from] TimingError),
#[error("Crossing record signature verification failed at sequence {sequence_number}")]
SignatureVerificationFailed { sequence_number: u8 },
#[error("Chain hash verification failed at sequence {sequence_number}")]
ChainHashVerificationFailed { sequence_number: u8 },
#[error("No cycle in progress — call begin_cycle first")]
NoCycleInProgress,
#[error("Cycle already in progress (cycle_index={cycle_index})")]
CycleAlreadyInProgress { cycle_index: u64 },
#[error(transparent)]
Signing(#[from] SigningProviderError),
}
pub const STRIPPED_POINTER_SENTINEL: [u8; 32] = [0u8; 32];
#[derive(Debug, Clone)]
pub struct StrippedEnvelope {
pub envelope: Envelope,
pub stripped_pointer: [u8; 32],
}
pub fn strip_pointer(envelope: &Envelope) -> StrippedEnvelope {
let stripped_pointer = envelope.quad.pointer;
let mut env = envelope.clone();
env.quad.pointer = STRIPPED_POINTER_SENTINEL;
StrippedEnvelope {
envelope: env,
stripped_pointer,
}
}
pub fn is_stripped(envelope: &Envelope) -> bool {
envelope.quad.pointer == STRIPPED_POINTER_SENTINEL
}
pub fn has_live_pointer(envelope: &Envelope) -> bool {
!is_stripped(envelope)
}
pub fn validate_routing(src: UnitId, dst: UnitId) -> Result<(), BoundaryError> {
let expected = src.successor();
if expected != dst {
return Err(BoundaryError::RoutingViolation {
src_unit: src,
dst_unit: dst,
expected,
});
}
Ok(())
}
pub fn validate_routing_for_crossing(
src: UnitId,
dst: UnitId,
crossing_type: CrossingType,
) -> Result<(), BoundaryError> {
match crossing_type {
CrossingType::Vertical => validate_routing(src, dst),
CrossingType::Horizontal => {
if src != dst {
return Err(BoundaryError::RoutingViolation {
src_unit: src,
dst_unit: dst,
expected: src,
});
}
Ok(())
}
}
}
pub fn validate_envelope(
envelope: &Envelope,
expected_source: UnitId,
destination: UnitId,
expected_cycle: u64,
) -> Result<(), BoundaryError> {
if envelope.source_unit != expected_source {
return Err(BoundaryError::SourceMismatch {
claimed: envelope.source_unit,
expected: expected_source,
});
}
validate_routing_for_crossing(expected_source, destination, envelope.crossing_type)?;
if envelope.cycle_index != expected_cycle {
return Err(BoundaryError::CycleIndexMismatch {
envelope_cycle: envelope.cycle_index,
boundary_cycle: expected_cycle,
});
}
if envelope.cycle_index > 0 && envelope.quad.is_empty() {
return Err(BoundaryError::EnvelopeInvalid {
reason: "empty Quad in active cycle".into(),
});
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct CrossingResult {
pub delivery_envelope: Envelope,
pub crossing_record: CrossingRecord,
pub stripped_pointer: [u8; 32],
}
struct CycleState {
cycle_index: u64,
prev_hash: [u8; 32],
next_sequence: u8,
timer: CycleTimer,
crossing_records: Vec<CrossingRecord>,
}
pub struct Boundary {
signer: Box<dyn CrossingSigner>,
timing_config: TimingConfig,
cycle_state: Option<CycleState>,
}
impl std::fmt::Debug for Boundary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Boundary")
.field("timing_config", &self.timing_config)
.field("cycle_active", &self.cycle_state.is_some())
.finish()
}
}
impl Boundary {
pub fn new(signer: Box<dyn CrossingSigner>, timing_config: TimingConfig) -> Self {
Self { signer, timing_config, cycle_state: None }
}
pub fn verifying_key_bytes(&self) -> [u8; 32] {
self.signer.verifying_key_bytes()
}
#[instrument(skip(self, initial_prev_hash), name = "boundary.begin_cycle")]
pub fn begin_cycle(
&mut self,
cycle_index: u64,
initial_prev_hash: [u8; 32],
) -> Result<(), BoundaryError> {
if let Some(state) = self.cycle_state.as_ref() {
return Err(BoundaryError::CycleAlreadyInProgress {
cycle_index: state.cycle_index,
});
}
self.cycle_state = Some(CycleState {
cycle_index,
prev_hash: initial_prev_hash,
next_sequence: 1,
timer: CycleTimer::new(self.timing_config),
crossing_records: Vec::with_capacity(12),
});
Ok(())
}
#[instrument(skip_all, fields(source = ?expected_source, dst = ?destination), name = "boundary.crossing")]
pub fn process_crossing(
&mut self,
envelope: &Envelope,
expected_source: UnitId,
destination: UnitId,
) -> Result<CrossingResult, BoundaryError> {
let crossing_type = envelope.crossing_type;
let state = self.cycle_state.as_mut().ok_or(BoundaryError::NoCycleInProgress)?;
validate_envelope(envelope, expected_source, destination, state.cycle_index)?;
state.timer.check_cycle_timeout()?;
let StrippedEnvelope { envelope: mut stripped_env, stripped_pointer } =
strip_pointer(envelope);
let timestamp_ns = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64;
let sequence_number = state.next_sequence;
let chain_hash = CrossingRecord::compute_chain_hash(
expected_source,
destination,
state.cycle_index,
sequence_number,
crossing_type,
timestamp_ns,
&state.prev_hash,
);
let mut record = CrossingRecord {
source: expected_source,
destination,
cycle_index: state.cycle_index,
sequence_number,
crossing_type,
timestamp_ns,
prev_hash: state.prev_hash,
chain_hash,
signature: [0u8; 64],
};
record.signature = self.signer.sign(&record)?;
stripped_env.crossing_record = Some(record.clone());
state.prev_hash = record.chain_hash;
state.next_sequence += 1;
state.crossing_records.push(record.clone());
Ok(CrossingResult {
delivery_envelope: stripped_env,
crossing_record: record,
stripped_pointer,
})
}
pub fn start_unit_timer(&mut self, unit_id: UnitId) -> Result<(), BoundaryError> {
let state = self.cycle_state.as_mut().ok_or(BoundaryError::NoCycleInProgress)?;
state.timer.start_unit(unit_id);
Ok(())
}
pub fn end_unit_timer(
&mut self,
unit_id: UnitId,
) -> Result<std::time::Duration, BoundaryError> {
let state = self.cycle_state.as_mut().ok_or(BoundaryError::NoCycleInProgress)?;
Ok(state.timer.end_unit(unit_id)?)
}
pub fn end_cycle(&mut self) -> Result<([u8; 32], Vec<CrossingRecord>), BoundaryError> {
let state = self.cycle_state.take().ok_or(BoundaryError::NoCycleInProgress)?;
state.timer.check_cycle_timeout()?;
Ok((state.prev_hash, state.crossing_records))
}
pub fn current_cycle_index(&self) -> Option<u64> {
self.cycle_state.as_ref().map(|s| s.cycle_index)
}
pub fn current_sequence(&self) -> Option<u8> {
self.cycle_state.as_ref().map(|s| s.next_sequence)
}
pub fn is_cycle_active(&self) -> bool {
self.cycle_state.is_some()
}
pub fn cycle_elapsed_ms(&self) -> Option<u64> {
self.cycle_state.as_ref().map(|s| s.timer.cycle_elapsed_ms())
}
}
#[cfg(test)]
mod tests {
use super::*;
use soma_som_core::quad::{Quad, Tree};
use soma_som_core::NoopSigner;
fn make_boundary() -> Boundary {
Boundary::new(Box::new(NoopSigner), TimingConfig::default())
}
fn make_envelope(source: UnitId, cycle: u64) -> Envelope {
let mut tree = Tree::new();
tree.insert("test.key".into(), vec![1, 2, 3]);
let quad = Quad::from_strings("root", "secret_pointer", tree);
Envelope::new(cycle, source, quad)
}
#[test]
fn begin_cycle_sets_state() {
let mut b = make_boundary();
assert!(!b.is_cycle_active());
b.begin_cycle(1, [0u8; 32]).unwrap();
assert!(b.is_cycle_active());
assert_eq!(b.current_cycle_index(), Some(1));
}
#[test]
fn double_begin_rejected() {
let mut b = make_boundary();
b.begin_cycle(1, [0u8; 32]).unwrap();
assert!(matches!(
b.begin_cycle(2, [0u8; 32]),
Err(BoundaryError::CycleAlreadyInProgress { cycle_index: 1 })
));
}
#[test]
fn end_cycle_clears_state() {
let mut b = make_boundary();
b.begin_cycle(1, [0u8; 32]).unwrap();
let (_, records) = b.end_cycle().unwrap();
assert!(records.is_empty());
assert!(!b.is_cycle_active());
}
#[test]
fn process_crossing_strips_pointer() {
let mut b = make_boundary();
b.begin_cycle(1, [0u8; 32]).unwrap();
let env = make_envelope(UnitId::FU, 1);
let orig_ptr = env.quad.pointer;
let result = b.process_crossing(&env, UnitId::FU, UnitId::MU).unwrap();
assert!(is_stripped(&result.delivery_envelope));
assert_eq!(result.stripped_pointer, orig_ptr);
}
#[test]
fn process_crossing_attaches_record() {
let mut b = make_boundary();
b.begin_cycle(1, [0u8; 32]).unwrap();
let env = make_envelope(UnitId::FU, 1);
let result = b.process_crossing(&env, UnitId::FU, UnitId::MU).unwrap();
assert!(result.delivery_envelope.has_crossing_record());
let rec = result.delivery_envelope.crossing_record.as_ref().unwrap();
assert_eq!(rec.source, UnitId::FU);
assert_eq!(rec.destination, UnitId::MU);
assert!(rec.verify_chain_hash());
}
#[test]
fn chain_hashes_link_correctly() {
let mut b = make_boundary();
let initial = [99u8; 32];
b.begin_cycle(1, initial).unwrap();
let r1 = b.process_crossing(&make_envelope(UnitId::FU, 1), UnitId::FU, UnitId::MU).unwrap();
let r2 = b.process_crossing(&make_envelope(UnitId::MU, 1), UnitId::MU, UnitId::CU).unwrap();
assert_eq!(r1.crossing_record.prev_hash, initial);
assert_eq!(r2.crossing_record.prev_hash, r1.crossing_record.chain_hash);
}
#[test]
fn routing_violation_detected() {
let mut b = make_boundary();
b.begin_cycle(1, [0u8; 32]).unwrap();
let env = make_envelope(UnitId::FU, 1);
assert!(matches!(
b.process_crossing(&env, UnitId::FU, UnitId::CU),
Err(BoundaryError::RoutingViolation { .. })
));
}
#[test]
fn full_ring_cycle_six_records() {
let mut b = make_boundary();
b.begin_cycle(1, [0u8; 32]).unwrap();
let transitions = [
(UnitId::FU, UnitId::MU),
(UnitId::MU, UnitId::CU),
(UnitId::CU, UnitId::OU),
(UnitId::OU, UnitId::SU),
(UnitId::SU, UnitId::HU),
(UnitId::HU, UnitId::FU),
];
for (src, dst) in transitions {
let env = make_envelope(src, 1);
b.process_crossing(&env, src, dst).unwrap();
}
let (final_hash, records) = b.end_cycle().unwrap();
assert_eq!(records.len(), 6);
assert_eq!(final_hash, records[5].chain_hash);
}
}