use std::fmt::Debug;
use sha2::{Digest, Sha256};
pub const HASH_LENGTH: usize = 32;
#[allow(dead_code)]
const MAX_DATA_LENGTH: usize = 64 * 1024 * 1024;
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
pub struct ChainHash([u8; HASH_LENGTH]);
impl ChainHash {
pub fn as_bytes(&self) -> &[u8; HASH_LENGTH] {
&self.0
}
pub fn from_bytes(bytes: &[u8; HASH_LENGTH]) -> Self {
Self(*bytes)
}
}
impl From<[u8; HASH_LENGTH]> for ChainHash {
fn from(value: [u8; HASH_LENGTH]) -> Self {
Self(value)
}
}
impl From<ChainHash> for [u8; HASH_LENGTH] {
fn from(value: ChainHash) -> Self {
value.0
}
}
impl Debug for ChainHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"ChainHash({:016x}...)",
u64::from_le_bytes(
self.0[..8]
.try_into()
.expect("invariant: hash is HASH_LENGTH bytes, slice [..8] always fits [u8; 8]"),
)
)
}
}
pub fn chain_hash(prev: Option<&ChainHash>, data: &[u8]) -> ChainHash {
debug_assert!(
data.len() <= MAX_DATA_LENGTH,
"data exceeds {MAX_DATA_LENGTH} byte sanity limit"
);
let mut hasher = Sha256::new();
if let Some(prev) = prev {
hasher.update(prev.0);
}
hasher.update(data);
let hash_bytes: [u8; HASH_LENGTH] = hasher.finalize().into();
assert!(
hash_bytes.iter().any(|&b| b != 0),
"SHA-256 produced all-zero hash - cryptographic library bug"
);
kimberlite_properties::always!(
{
let mut verify_hasher = Sha256::new();
if let Some(p) = prev {
verify_hasher.update(p.0);
}
verify_hasher.update(data);
let verify_bytes: [u8; HASH_LENGTH] = verify_hasher.finalize().into();
verify_bytes == hash_bytes
},
"crypto.chain_hash_deterministic",
"chain_hash must be deterministic: same input produces same hash"
);
ChainHash(hash_bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chain_incorporates_prev_hash() {
let data = b"same data";
let hash1 = chain_hash(None, data);
let hash2 = chain_hash(Some(&hash1), data);
assert_ne!(hash1, hash2);
}
#[test]
fn test_chain_is_deterministic() {
let prev = ChainHash::from([42u8; 32]);
let data = b"test data";
let hash1 = chain_hash(Some(&prev), data);
let hash2 = chain_hash(Some(&prev), data);
assert_eq!(hash1, hash2);
}
#[test]
fn test_chain_replay() {
let r0 = chain_hash(None, b"genesis");
let r1 = chain_hash(Some(&r0), b"second");
let r2 = chain_hash(Some(&r1), b"third");
let replay0 = chain_hash(None, b"genesis");
let replay1 = chain_hash(Some(&replay0), b"second");
let replay2 = chain_hash(Some(&replay1), b"third");
assert_eq!(r0, replay0);
assert_eq!(r1, replay1);
assert_eq!(r2, replay2);
}
#[test]
fn test_chainhash_uses_sha256() {
let hash = chain_hash(None, b"test data");
let mut hasher = Sha256::new();
hasher.update(b"test data");
let expected = hasher.finalize();
assert_eq!(
hash.as_bytes(),
expected.as_slice(),
"ChainHash must use SHA-256 for FIPS compliance"
);
}
#[test]
fn test_chained_hash_uses_sha256() {
let prev = ChainHash::from([0x42; 32]);
let hash = chain_hash(Some(&prev), b"data");
let mut hasher = Sha256::new();
hasher.update([0x42; 32]);
hasher.update(b"data");
let expected = hasher.finalize();
assert_eq!(
hash.as_bytes(),
expected.as_slice(),
"Chained hashes must use SHA-256"
);
}
}