#![allow(missing_docs)]
use serde::{Deserialize, Serialize};
use crate::error::SomaError;
use crate::traceability::{CycleContext, LEGACY_SEMANTIC_HASH};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LedgerEntry {
pub cycle_index: u64,
pub awareness_fingerprint: [u8; 32],
pub system_fingerprint: [u8; 32],
pub chain_hash: [u8; 32],
#[serde(default)]
pub semantic_hash: [u8; 32],
#[serde(default)]
pub context: Option<CycleContext>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemporalLedger {
entries: Vec<LedgerEntry>,
}
impl TemporalLedger {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn record_genesis(
&mut self,
awareness_fingerprint: [u8; 32],
system_fingerprint: [u8; 32],
genesis_hash: [u8; 32],
context: CycleContext,
) -> Result<(), SomaError> {
if !self.entries.is_empty() {
return Err(SomaError::GenesisAlreadyRecorded);
}
let semantic_hash = context.semantic_hash();
self.entries.push(LedgerEntry {
cycle_index: 0,
awareness_fingerprint,
system_fingerprint,
chain_hash: genesis_hash,
semantic_hash,
context: Some(context),
});
Ok(())
}
pub fn record_cycle(
&mut self,
awareness_fingerprint: [u8; 32],
system_fingerprint: [u8; 32],
context: CycleContext,
) -> Result<LedgerEntry, SomaError> {
let prev = self.entries.last().ok_or(SomaError::LedgerEmpty)?;
let prev_hash = prev.chain_hash;
let cycle_index = prev.cycle_index + 1;
let semantic_hash = context.semantic_hash();
let chain_hash = Self::compute_chain_hash(&system_fingerprint, &semantic_hash, &prev_hash);
let entry = LedgerEntry {
cycle_index,
awareness_fingerprint,
system_fingerprint,
chain_hash,
semantic_hash,
context: Some(context),
};
self.entries.push(entry.clone());
Ok(entry)
}
pub fn latest(&self) -> Option<&LedgerEntry> {
self.entries.last()
}
pub fn genesis(&self) -> Option<&LedgerEntry> {
self.entries.first()
}
pub fn entry(&self, cycle_index: u64) -> Option<&LedgerEntry> {
self.entries.get(cycle_index as usize)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn chain_head(&self) -> Option<[u8; 32]> {
self.entries.last().map(|e| e.chain_hash)
}
pub fn compute_chain_hash(
system_fingerprint: &[u8; 32],
semantic_hash: &[u8; 32],
prev_hash: &[u8; 32],
) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(system_fingerprint);
hasher.update(semantic_hash);
hasher.update(prev_hash);
*hasher.finalize().as_bytes()
}
fn compute_legacy_chain_hash(system_fingerprint: &[u8; 32], prev_hash: &[u8; 32]) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(system_fingerprint);
hasher.update(prev_hash);
*hasher.finalize().as_bytes()
}
pub fn verify_chain(&self) -> Result<(), SomaError> {
if self.entries.is_empty() {
return Ok(());
}
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 = if curr.semantic_hash == LEGACY_SEMANTIC_HASH {
Self::compute_legacy_chain_hash(&curr.system_fingerprint, &prev.chain_hash)
} else {
Self::compute_chain_hash(
&curr.system_fingerprint,
&curr.semantic_hash,
&prev.chain_hash,
)
};
if curr.chain_hash != expected {
return Err(SomaError::ChainIntegrityViolation {
cycle_index: curr.cycle_index,
});
}
}
Ok(())
}
}
impl Default for TemporalLedger {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traceability::CycleContext;
fn make_fingerprints(cycle: u64) -> ([u8; 32], [u8; 32]) {
let awareness = *blake3::hash(format!("awareness-{cycle}").as_bytes()).as_bytes();
let system = *blake3::hash(format!("system-{cycle}").as_bytes()).as_bytes();
(awareness, system)
}
#[test]
fn empty_ledger() {
let ledger = TemporalLedger::new();
assert!(ledger.is_empty());
assert_eq!(ledger.len(), 0);
assert!(ledger.verify_chain().is_ok());
}
#[test]
fn genesis_recording() {
let mut ledger = TemporalLedger::new();
let (a, s) = make_fingerprints(0);
let genesis_hash = *blake3::hash(b"genesis").as_bytes();
ledger
.record_genesis(a, s, genesis_hash, CycleContext::genesis())
.unwrap();
assert_eq!(ledger.len(), 1);
assert_eq!(ledger.genesis().unwrap().cycle_index, 0);
assert_eq!(ledger.chain_head(), Some(genesis_hash));
}
#[test]
fn genesis_carries_semantic_hash() {
let mut ledger = TemporalLedger::new();
let (a, s) = make_fingerprints(0);
let genesis_hash = *blake3::hash(b"genesis").as_bytes();
let ctx = CycleContext::genesis();
let expected_semantic = ctx.semantic_hash();
ledger.record_genesis(a, s, genesis_hash, ctx).unwrap();
let entry = ledger.genesis().unwrap();
assert_eq!(entry.semantic_hash, expected_semantic);
assert!(entry.context.is_some());
}
#[test]
fn cannot_record_genesis_twice() {
let mut ledger = TemporalLedger::new();
let (a, s) = make_fingerprints(0);
let h = *blake3::hash(b"genesis").as_bytes();
ledger
.record_genesis(a, s, h, CycleContext::genesis())
.unwrap();
assert!(
ledger
.record_genesis(a, s, h, CycleContext::genesis())
.is_err()
);
}
#[test]
fn multi_cycle_chain() {
let mut ledger = TemporalLedger::new();
let (a0, s0) = make_fingerprints(0);
let genesis_hash = *blake3::hash(b"genesis-seed").as_bytes();
ledger
.record_genesis(a0, s0, genesis_hash, CycleContext::genesis())
.unwrap();
for cycle in 1..=10 {
let (a, s) = make_fingerprints(cycle);
let entry = ledger
.record_cycle(a, s, CycleContext::heartbeat())
.unwrap();
assert_eq!(entry.cycle_index, cycle);
}
assert_eq!(ledger.len(), 11);
assert!(ledger.verify_chain().is_ok());
}
#[test]
fn chain_tamper_detection() {
let mut ledger = TemporalLedger::new();
let (a0, s0) = make_fingerprints(0);
let genesis_hash = *blake3::hash(b"genesis").as_bytes();
ledger
.record_genesis(a0, s0, genesis_hash, CycleContext::genesis())
.unwrap();
for cycle in 1..=5 {
let (a, s) = make_fingerprints(cycle);
ledger
.record_cycle(a, s, CycleContext::heartbeat())
.unwrap();
}
ledger.entries[3].system_fingerprint[0] ^= 0xFF;
let result = ledger.verify_chain();
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 each_cycle_hash_depends_on_predecessor() {
let mut ledger = TemporalLedger::new();
let (a0, s0) = make_fingerprints(0);
let genesis_hash = *blake3::hash(b"genesis").as_bytes();
ledger
.record_genesis(a0, s0, genesis_hash, CycleContext::genesis())
.unwrap();
let (a1, s1) = make_fingerprints(1);
let ctx = CycleContext::heartbeat();
let semantic_hash = ctx.semantic_hash();
let entry1 = ledger.record_cycle(a1, s1, ctx).unwrap();
let expected = TemporalLedger::compute_chain_hash(&s1, &semantic_hash, &genesis_hash);
assert_eq!(entry1.chain_hash, expected);
}
#[test]
fn semantic_hash_included_in_chain_hash() {
let mut ledger = TemporalLedger::new();
let (a0, s0) = make_fingerprints(0);
let genesis_hash = *blake3::hash(b"genesis").as_bytes();
ledger
.record_genesis(a0, s0, genesis_hash, CycleContext::genesis())
.unwrap();
let (a1, s1) = make_fingerprints(1);
let ctx_a = CycleContext::command("user.create", "alice", "req-1", "authorized");
let entry_a = ledger.record_cycle(a1, s1, ctx_a).unwrap();
let mut ledger2 = TemporalLedger::new();
ledger2
.record_genesis(a0, s0, genesis_hash, CycleContext::genesis())
.unwrap();
let ctx_b = CycleContext::command("user.delete", "bob", "req-2", "denied");
let entry_b = ledger2.record_cycle(a1, s1, ctx_b).unwrap();
assert_ne!(entry_a.chain_hash, entry_b.chain_hash);
assert!(ledger.verify_chain().is_ok());
assert!(ledger2.verify_chain().is_ok());
}
#[test]
fn semantic_tamper_detection() {
let mut ledger = TemporalLedger::new();
let (a0, s0) = make_fingerprints(0);
let genesis_hash = *blake3::hash(b"genesis").as_bytes();
ledger
.record_genesis(a0, s0, genesis_hash, CycleContext::genesis())
.unwrap();
let (a1, s1) = make_fingerprints(1);
let ctx = CycleContext::command("user.create", "admin", "req-1", "authorized");
ledger.record_cycle(a1, s1, ctx).unwrap();
assert!(ledger.verify_chain().is_ok());
ledger.entries[1].semantic_hash[0] ^= 0xFF;
let result = ledger.verify_chain();
assert!(result.is_err());
match result.unwrap_err() {
SomaError::ChainIntegrityViolation { cycle_index } => {
assert_eq!(cycle_index, 1);
}
other => panic!("Expected ChainIntegrityViolation, got {other:?}"),
}
}
#[test]
fn mixed_legacy_and_enriched_chain() {
let mut ledger = TemporalLedger::new();
let (a0, s0) = make_fingerprints(0);
let genesis_hash = *blake3::hash(b"genesis").as_bytes();
ledger.entries.push(LedgerEntry {
cycle_index: 0,
awareness_fingerprint: a0,
system_fingerprint: s0,
chain_hash: genesis_hash,
semantic_hash: LEGACY_SEMANTIC_HASH,
context: None,
});
let (a1, s1) = make_fingerprints(1);
let legacy_hash_1 = TemporalLedger::compute_legacy_chain_hash(&s1, &genesis_hash);
ledger.entries.push(LedgerEntry {
cycle_index: 1,
awareness_fingerprint: a1,
system_fingerprint: s1,
chain_hash: legacy_hash_1,
semantic_hash: LEGACY_SEMANTIC_HASH,
context: None,
});
let (a2, s2) = make_fingerprints(2);
let legacy_hash_2 = TemporalLedger::compute_legacy_chain_hash(&s2, &legacy_hash_1);
ledger.entries.push(LedgerEntry {
cycle_index: 2,
awareness_fingerprint: a2,
system_fingerprint: s2,
chain_hash: legacy_hash_2,
semantic_hash: LEGACY_SEMANTIC_HASH,
context: None,
});
let (a3, s3) = make_fingerprints(3);
ledger
.record_cycle(a3, s3, CycleContext::heartbeat())
.unwrap();
let (a4, s4) = make_fingerprints(4);
ledger
.record_cycle(
a4,
s4,
CycleContext::command("user.create", "admin", "req-1", "ok"),
)
.unwrap();
assert_eq!(ledger.len(), 5);
assert!(ledger.verify_chain().is_ok());
}
#[test]
fn cycle_context_carried_in_entry() {
let mut ledger = TemporalLedger::new();
let (a0, s0) = make_fingerprints(0);
let genesis_hash = *blake3::hash(b"genesis").as_bytes();
ledger
.record_genesis(a0, s0, genesis_hash, CycleContext::genesis())
.unwrap();
let (a1, s1) = make_fingerprints(1);
let ctx = CycleContext::command("user.create", "admin", "req-1", "authorized")
.with_detail("username=bob");
ledger.record_cycle(a1, s1, ctx.clone()).unwrap();
let entry = ledger.entry(1).unwrap();
assert_eq!(entry.context.as_ref(), Some(&ctx));
assert_eq!(entry.semantic_hash, ctx.semantic_hash());
}
#[test]
fn all_cycle_classes_in_chain() {
let mut ledger = TemporalLedger::new();
let (a0, s0) = make_fingerprints(0);
let genesis_hash = *blake3::hash(b"genesis").as_bytes();
ledger
.record_genesis(a0, s0, genesis_hash, CycleContext::genesis())
.unwrap();
let contexts = vec![
CycleContext::heartbeat(),
CycleContext::login("alice", "success"),
CycleContext::command("user.create", "admin", "req-1", "authorized"),
CycleContext::gate_transition("operator", "req-2", "authorized", "gate=G1 evidence=3"),
CycleContext::heartbeat(),
];
for (i, ctx) in contexts.into_iter().enumerate() {
let (a, s) = make_fingerprints((i + 1) as u64);
ledger.record_cycle(a, s, ctx).unwrap();
}
assert_eq!(ledger.len(), 6);
assert!(ledger.verify_chain().is_ok());
}
}