use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use uuid::Uuid;
pub const GENESIS_HASH: [u8; 32] = [0u8; 32];
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WitnessOperation {
Store,
Update,
Delete,
}
impl std::fmt::Display for WitnessOperation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WitnessOperation::Store => write!(f, "store"),
WitnessOperation::Update => write!(f, "update"),
WitnessOperation::Delete => write!(f, "delete"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WitnessSegment {
pub segment_id: Uuid,
pub timestamp: DateTime<Utc>,
pub operation: WitnessOperation,
pub data_hash: [u8; 32],
pub previous_hash: [u8; 32],
pub segment_hash: [u8; 32],
}
impl WitnessSegment {
pub fn compute_hash(
segment_id: &Uuid,
timestamp: &DateTime<Utc>,
operation: WitnessOperation,
data_hash: &[u8; 32],
previous_hash: &[u8; 32],
) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(segment_id.as_bytes());
hasher.update(timestamp.to_rfc3339().as_bytes());
hasher.update(operation.to_string().as_bytes());
hasher.update(data_hash);
hasher.update(previous_hash);
let result = hasher.finalize();
let mut hash = [0u8; 32];
hash.copy_from_slice(&result);
hash
}
pub fn verify_self(&self) -> bool {
let expected = Self::compute_hash(
&self.segment_id,
&self.timestamp,
self.operation,
&self.data_hash,
&self.previous_hash,
);
self.segment_hash == expected
}
}
#[non_exhaustive]
#[derive(Debug)]
pub enum WitnessError {
ChainCorrupted {
index: usize,
segment_id: Uuid,
},
LinkBroken {
index: usize,
segment_id: Uuid,
},
Serde(serde_json::Error),
Io(std::io::Error),
}
impl std::fmt::Display for WitnessError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WitnessError::ChainCorrupted { index, segment_id } => {
write!(
f,
"WITNESS chain corrupted at index {index} \
(segment {segment_id})"
)
}
WitnessError::LinkBroken { index, segment_id } => {
write!(
f,
"WITNESS chain link broken at index {index} \
(segment {segment_id})"
)
}
WitnessError::Serde(e) => write!(f, "WITNESS serde error: {e}"),
WitnessError::Io(e) => write!(f, "WITNESS I/O error: {e}"),
}
}
}
impl std::error::Error for WitnessError {}
impl From<serde_json::Error> for WitnessError {
fn from(e: serde_json::Error) -> Self {
WitnessError::Serde(e)
}
}
impl From<std::io::Error> for WitnessError {
fn from(e: std::io::Error) -> Self {
WitnessError::Io(e)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WitnessChain {
segments: Vec<WitnessSegment>,
}
impl WitnessChain {
pub fn new() -> Self {
Self {
segments: Vec::new(),
}
}
pub fn append(&mut self, operation: WitnessOperation, data: &[u8]) {
let previous_hash = self
.segments
.last()
.map(|s| s.segment_hash)
.unwrap_or(GENESIS_HASH);
let segment_id = Uuid::new_v4();
let timestamp = Utc::now();
let mut data_hasher = Sha256::new();
data_hasher.update(data);
let data_result = data_hasher.finalize();
let mut data_hash = [0u8; 32];
data_hash.copy_from_slice(&data_result);
let segment_hash = WitnessSegment::compute_hash(
&segment_id,
×tamp,
operation,
&data_hash,
&previous_hash,
);
self.segments.push(WitnessSegment {
segment_id,
timestamp,
operation,
data_hash,
previous_hash,
segment_hash,
});
}
pub fn verify(&self) -> bool {
self.verify_detailed().is_ok()
}
pub fn verify_detailed(&self) -> Result<(), WitnessError> {
let mut expected_previous = GENESIS_HASH;
for (i, segment) in self.segments.iter().enumerate() {
if segment.previous_hash != expected_previous {
return Err(WitnessError::LinkBroken {
index: i,
segment_id: segment.segment_id,
});
}
if !segment.verify_self() {
return Err(WitnessError::ChainCorrupted {
index: i,
segment_id: segment.segment_id,
});
}
expected_previous = segment.segment_hash;
}
Ok(())
}
pub fn len(&self) -> usize {
self.segments.len()
}
pub fn is_empty(&self) -> bool {
self.segments.is_empty()
}
pub fn segments(&self) -> &[WitnessSegment] {
&self.segments
}
pub fn segments_mut(&mut self) -> &mut Vec<WitnessSegment> {
&mut self.segments
}
pub fn tip_hash(&self) -> [u8; 32] {
self.segments
.last()
.map(|s| s.segment_hash)
.unwrap_or(GENESIS_HASH)
}
pub fn to_json(&self) -> Result<String, WitnessError> {
Ok(serde_json::to_string_pretty(self)?)
}
pub fn from_json(json: &str) -> Result<Self, WitnessError> {
Ok(serde_json::from_str(json)?)
}
pub fn save(&self, path: &std::path::Path) -> Result<(), WitnessError> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let json = self.to_json()?;
std::fs::write(path, json)?;
Ok(())
}
pub fn load(path: &std::path::Path) -> Result<Self, WitnessError> {
if !path.exists() {
return Ok(Self::new());
}
let json = std::fs::read_to_string(path)?;
Self::from_json(&json)
}
pub fn load_verified(
path: &std::path::Path,
) -> Result<Self, WitnessError> {
let chain = Self::load(path)?;
chain.verify_detailed()?;
Ok(chain)
}
}
impl Default for WitnessChain {
fn default() -> Self {
Self::new()
}
}
pub fn sha256_hash(data: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(data);
let result = hasher.finalize();
let mut hash = [0u8; 32];
hash.copy_from_slice(&result);
hash
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU64, Ordering};
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
fn temp_path(label: &str) -> std::path::PathBuf {
let n = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
std::env::temp_dir().join(format!(
"clawft_witness_test_{label}_{pid}_{n}.json"
))
}
#[test]
fn empty_chain_is_valid() {
let chain = WitnessChain::new();
assert!(chain.verify());
assert!(chain.is_empty());
assert_eq!(chain.len(), 0);
assert_eq!(chain.tip_hash(), GENESIS_HASH);
}
#[test]
fn single_segment_chain_valid() {
let mut chain = WitnessChain::new();
chain.append(WitnessOperation::Store, b"hello world");
assert!(chain.verify());
assert_eq!(chain.len(), 1);
let seg = &chain.segments()[0];
assert_eq!(seg.operation, WitnessOperation::Store);
assert_eq!(seg.previous_hash, GENESIS_HASH);
}
#[test]
fn multi_segment_chain_valid() {
let mut chain = WitnessChain::new();
chain.append(WitnessOperation::Store, b"entry 1");
chain.append(WitnessOperation::Update, b"entry 1 v2");
chain.append(WitnessOperation::Store, b"entry 2");
chain.append(WitnessOperation::Delete, b"entry 1");
assert!(chain.verify());
assert_eq!(chain.len(), 4);
}
#[test]
fn link_continuity() {
let mut chain = WitnessChain::new();
chain.append(WitnessOperation::Store, b"a");
chain.append(WitnessOperation::Store, b"b");
let s0 = &chain.segments()[0];
let s1 = &chain.segments()[1];
assert_eq!(s1.previous_hash, s0.segment_hash);
}
#[test]
fn tampering_detected_modified_data_hash() {
let mut chain = WitnessChain::new();
chain.append(WitnessOperation::Store, b"original");
chain.append(WitnessOperation::Store, b"second");
chain.segments[0].data_hash[0] ^= 0xFF;
assert!(!chain.verify());
let err = chain.verify_detailed().unwrap_err();
match err {
WitnessError::ChainCorrupted { index, .. } => {
assert_eq!(index, 0)
}
_ => panic!("expected ChainCorrupted error"),
}
}
#[test]
fn tampering_detected_modified_segment_hash() {
let mut chain = WitnessChain::new();
chain.append(WitnessOperation::Store, b"a");
chain.append(WitnessOperation::Store, b"b");
chain.segments[0].segment_hash[0] ^= 0xFF;
assert!(!chain.verify());
}
#[test]
fn tampering_detected_broken_link() {
let mut chain = WitnessChain::new();
chain.append(WitnessOperation::Store, b"a");
chain.append(WitnessOperation::Store, b"b");
chain.segments[1].previous_hash[0] ^= 0xFF;
assert!(!chain.verify());
let err = chain.verify_detailed().unwrap_err();
match err {
WitnessError::LinkBroken { index, .. } => {
assert_eq!(index, 1)
}
_ => panic!("expected LinkBroken error"),
}
}
#[test]
fn json_roundtrip() {
let mut chain = WitnessChain::new();
chain.append(WitnessOperation::Store, b"test data");
chain.append(WitnessOperation::Update, b"test data v2");
let json = chain.to_json().unwrap();
let loaded = WitnessChain::from_json(&json).unwrap();
assert!(loaded.verify());
assert_eq!(loaded.len(), 2);
assert_eq!(loaded.segments()[0].operation, WitnessOperation::Store);
assert_eq!(loaded.segments()[1].operation, WitnessOperation::Update);
}
#[test]
fn save_and_load_roundtrip() {
let path = temp_path("save_load");
let _ = std::fs::remove_file(&path);
let mut chain = WitnessChain::new();
chain.append(WitnessOperation::Store, b"persistent data");
chain.save(&path).unwrap();
let loaded = WitnessChain::load(&path).unwrap();
assert!(loaded.verify());
assert_eq!(loaded.len(), 1);
let _ = std::fs::remove_file(&path);
}
#[test]
fn load_nonexistent_returns_empty() {
let path = temp_path("nonexist");
let _ = std::fs::remove_file(&path);
let chain = WitnessChain::load(&path).unwrap();
assert!(chain.is_empty());
}
#[test]
fn load_verified_rejects_tampered() {
let path = temp_path("verified");
let _ = std::fs::remove_file(&path);
let mut chain = WitnessChain::new();
chain.append(WitnessOperation::Store, b"data");
chain.save(&path).unwrap();
let mut loaded = WitnessChain::load(&path).unwrap();
loaded.segments[0].data_hash[0] ^= 0xFF;
let tampered_json = loaded.to_json().unwrap();
std::fs::write(&path, tampered_json).unwrap();
let result = WitnessChain::load_verified(&path);
assert!(result.is_err());
let _ = std::fs::remove_file(&path);
}
#[test]
fn tip_hash_updates_on_append() {
let mut chain = WitnessChain::new();
assert_eq!(chain.tip_hash(), GENESIS_HASH);
chain.append(WitnessOperation::Store, b"a");
let tip1 = chain.tip_hash();
assert_ne!(tip1, GENESIS_HASH);
chain.append(WitnessOperation::Store, b"b");
let tip2 = chain.tip_hash();
assert_ne!(tip2, tip1);
}
#[test]
fn sha256_hash_deterministic() {
let h1 = sha256_hash(b"test data");
let h2 = sha256_hash(b"test data");
assert_eq!(h1, h2);
let h3 = sha256_hash(b"different data");
assert_ne!(h1, h3);
}
#[test]
fn operation_display() {
assert_eq!(format!("{}", WitnessOperation::Store), "store");
assert_eq!(format!("{}", WitnessOperation::Update), "update");
assert_eq!(format!("{}", WitnessOperation::Delete), "delete");
}
#[test]
fn witness_error_display() {
let err = WitnessError::ChainCorrupted {
index: 3,
segment_id: Uuid::nil(),
};
let msg = format!("{err}");
assert!(msg.contains("corrupted"));
assert!(msg.contains("3"));
let err = WitnessError::LinkBroken {
index: 5,
segment_id: Uuid::nil(),
};
let msg = format!("{err}");
assert!(msg.contains("broken"));
assert!(msg.contains("5"));
}
#[test]
fn segment_verify_self_valid() {
let mut chain = WitnessChain::new();
chain.append(WitnessOperation::Store, b"test");
assert!(chain.segments()[0].verify_self());
}
#[test]
fn segment_verify_self_invalid() {
let mut chain = WitnessChain::new();
chain.append(WitnessOperation::Store, b"test");
let mut seg = chain.segments()[0].clone();
seg.data_hash[0] ^= 0xFF;
assert!(!seg.verify_self());
}
#[test]
fn default_creates_empty() {
let chain = WitnessChain::default();
assert!(chain.is_empty());
}
}