use sha2::{Digest, Sha256};
#[derive(Debug, Clone)]
pub struct ChainRecord {
pub seq: u64,
pub prev_hash: String,
pub hash: String,
pub payload: String,
}
pub const GENESIS: &str = "GENESIS";
fn hash_hex(input: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
let bytes = hasher.finalize();
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push_str(&format!("{b:02x}"));
}
out
}
pub fn append(prev: Option<&ChainRecord>, payload: &str) -> ChainRecord {
let (seq, prev_hash) = match prev {
Some(r) => (r.seq + 1, r.hash.clone()),
None => (0, GENESIS.to_string()),
};
let hash = hash_hex(&format!("{prev_hash}:{seq}:{payload}"));
ChainRecord {
seq,
prev_hash,
hash,
payload: payload.to_string(),
}
}
pub fn verify_chain(records: &[ChainRecord]) -> Result<(), String> {
let mut expected_prev = GENESIS.to_string();
for (i, r) in records.iter().enumerate() {
if r.seq != i as u64 {
return Err(format!(
"audit_chain: seq gap at index {i}: expected {}, got {}",
i, r.seq
));
}
if r.prev_hash != expected_prev {
return Err(format!("audit_chain: prev_hash mismatch at seq {}", r.seq));
}
let recomputed = hash_hex(&format!("{}:{}:{}", r.prev_hash, r.seq, r.payload));
if recomputed != r.hash {
return Err(format!(
"audit_chain: content hash mismatch at seq {}",
r.seq
));
}
expected_prev = r.hash.clone();
}
Ok(())
}
#[cfg(test)]
mod chain_tests {
use super::*;
fn build(n: u64) -> Vec<ChainRecord> {
let mut out = Vec::new();
for i in 0..n {
let prev = out.last();
out.push(append(prev, &format!("event-{i}")));
}
out
}
#[test]
fn round_trip_verifies() {
let chain = build(5);
assert!(verify_chain(&chain).is_ok());
assert_eq!(chain[0].prev_hash, GENESIS);
assert_eq!(chain[4].seq, 4);
}
#[test]
fn tampered_payload_detected() {
let mut chain = build(3);
chain[1].payload = "forged".to_string();
assert!(verify_chain(&chain).is_err());
}
#[test]
fn reordered_records_detected() {
let mut chain = build(3);
chain.swap(0, 1);
assert!(verify_chain(&chain).is_err());
}
#[test]
fn truncated_tail_still_verifies_prefix() {
let chain = build(4);
assert!(verify_chain(&chain[..2]).is_ok());
}
#[test]
fn empty_chain_verifies() {
assert!(verify_chain(&[]).is_ok());
}
}