use sha2::{Digest, Sha256};
use crate::transcript::Level;
pub const SALT_LEN: usize = 16;
const LEAF_PREFIX: u8 = 0x00;
const NODE_PREFIX: u8 = 0x01;
const HEADER_PREFIX: u8 = 0x02;
#[must_use]
pub fn header_node(canonical_header: &str) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update([HEADER_PREFIX]);
hasher.update(canonical_header.as_bytes());
hasher.finalize().into()
}
#[must_use]
pub fn fact_leaf(salt: &[u8; SALT_LEN], canonical_fact: &str) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update([LEAF_PREFIX]);
hasher.update(salt);
hasher.update(canonical_fact.as_bytes());
hasher.finalize().into()
}
#[must_use]
pub fn chain_node(previous: &[u8; 32], at: &str, level: Level, leaf: &[u8; 32]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update([NODE_PREFIX]);
hasher.update(previous);
update_with_length(&mut hasher, at.as_bytes());
hasher.update(u64::from(level.value()).to_be_bytes());
hasher.update(leaf);
hasher.finalize().into()
}
fn update_with_length(hasher: &mut Sha256, bytes: &[u8]) {
let len = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
hasher.update(len.to_be_bytes());
hasher.update(bytes);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::canonical_json;
use serde_json::{Value, json};
fn hex(bytes: &[u8; 32]) -> String {
base16ct::lower::encode_string(bytes)
}
fn jcs(value: &Value) -> String {
canonical_json(value).expect("canonical")
}
#[test]
fn construction_vectors() {
let header = json!({
"rite_transcript": 1,
"vocabulary": 1,
"producer": "rite 0.6.0",
"run_id": "00000000000000000000000000000000",
"dry_run": false,
"levels": { "public": 10, "restricted": 20, "confidential": 30 },
});
let node_0 = header_node(&jcs(&header));
assert_eq!(
hex(&node_0),
"acd329c9b34b4e9443db1b47b4a5908b3f66affd630005935e5c5201c3f123ec"
);
let salt = [0x11; SALT_LEN];
let fact = json!({ "type": "act_started", "id": "setup", "label": "Setup" });
let leaf = fact_leaf(&salt, &jcs(&fact));
assert_eq!(
hex(&leaf),
"60d495f6e5117daeaa777bcac4b9aa5360c43aff76bdcf5461380864e0c053d0"
);
let node_1 = chain_node(&node_0, "2026-09-22T10:00:00.000000Z", Level::PUBLIC, &leaf);
assert_eq!(
hex(&node_1),
"e1942a1ab439cad63eb64ed52692d425d898c2c16b0fac24c08e5c04f71a1af5"
);
}
#[test]
fn every_committed_input_changes_the_node() {
let base = chain_node(&[0; 32], "t", Level::PUBLIC, &[1; 32]);
assert_ne!(base, chain_node(&[9; 32], "t", Level::PUBLIC, &[1; 32]));
assert_ne!(base, chain_node(&[0; 32], "u", Level::PUBLIC, &[1; 32]));
assert_ne!(base, chain_node(&[0; 32], "t", Level::RESTRICTED, &[1; 32]));
assert_ne!(base, chain_node(&[0; 32], "t", Level::PUBLIC, &[2; 32]));
}
#[test]
fn the_salt_and_the_fact_both_change_the_leaf() {
let fact = jcs(&json!({ "type": "x" }));
let leaf = fact_leaf(&[0; SALT_LEN], &fact);
assert_ne!(leaf, fact_leaf(&[1; SALT_LEN], &fact));
assert_ne!(
leaf,
fact_leaf(&[0; SALT_LEN], &jcs(&json!({ "type": "y" })))
);
}
#[test]
fn member_order_does_not_change_the_leaf() {
let a: Value = serde_json::from_str(r#"{"a":1,"b":2}"#).expect("parse");
let b: Value = serde_json::from_str(r#"{"b":2,"a":1}"#).expect("parse");
assert_eq!(
fact_leaf(&[0; SALT_LEN], &jcs(&a)),
fact_leaf(&[0; SALT_LEN], &jcs(&b))
);
}
}