use crate::crypto::{SigningKey, VerifyingKey};
use crate::{AionError, Result};
pub const LOG_LEAF_DOMAIN: &[u8] = b"AION_V2_LOG_LEAF_V1\0";
pub const LOG_NODE_DOMAIN: &[u8] = b"AION_V2_LOG_NODE_V1\0";
pub const LOG_STH_DOMAIN: &[u8] = b"AION_V2_LOG_STH_V1\0";
pub const LOG_EMPTY_DOMAIN: &[u8] = b"AION_V2_LOG_EMPTY_V1\0";
#[repr(u16)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogEntryKind {
VersionAttestation = 1,
ManifestSignature = 2,
KeyRotation = 3,
KeyRevocation = 4,
SlsaStatement = 5,
DsseEnvelope = 6,
}
impl LogEntryKind {
pub fn from_u16(value: u16) -> Result<Self> {
match value {
1 => Ok(Self::VersionAttestation),
2 => Ok(Self::ManifestSignature),
3 => Ok(Self::KeyRotation),
4 => Ok(Self::KeyRevocation),
5 => Ok(Self::SlsaStatement),
6 => Ok(Self::DsseEnvelope),
other => Err(AionError::InvalidFormat {
reason: format!("Unknown log entry kind: {other}"),
}),
}
}
}
#[derive(Debug, Clone)]
pub struct LogEntry {
pub kind: LogEntryKind,
pub seq: u64,
pub timestamp_version: u64,
pub prev_leaf_hash: [u8; 32],
pub payload_hash: [u8; 32],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InclusionProof {
pub leaf_index: u64,
pub tree_size: u64,
pub audit_path: Vec<[u8; 32]>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedTreeHead {
pub tree_size: u64,
pub root_hash: [u8; 32],
pub operator_signature: [u8; 64],
}
#[derive(Debug, Default)]
pub struct TransparencyLog {
entries: Vec<LogEntry>,
leaf_hashes: Vec<[u8; 32]>,
subtree_roots: Vec<Vec<[u8; 32]>>,
operator_master: Option<VerifyingKey>,
}
#[must_use]
pub fn leaf_hash(
kind: LogEntryKind,
seq: u64,
timestamp_version: u64,
prev_leaf_hash: &[u8; 32],
payload: &[u8],
) -> [u8; 32] {
let payload_digest = crate::crypto::hash(payload);
let canonical = canonical_leaf_bytes(
kind,
seq,
timestamp_version,
prev_leaf_hash,
&payload_digest,
);
let mut hasher = blake3::Hasher::new();
hasher.update(LOG_LEAF_DOMAIN);
hasher.update(&canonical);
*hasher.finalize().as_bytes()
}
fn canonical_leaf_bytes(
kind: LogEntryKind,
seq: u64,
timestamp_version: u64,
prev_leaf_hash: &[u8; 32],
payload_hash: &[u8; 32],
) -> Vec<u8> {
let mut buf = Vec::with_capacity(2 + 8 + 8 + 32 + 32);
buf.extend_from_slice(&(kind as u16).to_le_bytes());
buf.extend_from_slice(&seq.to_le_bytes());
buf.extend_from_slice(×tamp_version.to_le_bytes());
buf.extend_from_slice(prev_leaf_hash);
buf.extend_from_slice(payload_hash);
buf
}
fn node_hash(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(LOG_NODE_DOMAIN);
hasher.update(left);
hasher.update(right);
*hasher.finalize().as_bytes()
}
fn empty_root() -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(LOG_EMPTY_DOMAIN);
*hasher.finalize().as_bytes()
}
const fn split_point(n: usize) -> usize {
let mut k = 1usize;
while k.saturating_mul(2) < n {
k = k.saturating_mul(2);
}
k
}
fn compute_root_from_proof(
leaf: [u8; 32],
leaf_index: usize,
tree_size: usize,
proof: &[[u8; 32]],
) -> Result<[u8; 32]> {
if tree_size == 0 {
return Err(AionError::InvalidFormat {
reason: "tree_size == 0 in inclusion proof".to_string(),
});
}
if leaf_index >= tree_size {
return Err(AionError::InvalidFormat {
reason: "leaf_index >= tree_size".to_string(),
});
}
if tree_size == 1 {
if !proof.is_empty() {
return Err(AionError::InvalidFormat {
reason: "proof is longer than expected for tree_size=1".to_string(),
});
}
return Ok(leaf);
}
let k = split_point(tree_size);
if proof.is_empty() {
return Err(AionError::InvalidFormat {
reason: "proof is shorter than expected".to_string(),
});
}
let outer_sibling_index = proof.len().saturating_sub(1);
let outer_sibling =
*proof
.get(outer_sibling_index)
.ok_or_else(|| AionError::InvalidFormat {
reason: "proof index underflow".to_string(),
})?;
let inner_proof = proof.get(..outer_sibling_index).unwrap_or(&[]);
if leaf_index < k {
let left = compute_root_from_proof(leaf, leaf_index, k, inner_proof)?;
Ok(node_hash(&left, &outer_sibling))
} else {
let right_index = leaf_index.saturating_sub(k);
let right_size = tree_size.saturating_sub(k);
let right = compute_root_from_proof(leaf, right_index, right_size, inner_proof)?;
Ok(node_hash(&outer_sibling, &right))
}
}
pub fn verify_inclusion_proof(
leaf_hash: [u8; 32],
leaf_index: u64,
tree_size: u64,
proof: &[[u8; 32]],
expected_root: [u8; 32],
) -> Result<()> {
let leaf_index_usize = usize::try_from(leaf_index).map_err(|_| AionError::InvalidFormat {
reason: "leaf_index exceeds usize".to_string(),
})?;
let tree_size_usize = usize::try_from(tree_size).map_err(|_| AionError::InvalidFormat {
reason: "tree_size exceeds usize".to_string(),
})?;
let computed = compute_root_from_proof(leaf_hash, leaf_index_usize, tree_size_usize, proof)?;
if computed != expected_root {
return Err(AionError::InvalidFormat {
reason: "inclusion proof does not recompute to expected root".to_string(),
});
}
Ok(())
}
impl TransparencyLog {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn set_operator(&mut self, master_key: VerifyingKey) {
self.operator_master = Some(master_key);
}
#[must_use]
pub fn tree_size(&self) -> u64 {
self.entries.len() as u64
}
#[must_use]
pub fn root_hash(&self) -> [u8; 32] {
if self.leaf_hashes.is_empty() {
return empty_root();
}
self.cached_subtree_root(0, self.leaf_hashes.len())
}
#[must_use]
pub fn entry(&self, index: u64) -> Option<&LogEntry> {
let idx = usize::try_from(index).ok()?;
self.entries.get(idx)
}
#[must_use]
pub fn entries(&self) -> &[LogEntry] {
&self.entries
}
#[must_use]
pub fn leaf_hash_at(&self, index: u64) -> Option<[u8; 32]> {
let idx = usize::try_from(index).ok()?;
self.leaf_hashes.get(idx).copied()
}
pub fn append(
&mut self,
kind: LogEntryKind,
payload: &[u8],
timestamp_version: u64,
) -> Result<u64> {
let seq = self.entries.len() as u64;
let prev_leaf_hash = self.leaf_hashes.last().copied().unwrap_or([0u8; 32]);
let hash = leaf_hash(kind, seq, timestamp_version, &prev_leaf_hash, payload);
let payload_digest = crate::crypto::hash(payload);
let entry = LogEntry {
kind,
seq,
timestamp_version,
prev_leaf_hash,
payload_hash: payload_digest,
};
self.entries.push(entry);
self.leaf_hashes.push(hash);
self.cascade_subtree_roots(hash);
Ok(seq)
}
#[allow(clippy::arithmetic_side_effects)] #[allow(clippy::indexing_slicing)] fn cascade_subtree_roots(&mut self, leaf: [u8; 32]) {
if self.subtree_roots.is_empty() {
self.subtree_roots.push(Vec::new());
}
self.subtree_roots[0].push(leaf);
let mut level: usize = 0;
loop {
let lower_len = self.subtree_roots[level].len();
if lower_len % 2 != 0 {
break;
}
let right_idx = lower_len - 1;
let left_idx = lower_len - 2;
let combined = node_hash(
&self.subtree_roots[level][left_idx],
&self.subtree_roots[level][right_idx],
);
level += 1;
if self.subtree_roots.len() <= level {
self.subtree_roots.push(Vec::new());
}
self.subtree_roots[level].push(combined);
}
}
#[allow(clippy::arithmetic_side_effects)] #[allow(clippy::indexing_slicing)] fn cached_subtree_root(&self, start: usize, len: usize) -> [u8; 32] {
debug_assert!(start + len <= self.leaf_hashes.len());
if len == 0 {
return empty_root();
}
if len == 1 {
return self.leaf_hashes[start];
}
if len.is_power_of_two() && start % len == 0 {
let level = len.trailing_zeros() as usize;
let j = start / len;
if let Some(level_vec) = self.subtree_roots.get(level) {
if let Some(h) = level_vec.get(j) {
return *h;
}
}
}
let k = split_point(len);
let left = self.cached_subtree_root(start, k);
let right = self.cached_subtree_root(start + k, len - k);
node_hash(&left, &right)
}
#[allow(clippy::arithmetic_side_effects)] fn cached_audit_path(&self, m: usize, range_start: usize, range_len: usize) -> Vec<[u8; 32]> {
if range_len <= 1 {
return Vec::new();
}
let k = split_point(range_len);
if m < range_start + k {
let mut path = self.cached_audit_path(m, range_start, k);
let sib = self.cached_subtree_root(range_start + k, range_len - k);
path.push(sib);
path
} else {
let mut path = self.cached_audit_path(m, range_start + k, range_len - k);
let sib = self.cached_subtree_root(range_start, k);
path.push(sib);
path
}
}
pub fn inclusion_proof(&self, leaf_index: u64) -> Result<InclusionProof> {
let idx = usize::try_from(leaf_index).map_err(|_| AionError::InvalidFormat {
reason: "leaf_index exceeds usize".to_string(),
})?;
if idx >= self.leaf_hashes.len() {
return Err(AionError::InvalidFormat {
reason: format!(
"leaf_index {idx} out of range (tree_size {})",
self.leaf_hashes.len()
),
});
}
let path = self.cached_audit_path(idx, 0, self.leaf_hashes.len());
Ok(InclusionProof {
leaf_index,
tree_size: self.tree_size(),
audit_path: path,
})
}
#[must_use]
pub fn canonical_tree_head(&self) -> Vec<u8> {
canonical_sth_bytes(self.tree_size(), &self.root_hash())
}
#[must_use]
pub fn sign_tree_head(&self, operator_key: &SigningKey) -> SignedTreeHead {
let tree_size = self.tree_size();
let root_hash = self.root_hash();
let message = canonical_sth_bytes(tree_size, &root_hash);
let operator_signature = operator_key.sign(&message);
SignedTreeHead {
tree_size,
root_hash,
operator_signature,
}
}
pub fn verify_tree_head(&self, sth: &SignedTreeHead) -> Result<()> {
let master = self
.operator_master
.as_ref()
.ok_or_else(|| AionError::InvalidFormat {
reason: "no operator master key registered".to_string(),
})?;
let message = canonical_sth_bytes(sth.tree_size, &sth.root_hash);
master.verify(&message, &sth.operator_signature)?;
if sth.tree_size != self.tree_size() || sth.root_hash != self.root_hash() {
return Err(AionError::InvalidFormat {
reason: "STH does not match current log state".to_string(),
});
}
Ok(())
}
}
fn canonical_sth_bytes(tree_size: u64, root_hash: &[u8; 32]) -> Vec<u8> {
let capacity = LOG_STH_DOMAIN.len().saturating_add(8).saturating_add(32);
let mut buf = Vec::with_capacity(capacity);
buf.extend_from_slice(LOG_STH_DOMAIN);
buf.extend_from_slice(&tree_size.to_le_bytes());
buf.extend_from_slice(root_hash);
buf
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::arithmetic_side_effects
)]
mod tests {
use super::*;
#[test]
fn empty_log_has_empty_root_sentinel() {
let log = TransparencyLog::new();
assert_eq!(log.tree_size(), 0);
assert_eq!(log.root_hash(), empty_root());
}
#[test]
fn append_increments_tree_size() {
let mut log = TransparencyLog::new();
log.append(LogEntryKind::VersionAttestation, b"a", 1)
.unwrap();
log.append(LogEntryKind::ManifestSignature, b"b", 2)
.unwrap();
log.append(LogEntryKind::KeyRotation, b"c", 3).unwrap();
assert_eq!(log.tree_size(), 3);
assert_eq!(log.entries().len(), 3);
}
#[test]
fn leaf_chain_links_prev_hashes() {
let mut log = TransparencyLog::new();
log.append(LogEntryKind::VersionAttestation, b"a", 1)
.unwrap();
log.append(LogEntryKind::ManifestSignature, b"b", 2)
.unwrap();
let e0 = log.entry(0).unwrap();
let e1 = log.entry(1).unwrap();
let expected_prev = leaf_hash(
e0.kind,
e0.seq,
e0.timestamp_version,
&e0.prev_leaf_hash,
b"a",
);
assert_eq!(e1.prev_leaf_hash, expected_prev);
assert_eq!(e0.prev_leaf_hash, [0u8; 32]);
}
#[test]
fn inclusion_proof_verifies_for_every_leaf() {
let mut log = TransparencyLog::new();
let payloads: Vec<&[u8]> = vec![b"one", b"two", b"three", b"four", b"five"];
let kinds = [
LogEntryKind::VersionAttestation,
LogEntryKind::ManifestSignature,
LogEntryKind::KeyRotation,
LogEntryKind::SlsaStatement,
LogEntryKind::DsseEnvelope,
];
for (i, p) in payloads.iter().enumerate() {
log.append(kinds[i], p, (i as u64) + 1).unwrap();
}
let root = log.root_hash();
for (i, p) in payloads.iter().enumerate() {
let entry = log.entry(i as u64).unwrap();
let proof = log.inclusion_proof(i as u64).unwrap();
let leaf = leaf_hash(
kinds[i],
entry.seq,
entry.timestamp_version,
&entry.prev_leaf_hash,
p,
);
verify_inclusion_proof(
leaf,
proof.leaf_index,
proof.tree_size,
&proof.audit_path,
root,
)
.unwrap();
}
}
#[test]
fn inclusion_proof_rejects_out_of_range_index() {
let mut log = TransparencyLog::new();
log.append(LogEntryKind::VersionAttestation, b"a", 1)
.unwrap();
assert!(log.inclusion_proof(5).is_err());
}
#[test]
fn sth_round_trip_verifies() {
let mut log = TransparencyLog::new();
let operator = SigningKey::generate();
log.set_operator(operator.verifying_key());
log.append(LogEntryKind::VersionAttestation, b"x", 1)
.unwrap();
let sth = log.sign_tree_head(&operator);
assert!(log.verify_tree_head(&sth).is_ok());
}
#[test]
fn sth_with_tampered_root_rejects() {
let mut log = TransparencyLog::new();
let operator = SigningKey::generate();
log.set_operator(operator.verifying_key());
log.append(LogEntryKind::VersionAttestation, b"x", 1)
.unwrap();
let mut sth = log.sign_tree_head(&operator);
sth.root_hash[0] ^= 0x01;
assert!(log.verify_tree_head(&sth).is_err());
}
#[test]
fn sth_without_operator_rejects() {
let mut log = TransparencyLog::new();
log.append(LogEntryKind::VersionAttestation, b"x", 1)
.unwrap();
let operator = SigningKey::generate();
let sth = log.sign_tree_head(&operator);
assert!(log.verify_tree_head(&sth).is_err());
}
#[test]
fn kind_round_trips() {
for kind in [
LogEntryKind::VersionAttestation,
LogEntryKind::ManifestSignature,
LogEntryKind::KeyRotation,
LogEntryKind::KeyRevocation,
LogEntryKind::SlsaStatement,
LogEntryKind::DsseEnvelope,
] {
let raw = kind as u16;
assert_eq!(LogEntryKind::from_u16(raw).unwrap(), kind);
}
assert!(LogEntryKind::from_u16(999).is_err());
}
mod properties {
use super::*;
use hegel::generators as gs;
fn draw_payloads(tc: &hegel::TestCase) -> Vec<Vec<u8>> {
let n = tc.draw(gs::integers::<usize>().min_value(1).max_value(256));
let mut out: Vec<Vec<u8>> = Vec::with_capacity(n);
for _ in 0..n {
out.push(tc.draw(gs::binary().max_size(64)));
}
out
}
fn build_log(payloads: &[Vec<u8>]) -> TransparencyLog {
let mut log = TransparencyLog::new();
for (i, p) in payloads.iter().enumerate() {
log.append(LogEntryKind::DsseEnvelope, p, (i as u64) + 1)
.unwrap_or_else(|_| std::process::abort());
}
log
}
#[hegel::test]
fn prop_tree_size_matches_entries(tc: hegel::TestCase) {
let payloads = draw_payloads(&tc);
let log = build_log(&payloads);
assert_eq!(log.tree_size() as usize, payloads.len());
assert_eq!(log.entries().len(), payloads.len());
}
#[hegel::test]
fn prop_inclusion_proof_roundtrip_for_any_n(tc: hegel::TestCase) {
let payloads = draw_payloads(&tc);
let log = build_log(&payloads);
let root = log.root_hash();
for (i, p) in payloads.iter().enumerate() {
let entry = log.entry(i as u64).unwrap_or_else(|| std::process::abort());
let proof = log
.inclusion_proof(i as u64)
.unwrap_or_else(|_| std::process::abort());
let leaf = leaf_hash(
entry.kind,
entry.seq,
entry.timestamp_version,
&entry.prev_leaf_hash,
p,
);
verify_inclusion_proof(
leaf,
proof.leaf_index,
proof.tree_size,
&proof.audit_path,
root,
)
.unwrap_or_else(|_| std::process::abort());
}
}
#[hegel::test]
fn prop_tampered_payload_rejects(tc: hegel::TestCase) {
let payloads = draw_payloads(&tc);
let log = build_log(&payloads);
let root = log.root_hash();
let idx = tc.draw(gs::integers::<usize>().max_value(payloads.len().saturating_sub(1)));
let entry = log
.entry(idx as u64)
.unwrap_or_else(|| std::process::abort());
let original = payloads
.get(idx)
.unwrap_or_else(|| std::process::abort())
.clone();
let mut tampered = original;
tampered.push(0xFF);
let proof = log
.inclusion_proof(idx as u64)
.unwrap_or_else(|_| std::process::abort());
let leaf = leaf_hash(
entry.kind,
entry.seq,
entry.timestamp_version,
&entry.prev_leaf_hash,
&tampered,
);
assert!(verify_inclusion_proof(
leaf,
proof.leaf_index,
proof.tree_size,
&proof.audit_path,
root
)
.is_err());
}
#[hegel::test]
fn prop_wrong_index_rejects(tc: hegel::TestCase) {
let n = tc.draw(gs::integers::<usize>().min_value(2).max_value(256));
let mut payloads: Vec<Vec<u8>> = Vec::with_capacity(n);
for _ in 0..n {
payloads.push(tc.draw(gs::binary().max_size(64)));
}
let log = build_log(&payloads);
let root = log.root_hash();
let real = tc.draw(gs::integers::<usize>().max_value(n - 1));
let wrong_candidate = tc.draw(gs::integers::<usize>().max_value(n - 1));
let wrong = if wrong_candidate == real {
(real + 1) % n
} else {
wrong_candidate
};
let entry = log
.entry(real as u64)
.unwrap_or_else(|| std::process::abort());
let payload = payloads.get(real).unwrap_or_else(|| std::process::abort());
let proof = log
.inclusion_proof(real as u64)
.unwrap_or_else(|_| std::process::abort());
let leaf = leaf_hash(
entry.kind,
entry.seq,
entry.timestamp_version,
&entry.prev_leaf_hash,
payload,
);
let result = verify_inclusion_proof(
leaf,
wrong as u64,
proof.tree_size,
&proof.audit_path,
root,
);
assert!(result.is_err());
}
#[hegel::test]
fn prop_tampered_proof_sibling_rejects(tc: hegel::TestCase) {
let n = tc.draw(gs::integers::<usize>().min_value(2).max_value(256));
let mut payloads: Vec<Vec<u8>> = Vec::with_capacity(n);
for _ in 0..n {
payloads.push(tc.draw(gs::binary().max_size(64)));
}
let log = build_log(&payloads);
let root = log.root_hash();
let idx = tc.draw(gs::integers::<usize>().max_value(n - 1));
let entry = log
.entry(idx as u64)
.unwrap_or_else(|| std::process::abort());
let payload = payloads.get(idx).unwrap_or_else(|| std::process::abort());
let mut proof = log
.inclusion_proof(idx as u64)
.unwrap_or_else(|_| std::process::abort());
if proof.audit_path.is_empty() {
return;
}
let sibling_index =
tc.draw(gs::integers::<usize>().max_value(proof.audit_path.len() - 1));
if let Some(sibling) = proof.audit_path.get_mut(sibling_index) {
sibling[0] ^= 0x01;
}
let leaf = leaf_hash(
entry.kind,
entry.seq,
entry.timestamp_version,
&entry.prev_leaf_hash,
payload,
);
assert!(verify_inclusion_proof(
leaf,
proof.leaf_index,
proof.tree_size,
&proof.audit_path,
root
)
.is_err());
}
#[hegel::test]
fn prop_leaf_chain_is_monotonic(tc: hegel::TestCase) {
let payloads = draw_payloads(&tc);
let log = build_log(&payloads);
let entries = log.entries();
for pair in entries.windows(2) {
let prev = &pair[0];
let curr = &pair[1];
assert_eq!(curr.seq, prev.seq.saturating_add(1));
let expected_prev_hash = leaf_hash(
prev.kind,
prev.seq,
prev.timestamp_version,
&prev.prev_leaf_hash,
payloads
.get(prev.seq as usize)
.unwrap_or_else(|| std::process::abort()),
);
assert_eq!(curr.prev_leaf_hash, expected_prev_hash);
}
}
#[hegel::test]
fn prop_sth_sign_verify_roundtrip(tc: hegel::TestCase) {
let payloads = draw_payloads(&tc);
let mut log = build_log(&payloads);
let operator = SigningKey::generate();
log.set_operator(operator.verifying_key());
let sth = log.sign_tree_head(&operator);
assert!(log.verify_tree_head(&sth).is_ok());
}
#[hegel::test]
fn prop_forged_sth_rejects(tc: hegel::TestCase) {
let payloads = draw_payloads(&tc);
let mut log = build_log(&payloads);
let operator = SigningKey::generate();
log.set_operator(operator.verifying_key());
let mut sth = log.sign_tree_head(&operator);
sth.root_hash[0] ^= 0x01;
assert!(log.verify_tree_head(&sth).is_err());
}
#[hegel::test]
fn prop_subtree_cache_matches_from_scratch(tc: hegel::TestCase) {
let payloads = draw_payloads(&tc);
let mut log = TransparencyLog::new();
let mut incremental_roots = Vec::with_capacity(payloads.len());
for (i, p) in payloads.iter().enumerate() {
log.append(LogEntryKind::DsseEnvelope, p, (i as u64) + 1)
.unwrap_or_else(|_| std::process::abort());
incremental_roots.push(log.root_hash());
}
let leaves: Vec<[u8; 32]> = (0..log.tree_size())
.map(|i| log.leaf_hash_at(i).unwrap_or_else(|| std::process::abort()))
.collect();
for (i, expected) in incremental_roots.iter().enumerate() {
let from_scratch = mth_from_scratch(&leaves[..=i]);
if from_scratch != *expected {
std::process::abort();
}
}
}
fn mth_from_scratch(leaves: &[[u8; 32]]) -> [u8; 32] {
match leaves.len() {
0 => empty_root_for_test(),
1 => leaves[0],
n => {
let k = split_point_for_test(n);
let left = mth_from_scratch(&leaves[..k]);
let right = mth_from_scratch(&leaves[k..]);
node_hash_for_test(&left, &right)
}
}
}
const fn split_point_for_test(n: usize) -> usize {
let mut k = 1usize;
while k.saturating_mul(2) < n {
k = k.saturating_mul(2);
}
k
}
fn empty_root_for_test() -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(LOG_EMPTY_DOMAIN);
*h.finalize().as_bytes()
}
fn node_hash_for_test(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(LOG_NODE_DOMAIN);
h.update(left);
h.update(right);
*h.finalize().as_bytes()
}
#[hegel::test]
fn prop_self_contained_inclusion_proof_verification(tc: hegel::TestCase) {
let payloads = draw_payloads(&tc);
let log = build_log(&payloads);
let root = log.root_hash();
drop(payloads);
for i in 0..log.tree_size() {
let leaf = log.leaf_hash_at(i).unwrap_or_else(|| std::process::abort());
let proof = log
.inclusion_proof(i)
.unwrap_or_else(|_| std::process::abort());
verify_inclusion_proof(
leaf,
proof.leaf_index,
proof.tree_size,
&proof.audit_path,
root,
)
.unwrap_or_else(|_| std::process::abort());
}
}
}
mod leaf_hash_at_tests {
use super::*;
#[test]
fn leaf_hash_at_matches_leaf_hash_for_every_entry() {
let mut log = TransparencyLog::new();
let payloads: Vec<&[u8]> = vec![b"alpha", b"beta", b"gamma", b"delta", b"epsilon"];
for (i, p) in payloads.iter().enumerate() {
log.append(LogEntryKind::DsseEnvelope, p, (i as u64) + 1)
.unwrap();
}
for (i, p) in payloads.iter().enumerate() {
let entry = log.entry(i as u64).unwrap();
let expected = leaf_hash(
entry.kind,
entry.seq,
entry.timestamp_version,
&entry.prev_leaf_hash,
p,
);
let stored = log.leaf_hash_at(i as u64).unwrap();
assert_eq!(stored, expected);
}
}
#[test]
fn leaf_hash_at_out_of_range_returns_none() {
let mut log = TransparencyLog::new();
log.append(LogEntryKind::VersionAttestation, b"only", 1)
.unwrap();
assert!(log.leaf_hash_at(0).is_some());
assert!(log.leaf_hash_at(1).is_none());
assert!(log.leaf_hash_at(u64::MAX).is_none());
}
#[test]
fn leaf_hash_at_on_empty_log_returns_none() {
let log = TransparencyLog::new();
assert!(log.leaf_hash_at(0).is_none());
}
}
}