#![allow(missing_docs)]
use serde::{Deserialize, Serialize};
use crate::error::SomaError;
use crate::quad::Quad;
use crate::ring::UnitState;
use crate::types::{Layer, UnitId};
pub trait PerspectivalState {
fn perspectival_state_hash(&self) -> [u8; 32];
}
impl PerspectivalState for UnitState {
fn perspectival_state_hash(&self) -> [u8; 32] {
unit_state_hash(self)
}
}
pub fn unit_state_hash(unit: &UnitState) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
for &layer in &Layer::ALL {
hash_quad_into(&mut hasher, unit.som_quad(layer));
}
hash_quad_into(&mut hasher, &unit.soma_quad);
*hasher.finalize().as_bytes()
}
fn hash_quad_into(hasher: &mut blake3::Hasher, quad: &Quad) {
hasher.update(&quad.root);
hasher.update(&quad.pointer);
for (key, value) in &quad.tree {
hasher.update(key.as_bytes());
hasher.update(&(value.len() as u64).to_le_bytes());
hasher.update(value);
}
}
#[derive(Debug, Clone)]
pub struct PerspectivalChain {
unit_id: UnitId,
entries: Vec<PerspectivalEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PerspectivalEntry {
pub cycle_index: u64,
pub state_hash: [u8; 32],
pub perspectival_hash: [u8; 32],
}
impl PerspectivalChain {
pub fn new(unit_id: UnitId) -> Self {
Self {
unit_id,
entries: Vec::new(),
}
}
pub fn unit_id(&self) -> UnitId {
self.unit_id
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn head(&self) -> Option<[u8; 32]> {
self.entries.last().map(|e| e.perspectival_hash)
}
pub fn entry(&self, cycle_index: u64) -> Option<&PerspectivalEntry> {
self.entries.get(cycle_index as usize)
}
pub fn genesis(&self) -> Option<&PerspectivalEntry> {
self.entries.first()
}
pub fn latest(&self) -> Option<&PerspectivalEntry> {
self.entries.last()
}
pub fn commit_genesis(
&mut self,
state_hash: [u8; 32],
genesis_seed: &[u8],
) -> Result<PerspectivalEntry, SomaError> {
if !self.entries.is_empty() {
return Err(SomaError::GenesisAlreadyRecorded);
}
let perspectival_hash = compute_perspectival_hash(&state_hash, genesis_seed);
let entry = PerspectivalEntry {
cycle_index: 0,
state_hash,
perspectival_hash,
};
self.entries.push(entry.clone());
Ok(entry)
}
pub fn commit(&mut self, state_hash: [u8; 32]) -> Result<PerspectivalEntry, SomaError> {
let prev = self.entries.last().ok_or(SomaError::LedgerEmpty)?;
let prev_hash = prev.perspectival_hash;
let cycle_index = prev.cycle_index + 1;
let perspectival_hash = compute_perspectival_hash(&state_hash, &prev_hash);
let entry = PerspectivalEntry {
cycle_index,
state_hash,
perspectival_hash,
};
self.entries.push(entry.clone());
Ok(entry)
}
pub fn verify_chain(&self, genesis_seed: &[u8]) -> Result<(), SomaError> {
if self.entries.is_empty() {
return Ok(());
}
#[allow(clippy::indexing_slicing)] let genesis = &self.entries[0];
let expected_genesis = compute_perspectival_hash(&genesis.state_hash, genesis_seed);
if genesis.perspectival_hash != expected_genesis {
return Err(SomaError::ChainIntegrityViolation { cycle_index: 0 });
}
for i in 1..self.entries.len() {
#[allow(clippy::indexing_slicing)] let prev = &self.entries[i - 1];
#[allow(clippy::indexing_slicing)]
let curr = &self.entries[i];
let expected = compute_perspectival_hash(&curr.state_hash, &prev.perspectival_hash);
if curr.perspectival_hash != expected {
return Err(SomaError::ChainIntegrityViolation {
cycle_index: curr.cycle_index,
});
}
}
Ok(())
}
}
pub fn cross_verification_digest(perspectival_hashes: &[[u8; 32]; 6]) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
for hash in perspectival_hashes {
hasher.update(hash);
}
*hasher.finalize().as_bytes()
}
fn compute_perspectival_hash(state_hash: &[u8; 32], chain_input: &[u8]) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(state_hash);
hasher.update(chain_input);
*hasher.finalize().as_bytes()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::quad::Tree;
const TEST_SEED: &[u8] = b"soma-perspectival-test-seed-v1";
fn make_unit_state(unit: UnitId, cycle: u64) -> UnitState {
let mut us = UnitState::new(unit);
us.soma_quad = Quad::from_strings(
&format!("{unit}.soma.root.c{cycle}"),
&format!("{unit}.soma.ptr.c{cycle}"),
{
let mut t = Tree::new();
t.insert("unit".into(), format!("{unit}").into_bytes());
t.insert("cycle".into(), cycle.to_le_bytes().to_vec());
t
},
);
for &layer in &Layer::ALL {
let mut tree = Tree::new();
tree.insert(
"origin".into(),
format!("{unit}.{layer}.c{cycle}").into_bytes(),
);
*us.som_quad_mut(layer) = Quad::from_strings(
&format!("{unit}.{layer}.root.c{cycle}"),
&format!("{unit}.{layer}.ptr.c{cycle}"),
tree,
);
}
us
}
#[test]
fn perspectival_state_hash_is_deterministic() {
let us = make_unit_state(UnitId::FU, 0);
let h1 = us.perspectival_state_hash();
let h2 = us.perspectival_state_hash();
assert_eq!(h1, h2);
}
#[test]
fn perspectival_state_hash_differs_across_units() {
let states: Vec<_> = UnitId::ALL
.iter()
.map(|&u| make_unit_state(u, 0).perspectival_state_hash())
.collect();
let mut sorted = states.clone();
sorted.sort();
sorted.dedup();
assert_eq!(
sorted.len(),
6,
"All units must produce distinct state hashes"
);
}
#[test]
fn perspectival_state_hash_differs_across_cycles() {
let h0 = make_unit_state(UnitId::FU, 0).perspectival_state_hash();
let h1 = make_unit_state(UnitId::FU, 1).perspectival_state_hash();
assert_ne!(
h0, h1,
"Same unit at different cycles must produce different state hashes"
);
}
#[test]
fn perspectival_state_hash_sensitive_to_any_quad_change() {
let base = make_unit_state(UnitId::CU, 3);
let base_hash = base.perspectival_state_hash();
for &layer in &Layer::ALL {
let mut modified = base.clone();
modified.som_quad_mut(layer).root[0] ^= 0xFF;
assert_ne!(
modified.perspectival_state_hash(),
base_hash,
"Changing {}.{:?} root must change perspectival state hash",
UnitId::CU,
layer
);
}
let mut modified = base.clone();
modified.soma_quad.root[0] ^= 0xFF;
assert_ne!(
modified.perspectival_state_hash(),
base_hash,
"Changing SOMA Quad must change perspectival state hash"
);
}
#[test]
fn genesis_commit_creates_chain() {
let mut chain = PerspectivalChain::new(UnitId::FU);
let state_hash = make_unit_state(UnitId::FU, 0).perspectival_state_hash();
let entry = chain.commit_genesis(state_hash, TEST_SEED).unwrap();
assert_eq!(entry.cycle_index, 0);
assert_eq!(chain.len(), 1);
assert_eq!(chain.head(), Some(entry.perspectival_hash));
}
#[test]
fn genesis_hash_formula_is_correct() {
let mut chain = PerspectivalChain::new(UnitId::FU);
let state_hash = make_unit_state(UnitId::FU, 0).perspectival_state_hash();
let entry = chain.commit_genesis(state_hash, TEST_SEED).unwrap();
let mut hasher = blake3::Hasher::new();
hasher.update(&state_hash);
hasher.update(TEST_SEED);
let expected: [u8; 32] = *hasher.finalize().as_bytes();
assert_eq!(entry.perspectival_hash, expected);
}
#[test]
fn cannot_commit_genesis_twice() {
let mut chain = PerspectivalChain::new(UnitId::FU);
let state_hash = [0xAA; 32];
chain.commit_genesis(state_hash, TEST_SEED).unwrap();
assert!(chain.commit_genesis(state_hash, TEST_SEED).is_err());
}
#[test]
fn different_seeds_produce_different_genesis_hashes() {
let state_hash = make_unit_state(UnitId::FU, 0).perspectival_state_hash();
let mut chain_a = PerspectivalChain::new(UnitId::FU);
let entry_a = chain_a.commit_genesis(state_hash, b"seed-alpha").unwrap();
let mut chain_b = PerspectivalChain::new(UnitId::FU);
let entry_b = chain_b.commit_genesis(state_hash, b"seed-beta").unwrap();
assert_ne!(entry_a.perspectival_hash, entry_b.perspectival_hash);
}
#[test]
fn standard_commit_extends_chain() {
let mut chain = PerspectivalChain::new(UnitId::MU);
let s0 = make_unit_state(UnitId::MU, 0).perspectival_state_hash();
chain.commit_genesis(s0, TEST_SEED).unwrap();
let s1 = make_unit_state(UnitId::MU, 1).perspectival_state_hash();
let entry = chain.commit(s1).unwrap();
assert_eq!(entry.cycle_index, 1);
assert_eq!(chain.len(), 2);
}
#[test]
fn standard_hash_formula_is_correct() {
let mut chain = PerspectivalChain::new(UnitId::MU);
let s0 = make_unit_state(UnitId::MU, 0).perspectival_state_hash();
let genesis_entry = chain.commit_genesis(s0, TEST_SEED).unwrap();
let s1 = make_unit_state(UnitId::MU, 1).perspectival_state_hash();
let entry = chain.commit(s1).unwrap();
let mut hasher = blake3::Hasher::new();
hasher.update(&s1);
hasher.update(&genesis_entry.perspectival_hash);
let expected: [u8; 32] = *hasher.finalize().as_bytes();
assert_eq!(entry.perspectival_hash, expected);
}
#[test]
fn cannot_commit_standard_without_genesis() {
let mut chain = PerspectivalChain::new(UnitId::FU);
assert!(chain.commit([0xBB; 32]).is_err());
}
#[test]
fn multi_cycle_chain() {
let mut chain = PerspectivalChain::new(UnitId::HU);
let s0 = make_unit_state(UnitId::HU, 0).perspectival_state_hash();
chain.commit_genesis(s0, TEST_SEED).unwrap();
for cycle in 1..=10 {
let sh = make_unit_state(UnitId::HU, cycle).perspectival_state_hash();
let entry = chain.commit(sh).unwrap();
assert_eq!(entry.cycle_index, cycle);
}
assert_eq!(chain.len(), 11);
}
#[test]
fn chain_verification_succeeds_for_honest_chain() {
let mut chain = PerspectivalChain::new(UnitId::CU);
let s0 = make_unit_state(UnitId::CU, 0).perspectival_state_hash();
chain.commit_genesis(s0, TEST_SEED).unwrap();
for cycle in 1..=5 {
let sh = make_unit_state(UnitId::CU, cycle).perspectival_state_hash();
chain.commit(sh).unwrap();
}
assert!(chain.verify_chain(TEST_SEED).is_ok());
}
#[test]
fn chain_verification_detects_tampered_state_hash() {
let mut chain = PerspectivalChain::new(UnitId::OU);
let s0 = make_unit_state(UnitId::OU, 0).perspectival_state_hash();
chain.commit_genesis(s0, TEST_SEED).unwrap();
for cycle in 1..=5 {
let sh = make_unit_state(UnitId::OU, cycle).perspectival_state_hash();
chain.commit(sh).unwrap();
}
chain.entries[3].state_hash[0] ^= 0xFF;
let result = chain.verify_chain(TEST_SEED);
assert!(result.is_err());
match result.unwrap_err() {
SomaError::ChainIntegrityViolation { cycle_index } => {
assert_eq!(cycle_index, 3);
}
other => panic!("Expected ChainIntegrityViolation, got {other:?}"),
}
}
#[test]
fn chain_verification_detects_tampered_genesis() {
let mut chain = PerspectivalChain::new(UnitId::SU);
let s0 = make_unit_state(UnitId::SU, 0).perspectival_state_hash();
chain.commit_genesis(s0, TEST_SEED).unwrap();
chain.entries[0].state_hash[0] ^= 0xFF;
let result = chain.verify_chain(TEST_SEED);
assert!(result.is_err());
match result.unwrap_err() {
SomaError::ChainIntegrityViolation { cycle_index } => {
assert_eq!(cycle_index, 0);
}
other => panic!(
"Expected ChainIntegrityViolation at genesis, got {other:?}"
),
}
}
#[test]
fn chain_verification_detects_wrong_seed() {
let mut chain = PerspectivalChain::new(UnitId::FU);
let s0 = make_unit_state(UnitId::FU, 0).perspectival_state_hash();
chain.commit_genesis(s0, TEST_SEED).unwrap();
let result = chain.verify_chain(b"wrong-seed");
assert!(result.is_err());
}
#[test]
fn empty_chain_verification_succeeds() {
let chain = PerspectivalChain::new(UnitId::FU);
assert!(chain.verify_chain(TEST_SEED).is_ok());
}
#[test]
fn perspectival_chains_are_independent() {
let mut chains: Vec<PerspectivalChain> = UnitId::ALL
.iter()
.map(|&u| {
let mut chain = PerspectivalChain::new(u);
let s0 = make_unit_state(u, 0).perspectival_state_hash();
chain.commit_genesis(s0, TEST_SEED).unwrap();
for cycle in 1..=3 {
let sh = make_unit_state(u, cycle).perspectival_state_hash();
chain.commit(sh).unwrap();
}
chain
})
.collect();
let original_heads: Vec<[u8; 32]> = chains.iter().map(|c| c.head().unwrap()).collect();
let extra = make_unit_state(UnitId::FU, 4).perspectival_state_hash();
chains[0].commit(extra).unwrap();
assert_ne!(chains[0].head().unwrap(), original_heads[0]);
for i in 1..6 {
assert_eq!(
chains[i].head().unwrap(),
original_heads[i],
"Chain for {:?} should not change when FU's chain is extended",
UnitId::ALL[i]
);
}
}
#[test]
fn cross_verification_digest_is_deterministic() {
let hashes: [[u8; 32]; 6] =
core::array::from_fn(|i| *blake3::hash(format!("unit-{i}").as_bytes()).as_bytes());
let d1 = cross_verification_digest(&hashes);
let d2 = cross_verification_digest(&hashes);
assert_eq!(d1, d2);
}
#[test]
fn cross_verification_digest_formula_is_correct() {
let hashes: [[u8; 32]; 6] =
core::array::from_fn(|i| *blake3::hash(format!("unit-{i}").as_bytes()).as_bytes());
let digest = cross_verification_digest(&hashes);
let mut hasher = blake3::Hasher::new();
for h in &hashes {
hasher.update(h);
}
let expected: [u8; 32] = *hasher.finalize().as_bytes();
assert_eq!(digest, expected);
}
#[test]
fn cross_verification_sensitivity_to_single_unit_change() {
let base_hashes: [[u8; 32]; 6] =
core::array::from_fn(|i| *blake3::hash(format!("unit-{i}").as_bytes()).as_bytes());
let base_digest = cross_verification_digest(&base_hashes);
for i in 0..6 {
let mut modified = base_hashes;
modified[i][0] ^= 0xFF; let modified_digest = cross_verification_digest(&modified);
assert_ne!(
base_digest,
modified_digest,
"Changing unit {}'s hash must change the cross-verification digest",
UnitId::ALL[i]
);
}
}
#[test]
fn cross_verification_is_ring_order_sensitive() {
let hashes: [[u8; 32]; 6] = core::array::from_fn(|i| {
*blake3::hash(format!("distinct-unit-{i}").as_bytes()).as_bytes()
});
let original_digest = cross_verification_digest(&hashes);
let mut swapped = hashes;
swapped.swap(0, 1);
let swapped_digest = cross_verification_digest(&swapped);
assert_ne!(
original_digest, swapped_digest,
"Swapping unit order must change the digest (ring-order sensitivity)"
);
}
#[test]
fn cross_verification_all_single_changes_produce_distinct_digests() {
let base_hashes: [[u8; 32]; 6] = core::array::from_fn(|i| {
*blake3::hash(format!("base-unit-{i}").as_bytes()).as_bytes()
});
let mut digests = Vec::with_capacity(6);
for i in 0..6 {
let mut modified = base_hashes;
modified[i][0] ^= 0xFF;
digests.push(cross_verification_digest(&modified));
}
let count = digests.len();
digests.sort();
digests.dedup();
assert_eq!(
digests.len(),
count,
"Each single-unit change must produce a distinct digest"
);
}
#[test]
fn full_perspectival_ledger_workflow() {
let mut chains: Vec<PerspectivalChain> = UnitId::ALL
.iter()
.map(|&u| PerspectivalChain::new(u))
.collect();
let mut genesis_hashes = [[0u8; 32]; 6];
for (i, &unit) in UnitId::ALL.iter().enumerate() {
let state_hash = make_unit_state(unit, 0).perspectival_state_hash();
let entry = chains[i].commit_genesis(state_hash, TEST_SEED).unwrap();
genesis_hashes[i] = entry.perspectival_hash;
}
let genesis_xv = cross_verification_digest(&genesis_hashes);
assert_ne!(genesis_xv, [0u8; 32]);
let mut prev_xv = genesis_xv;
for cycle in 1..=3u64 {
let mut cycle_hashes = [[0u8; 32]; 6];
for (i, &unit) in UnitId::ALL.iter().enumerate() {
let state_hash = make_unit_state(unit, cycle).perspectival_state_hash();
let entry = chains[i].commit(state_hash).unwrap();
cycle_hashes[i] = entry.perspectival_hash;
}
let xv = cross_verification_digest(&cycle_hashes);
assert_ne!(xv, prev_xv, "Digest must change between cycles");
prev_xv = xv;
}
for chain in &chains {
assert!(chain.verify_chain(TEST_SEED).is_ok());
}
}
}