use crate::error::{Error, Result};
use serde::Serialize;
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
pub const GENESIS_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
#[must_use]
pub fn sha256_hex(bytes: &[u8]) -> String {
hex::encode(Sha256::digest(bytes))
}
pub fn canonical_json<T: Serialize>(value: &T) -> Result<String> {
let v = serde_json::to_value(value)?;
Ok(serde_json::to_string(&canonicalize(v))?)
}
fn canonicalize(v: Value) -> Value {
match v {
Value::Object(map) => {
let sorted: BTreeMap<String, Value> = map
.into_iter()
.map(|(k, val)| (k, canonicalize(val)))
.collect();
let mut out = serde_json::Map::new();
for (k, val) in sorted {
out.insert(k, val);
}
Value::Object(out)
}
Value::Array(arr) => Value::Array(arr.into_iter().map(canonicalize).collect()),
other => other,
}
}
pub fn record_hash<T: Serialize>(record: &T) -> Result<String> {
Ok(sha256_hex(canonical_json(record)?.as_bytes()))
}
pub trait Chained {
fn prev_hash(&self) -> &str;
}
pub fn verify_chain<T: Serialize + Chained>(records: &[T], genesis: &str) -> Result<()> {
let mut expected_prev = genesis.to_owned();
for (i, rec) in records.iter().enumerate() {
if rec.prev_hash() != expected_prev {
return Err(Error::ChainBroken {
index: i,
detail: format!(
"prev_hash link mismatch: expected {expected_prev}, found {}",
rec.prev_hash()
),
});
}
expected_prev = record_hash(rec)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Serialize;
use serde_json::json;
#[test]
fn canonical_json_sorts_keys_at_every_depth() {
let v = json!({ "b": 1, "a": { "d": 4, "c": 3 }, "arr": [ { "y": 2, "x": 1 } ] });
assert_eq!(
canonical_json(&v).unwrap(),
r#"{"a":{"c":3,"d":4},"arr":[{"x":1,"y":2}],"b":1}"#
);
}
#[test]
fn canonical_json_is_field_order_independent() {
let a = json!({ "x": 1, "y": 2 });
let b = json!({ "y": 2, "x": 1 });
assert_eq!(record_hash(&a).unwrap(), record_hash(&b).unwrap());
}
#[test]
fn record_hash_is_deterministic_and_sensitive() {
let a = json!({ "n": 1 });
assert_eq!(record_hash(&a).unwrap(), record_hash(&a).unwrap());
assert_ne!(
record_hash(&a).unwrap(),
record_hash(&json!({ "n": 2 })).unwrap()
);
}
#[derive(Serialize)]
struct Rec {
prev_hash: String,
n: u64,
}
impl Chained for Rec {
fn prev_hash(&self) -> &str {
&self.prev_hash
}
}
fn build_chain(payloads: &[u64]) -> Vec<Rec> {
let mut prev = GENESIS_HASH.to_owned();
let mut out = Vec::new();
for &n in payloads {
let rec = Rec {
prev_hash: prev.clone(),
n,
};
prev = record_hash(&rec).unwrap();
out.push(rec);
}
out
}
#[test]
fn valid_chain_verifies() {
let chain = build_chain(&[10, 20, 30]);
assert!(verify_chain(&chain, GENESIS_HASH).is_ok());
}
#[test]
fn tampering_payload_breaks_the_next_link() {
let mut chain = build_chain(&[10, 20, 30]);
chain[1].n = 999; match verify_chain(&chain, GENESIS_HASH) {
Err(Error::ChainBroken { index, .. }) => assert_eq!(index, 2),
other => panic!("expected ChainBroken at 2, got {other:?}"),
}
}
#[test]
fn relinking_a_prev_hash_is_detected() {
let mut chain = build_chain(&[10, 20, 30]);
chain[1].prev_hash = GENESIS_HASH.to_owned(); match verify_chain(&chain, GENESIS_HASH) {
Err(Error::ChainBroken { index, .. }) => assert_eq!(index, 1),
other => panic!("expected ChainBroken at 1, got {other:?}"),
}
}
}