1use bitvec::prelude::*;
8use nomt_core::hasher::Blake3Hasher;
9use nomt_core::proof::PathProof;
10use nomt_core::trie::LeafData;
11use sha2::{Digest, Sha256};
12
13pub use nomt_core::hasher::Blake3Hasher as Hasher;
15pub use nomt_core::proof::PathProof as NomtPathProof;
16pub use nomt_core::trie::LeafData as NomtLeafData;
17
18pub fn key_for_nullifier(nullifier: &[u8; 32]) -> [u8; 32] {
20 let mut hasher = Sha256::new();
21 hasher.update(b"zidecar:nullifier:");
22 hasher.update(nullifier);
23 hasher.finalize().into()
24}
25
26pub fn key_for_note(cmx: &[u8; 32]) -> [u8; 32] {
28 let mut hasher = Sha256::new();
29 hasher.update(b"zidecar:note:");
30 hasher.update(cmx);
31 hasher.finalize().into()
32}
33
34#[derive(Debug, thiserror::Error)]
36pub enum NomtVerifyError {
37 #[error("no path proof data (old server?)")]
38 MissingProof,
39 #[error("deserialize path proof: {0}")]
40 Deserialize(String),
41 #[error("path proof verification failed: {0}")]
42 PathVerify(String),
43 #[error("key out of scope of proof")]
44 OutOfScope,
45}
46
47pub fn verify_commitment_proof(
53 cmx: &[u8; 32],
54 tree_root: [u8; 32],
55 path_proof_raw: &[u8],
56 value_hash: [u8; 32],
57) -> Result<bool, NomtVerifyError> {
58 if path_proof_raw.is_empty() {
59 return Err(NomtVerifyError::MissingProof);
60 }
61
62 let path_proof: PathProof = bincode::deserialize(path_proof_raw)
63 .map_err(|e| NomtVerifyError::Deserialize(e.to_string()))?;
64
65 let key = key_for_note(cmx);
66
67 let verified = path_proof
68 .verify::<Blake3Hasher>(key.view_bits::<Msb0>(), tree_root)
69 .map_err(|e| NomtVerifyError::PathVerify(format!("{:?}", e)))?;
70
71 let expected_leaf = LeafData {
72 key_path: key,
73 value_hash,
74 };
75 match verified.confirm_value(&expected_leaf) {
76 Ok(v) => Ok(v),
77 Err(_) => Err(NomtVerifyError::OutOfScope),
78 }
79}
80
81pub fn verify_nullifier_proof(
90 nullifier: &[u8; 32],
91 nullifier_root: [u8; 32],
92 is_spent: bool,
93 path_proof_raw: &[u8],
94 value_hash: [u8; 32],
95) -> Result<bool, NomtVerifyError> {
96 if path_proof_raw.is_empty() {
97 return Err(NomtVerifyError::MissingProof);
98 }
99
100 let path_proof: PathProof = bincode::deserialize(path_proof_raw)
101 .map_err(|e| NomtVerifyError::Deserialize(e.to_string()))?;
102
103 let key = key_for_nullifier(nullifier);
104
105 let verified = path_proof
106 .verify::<Blake3Hasher>(key.view_bits::<Msb0>(), nullifier_root)
107 .map_err(|e| NomtVerifyError::PathVerify(format!("{:?}", e)))?;
108
109 if is_spent {
110 let expected_leaf = LeafData {
111 key_path: key,
112 value_hash,
113 };
114 match verified.confirm_value(&expected_leaf) {
115 Ok(v) => Ok(v),
116 Err(_) => Err(NomtVerifyError::OutOfScope),
117 }
118 } else {
119 match verified.confirm_nonexistence(&key) {
120 Ok(v) => Ok(v),
121 Err(_) => Err(NomtVerifyError::OutOfScope),
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn key_derivation_deterministic() {
132 let nf = [0xab; 32];
133 let k1 = key_for_nullifier(&nf);
134 let k2 = key_for_nullifier(&nf);
135 assert_eq!(k1, k2);
136 }
137
138 #[test]
139 fn key_derivation_domain_separation() {
140 let data = [0x42; 32];
141 let nf_key = key_for_nullifier(&data);
142 let note_key = key_for_note(&data);
143 assert_ne!(nf_key, note_key, "different domains must produce different keys");
144 }
145
146 #[test]
147 fn empty_proof_rejected() {
148 let cmx = [1u8; 32];
149 let root = [0u8; 32];
150 let err = verify_commitment_proof(&cmx, root, &[], [0u8; 32]);
151 assert!(matches!(err, Err(NomtVerifyError::MissingProof)));
152 }
153
154 #[test]
155 fn garbage_proof_rejected() {
156 let cmx = [1u8; 32];
157 let root = [0u8; 32];
158 let err = verify_commitment_proof(&cmx, root, &[0xff; 64], [0u8; 32]);
159 assert!(matches!(err, Err(NomtVerifyError::Deserialize(_))));
160 }
161}