Skip to main content

kaizen/interchange/
hash_chain.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Deterministic event hash-chain helpers.
3
4use 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 {}