confium_log_monitor/
verify.rs1use anyhow::{Result, bail, ensure};
8use sha2::{Digest, Sha256};
9
10use crate::client::{ConsistencyProof, TreeHead};
11
12pub fn verify_consistency(
17 old_root: &str,
18 old_size: u64,
19 new_head: &TreeHead,
20 proof: &ConsistencyProof,
21) -> Result<()> {
22 ensure!(
23 proof.old_size == old_size,
24 "proof old_size {} doesn't match requested {}",
25 proof.old_size,
26 old_size
27 );
28 ensure!(
29 proof.new_size == new_head.tree_size,
30 "proof new_size {} doesn't match head {}",
31 proof.new_size,
32 new_head.tree_size
33 );
34 use subtle::ConstantTimeEq;
38 let proof_root_bytes = hex::decode(&proof.new_root).unwrap_or_default();
39 let head_root_bytes = hex::decode(&new_head.root).unwrap_or_default();
40 let root_ok: bool = proof_root_bytes.ct_eq(&head_root_bytes).into();
41 ensure!(root_ok, "proof new_root doesn't match head root");
42
43 let proof_hashes: Vec<[u8; 32]> = proof
49 .proof
50 .iter()
51 .map(|h| {
52 let bytes = hex::decode(h).unwrap_or_default();
53 let mut arr = [0u8; 32];
54 if bytes.len() == 32 {
55 arr.copy_from_slice(&bytes);
56 }
57 arr
58 })
59 .collect();
60
61 ensure!(
67 !proof_hashes.is_empty() || old_size == 0,
68 "consistency proof is empty for non-zero old_size"
69 );
70
71 if old_size.is_power_of_two() && old_size > 0 {
74 let computed_old = hex::encode(proof_hashes[0]);
75 ensure!(
76 computed_old == old_root,
77 "computed old root {} doesn't match cached {}",
78 computed_old,
79 old_root
80 );
81 }
82
83 tracing::debug!(
84 old_size,
85 new_size = new_head.tree_size,
86 proof_len = proof_hashes.len(),
87 "consistency proof structure OK"
88 );
89 Ok(())
90}
91
92#[allow(dead_code)]
96pub fn verify_inclusion(
97 leaf_hash: &[u8; 32],
98 steps: &[(Vec<u8>, bool)], root: &[u8; 32],
100) -> Result<()> {
101 let mut current = *leaf_hash;
102 for (sibling, is_left) in steps {
103 if sibling.len() != 32 {
104 bail!("sibling must be 32 bytes, got {}", sibling.len());
105 }
106 let mut sib_arr = [0u8; 32];
107 sib_arr.copy_from_slice(sibling);
108 current = if *is_left {
109 hash_pair(&sib_arr, ¤t)
110 } else {
111 hash_pair(¤t, &sib_arr)
112 };
113 }
114 use subtle::ConstantTimeEq;
115 let root_ok: bool = current.ct_eq(root).into();
116 ensure!(root_ok, "inclusion proof doesn't reach root");
117 Ok(())
118}
119
120fn hash_pair(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
121 let mut h = Sha256::new();
122 h.update(left);
123 h.update(right);
124 let digest: [u8; 32] = h.finalize().into();
125 digest
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn inclusion_proof_round_trip() {
134 let leaf = [0xaa; 32];
135 let sibling = [0xbb; 32];
136 let root = hash_pair(&leaf, &sibling);
137 verify_inclusion(&leaf, &[(sibling.to_vec(), false)], &root).unwrap();
138 }
139
140 #[test]
141 fn inclusion_proof_rejects_wrong_root() {
142 let leaf = [0xaa; 32];
143 let sibling = [0xbb; 32];
144 let wrong_root = [0xff; 32];
145 assert!(verify_inclusion(&leaf, &[(sibling.to_vec(), false)], &wrong_root).is_err());
146 }
147}