firstpass_core/
hashchain.rs1use crate::error::{Error, Result};
14use serde::Serialize;
15use serde_json::Value;
16use sha2::{Digest, Sha256};
17use std::collections::BTreeMap;
18
19pub const GENESIS_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
21
22#[must_use]
24pub fn sha256_hex(bytes: &[u8]) -> String {
25 hex::encode(Sha256::digest(bytes))
26}
27
28pub fn canonical_json<T: Serialize>(value: &T) -> Result<String> {
38 let v = serde_json::to_value(value)?;
39 Ok(serde_json::to_string(&canonicalize(v))?)
40}
41
42fn canonicalize(v: Value) -> Value {
46 match v {
47 Value::Object(map) => {
48 let sorted: BTreeMap<String, Value> = map
49 .into_iter()
50 .map(|(k, val)| (k, canonicalize(val)))
51 .collect();
52 let mut out = serde_json::Map::new();
53 for (k, val) in sorted {
54 out.insert(k, val);
55 }
56 Value::Object(out)
57 }
58 Value::Array(arr) => Value::Array(arr.into_iter().map(canonicalize).collect()),
59 other => other,
60 }
61}
62
63pub fn record_hash<T: Serialize>(record: &T) -> Result<String> {
68 Ok(sha256_hex(canonical_json(record)?.as_bytes()))
69}
70
71pub trait Chained {
73 fn prev_hash(&self) -> &str;
75}
76
77pub fn verify_chain<T: Serialize + Chained>(records: &[T], genesis: &str) -> Result<()> {
87 let mut expected_prev = genesis.to_owned();
88 for (i, rec) in records.iter().enumerate() {
89 if rec.prev_hash() != expected_prev {
90 return Err(Error::ChainBroken {
91 index: i,
92 detail: format!(
93 "prev_hash link mismatch: expected {expected_prev}, found {}",
94 rec.prev_hash()
95 ),
96 });
97 }
98 expected_prev = record_hash(rec)?;
99 }
100 Ok(())
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use serde::Serialize;
107 use serde_json::json;
108
109 #[test]
110 fn canonical_json_sorts_keys_at_every_depth() {
111 let v = json!({ "b": 1, "a": { "d": 4, "c": 3 }, "arr": [ { "y": 2, "x": 1 } ] });
112 assert_eq!(
113 canonical_json(&v).unwrap(),
114 r#"{"a":{"c":3,"d":4},"arr":[{"x":1,"y":2}],"b":1}"#
115 );
116 }
117
118 #[test]
119 fn canonical_json_is_field_order_independent() {
120 let a = json!({ "x": 1, "y": 2 });
122 let b = json!({ "y": 2, "x": 1 });
123 assert_eq!(record_hash(&a).unwrap(), record_hash(&b).unwrap());
124 }
125
126 #[test]
127 fn record_hash_is_deterministic_and_sensitive() {
128 let a = json!({ "n": 1 });
129 assert_eq!(record_hash(&a).unwrap(), record_hash(&a).unwrap());
130 assert_ne!(
131 record_hash(&a).unwrap(),
132 record_hash(&json!({ "n": 2 })).unwrap()
133 );
134 }
135
136 #[derive(Serialize)]
137 struct Rec {
138 prev_hash: String,
139 n: u64,
140 }
141 impl Chained for Rec {
142 fn prev_hash(&self) -> &str {
143 &self.prev_hash
144 }
145 }
146
147 fn build_chain(payloads: &[u64]) -> Vec<Rec> {
148 let mut prev = GENESIS_HASH.to_owned();
149 let mut out = Vec::new();
150 for &n in payloads {
151 let rec = Rec {
152 prev_hash: prev.clone(),
153 n,
154 };
155 prev = record_hash(&rec).unwrap();
156 out.push(rec);
157 }
158 out
159 }
160
161 #[test]
162 fn valid_chain_verifies() {
163 let chain = build_chain(&[10, 20, 30]);
164 assert!(verify_chain(&chain, GENESIS_HASH).is_ok());
165 }
166
167 #[test]
168 fn tampering_payload_breaks_the_next_link() {
169 let mut chain = build_chain(&[10, 20, 30]);
170 chain[1].n = 999; match verify_chain(&chain, GENESIS_HASH) {
172 Err(Error::ChainBroken { index, .. }) => assert_eq!(index, 2),
173 other => panic!("expected ChainBroken at 2, got {other:?}"),
174 }
175 }
176
177 #[test]
178 fn relinking_a_prev_hash_is_detected() {
179 let mut chain = build_chain(&[10, 20, 30]);
180 chain[1].prev_hash = GENESIS_HASH.to_owned(); match verify_chain(&chain, GENESIS_HASH) {
182 Err(Error::ChainBroken { index, .. }) => assert_eq!(index, 1),
183 other => panic!("expected ChainBroken at 1, got {other:?}"),
184 }
185 }
186}