use bitvec::prelude::*;
use nomt_core::hasher::Blake3Hasher;
use nomt_core::proof::PathProof;
use nomt_core::trie::LeafData;
use sha2::{Digest, Sha256};
pub use nomt_core::hasher::Blake3Hasher as Hasher;
pub use nomt_core::proof::PathProof as NomtPathProof;
pub use nomt_core::trie::LeafData as NomtLeafData;
pub fn key_for_nullifier(nullifier: &[u8; 32]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(b"zidecar:nullifier:");
hasher.update(nullifier);
hasher.finalize().into()
}
pub fn key_for_note(cmx: &[u8; 32]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(b"zidecar:note:");
hasher.update(cmx);
hasher.finalize().into()
}
#[derive(Debug, thiserror::Error)]
pub enum NomtVerifyError {
#[error("no path proof data (old server?)")]
MissingProof,
#[error("deserialize path proof: {0}")]
Deserialize(String),
#[error("path proof verification failed: {0}")]
PathVerify(String),
#[error("key out of scope of proof")]
OutOfScope,
}
pub fn verify_commitment_proof(
cmx: &[u8; 32],
tree_root: [u8; 32],
path_proof_raw: &[u8],
value_hash: [u8; 32],
) -> Result<bool, NomtVerifyError> {
if path_proof_raw.is_empty() {
return Err(NomtVerifyError::MissingProof);
}
let path_proof: PathProof = bincode::deserialize(path_proof_raw)
.map_err(|e| NomtVerifyError::Deserialize(e.to_string()))?;
let key = key_for_note(cmx);
let verified = path_proof
.verify::<Blake3Hasher>(key.view_bits::<Msb0>(), tree_root)
.map_err(|e| NomtVerifyError::PathVerify(format!("{:?}", e)))?;
let expected_leaf = LeafData {
key_path: key,
value_hash,
};
match verified.confirm_value(&expected_leaf) {
Ok(v) => Ok(v),
Err(_) => Err(NomtVerifyError::OutOfScope),
}
}
pub fn verify_nullifier_proof(
nullifier: &[u8; 32],
nullifier_root: [u8; 32],
is_spent: bool,
path_proof_raw: &[u8],
value_hash: [u8; 32],
) -> Result<bool, NomtVerifyError> {
if path_proof_raw.is_empty() {
return Err(NomtVerifyError::MissingProof);
}
let path_proof: PathProof = bincode::deserialize(path_proof_raw)
.map_err(|e| NomtVerifyError::Deserialize(e.to_string()))?;
let key = key_for_nullifier(nullifier);
let verified = path_proof
.verify::<Blake3Hasher>(key.view_bits::<Msb0>(), nullifier_root)
.map_err(|e| NomtVerifyError::PathVerify(format!("{:?}", e)))?;
if is_spent {
let expected_leaf = LeafData {
key_path: key,
value_hash,
};
match verified.confirm_value(&expected_leaf) {
Ok(v) => Ok(v),
Err(_) => Err(NomtVerifyError::OutOfScope),
}
} else {
match verified.confirm_nonexistence(&key) {
Ok(v) => Ok(v),
Err(_) => Err(NomtVerifyError::OutOfScope),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_derivation_deterministic() {
let nf = [0xab; 32];
let k1 = key_for_nullifier(&nf);
let k2 = key_for_nullifier(&nf);
assert_eq!(k1, k2);
}
#[test]
fn key_derivation_domain_separation() {
let data = [0x42; 32];
let nf_key = key_for_nullifier(&data);
let note_key = key_for_note(&data);
assert_ne!(nf_key, note_key, "different domains must produce different keys");
}
#[test]
fn empty_proof_rejected() {
let cmx = [1u8; 32];
let root = [0u8; 32];
let err = verify_commitment_proof(&cmx, root, &[], [0u8; 32]);
assert!(matches!(err, Err(NomtVerifyError::MissingProof)));
}
#[test]
fn garbage_proof_rejected() {
let cmx = [1u8; 32];
let root = [0u8; 32];
let err = verify_commitment_proof(&cmx, root, &[0xff; 64], [0u8; 32]);
assert!(matches!(err, Err(NomtVerifyError::Deserialize(_))));
}
}