kaizen/interchange/
hash_chain.rs1use serde::{Deserialize, Serialize};
5use std::error::Error;
6use std::fmt::{Display, Formatter};
7
8const HASH_PREFIX: &str = "blake3:";
9const GENESIS: &str = "genesis";
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct HashChainEvent {
13 pub event_id: String,
14 pub canonical_json: Vec<u8>,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct HashChainLink {
19 pub event_id: String,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub prev_hash: Option<String>,
22 pub event_hash: String,
23 pub chain_hash: String,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum HashChainError {
28 Serialize(String),
29 LengthMismatch {
30 expected: usize,
31 actual: usize,
32 },
33 LinkMismatch {
34 index: usize,
35 expected: String,
36 actual: String,
37 },
38}
39
40impl HashChainEvent {
41 pub fn from_json<T: Serialize>(event_id: String, event: &T) -> Result<Self, HashChainError> {
42 Ok(Self {
43 event_id,
44 canonical_json: serde_json::to_vec(event)
45 .map_err(|e| HashChainError::Serialize(e.to_string()))?,
46 })
47 }
48}
49
50pub fn compute_hash_chain(events: &[HashChainEvent]) -> Vec<HashChainLink> {
51 events
52 .iter()
53 .scan(None, |prev, event| {
54 let link = link_for(prev.clone(), event);
55 *prev = Some(link.chain_hash.clone());
56 Some(link)
57 })
58 .collect()
59}
60
61pub fn verify_hash_chain(
62 events: &[HashChainEvent],
63 links: &[HashChainLink],
64) -> Result<(), HashChainError> {
65 if events.len() != links.len() {
66 return Err(HashChainError::LengthMismatch {
67 expected: events.len(),
68 actual: links.len(),
69 });
70 }
71 first_mismatch(&compute_hash_chain(events), links).map_or(Ok(()), Err)
72}
73
74fn first_mismatch(expected: &[HashChainLink], actual: &[HashChainLink]) -> Option<HashChainError> {
75 expected
76 .iter()
77 .zip(actual)
78 .enumerate()
79 .find_map(|(index, (a, b))| {
80 (a != b).then(|| HashChainError::LinkMismatch {
81 index,
82 expected: a.chain_hash.clone(),
83 actual: b.chain_hash.clone(),
84 })
85 })
86}
87
88fn link_for(prev_hash: Option<String>, event: &HashChainEvent) -> HashChainLink {
89 let event_hash = hash_bytes(&event.canonical_json);
90 HashChainLink {
91 event_id: event.event_id.clone(),
92 chain_hash: chain_hash(prev_hash.as_deref(), &event_hash),
93 event_hash,
94 prev_hash,
95 }
96}
97
98fn hash_bytes(bytes: &[u8]) -> String {
99 let digest = blake3::hash(bytes);
100 format!("{HASH_PREFIX}{}", hex::encode(digest.as_bytes()))
101}
102
103fn chain_hash(prev_hash: Option<&str>, event_hash: &str) -> String {
104 let mut hasher = blake3::Hasher::new();
105 hasher.update(prev_hash.unwrap_or(GENESIS).as_bytes());
106 hasher.update(b"\n");
107 hasher.update(event_hash.as_bytes());
108 format!("{HASH_PREFIX}{}", hex::encode(hasher.finalize().as_bytes()))
109}
110
111impl Display for HashChainError {
112 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
113 match self {
114 Self::Serialize(err) => write!(f, "hash-chain serialize error: {err}"),
115 Self::LengthMismatch { expected, actual } => {
116 write!(
117 f,
118 "hash-chain length mismatch: expected {expected}, got {actual}"
119 )
120 }
121 Self::LinkMismatch { index, .. } => write!(f, "hash-chain link mismatch at {index}"),
122 }
123 }
124}
125
126impl Error for HashChainError {}