Skip to main content

zync_core/
nomt.rs

1//! NOMT proof verification primitives shared between zcli and zidecar.
2//!
3//! Domain-separated key derivation and merkle proof verification using nomt-core.
4//! Both client (zcli) and server (zidecar) MUST use these same functions to ensure
5//! key derivation is consistent.
6
7use bitvec::prelude::*;
8use nomt_core::hasher::Blake3Hasher;
9use nomt_core::proof::PathProof;
10use nomt_core::trie::LeafData;
11use sha2::{Digest, Sha256};
12
13// re-export for consumers
14pub use nomt_core::hasher::Blake3Hasher as Hasher;
15pub use nomt_core::proof::PathProof as NomtPathProof;
16pub use nomt_core::trie::LeafData as NomtLeafData;
17
18/// Domain-separated key for nullifier lookups in the NOMT tree.
19pub 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
26/// Domain-separated key for note/commitment lookups in the NOMT tree.
27pub 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/// Error from NOMT proof verification.
35#[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
47/// Verify a commitment (note existence) proof against a tree root.
48///
49/// Returns `Ok(true)` if the note exists with the expected value,
50/// `Ok(false)` if the proof is valid but the value doesn't match,
51/// `Err` if the proof is cryptographically invalid.
52pub 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
81/// Verify a nullifier proof against a nullifier root.
82///
83/// If `is_spent` is true, verifies the nullifier EXISTS in the tree.
84/// If `is_spent` is false, verifies the nullifier does NOT exist.
85///
86/// Returns `Ok(true)` if the proof matches the claimed spent status,
87/// `Ok(false)` if the proof contradicts the claim (server lied),
88/// `Err` if the proof is cryptographically invalid.
89pub 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}