1use crate::error::ZyncError;
32use crate::verifier;
33use crate::{actions, ACTIVATION_HASH_MAINNET, EPOCH_SIZE};
34
35use zcash_note_encryption::ENC_CIPHERTEXT_SIZE;
36
37#[derive(Clone, Debug, Default)]
40pub struct ProvenRoots {
41 pub tree_root: [u8; 32],
42 pub nullifier_root: [u8; 32],
43 pub actions_commitment: [u8; 32],
44}
45
46#[derive(Debug)]
48pub struct CrossVerifyTally {
49 pub agree: u32,
50 pub disagree: u32,
51}
52
53impl CrossVerifyTally {
54 pub fn has_majority(&self) -> bool {
56 let total = self.agree + self.disagree;
57 if total == 0 {
58 return false;
59 }
60 let threshold = (total * 2).div_ceil(3);
61 self.agree >= threshold
62 }
63
64 pub fn total(&self) -> u32 {
65 self.agree + self.disagree
66 }
67}
68
69pub fn hashes_match(a: &[u8], b: &[u8]) -> bool {
72 if a.is_empty() || b.is_empty() {
73 return true; }
75 if a == b {
76 return true;
77 }
78 let mut b_rev = b.to_vec();
79 b_rev.reverse();
80 a == b_rev.as_slice()
81}
82
83pub fn verify_header_proof(
88 proof_bytes: &[u8],
89 tip: u32,
90 mainnet: bool,
91) -> Result<ProvenRoots, ZyncError> {
92 let result = verifier::verify_proofs_full(proof_bytes)
93 .map_err(|e| ZyncError::InvalidProof(format!("header proof: {}", e)))?;
94
95 if !result.epoch_proof_valid {
96 return Err(ZyncError::InvalidProof("epoch proof invalid".into()));
97 }
98 if !result.tip_valid {
99 return Err(ZyncError::InvalidProof("tip proof invalid".into()));
100 }
101 if !result.continuous {
102 return Err(ZyncError::InvalidProof("proof chain discontinuous".into()));
103 }
104
105 if mainnet && result.epoch_outputs.start_hash != ACTIVATION_HASH_MAINNET {
107 return Err(ZyncError::InvalidProof(format!(
108 "epoch proof start_hash doesn't match activation anchor: got {}",
109 hex::encode(&result.epoch_outputs.start_hash[..8]),
110 )));
111 }
112
113 let outputs = result
115 .tip_outputs
116 .as_ref()
117 .unwrap_or(&result.epoch_outputs);
118
119 if outputs.end_height + EPOCH_SIZE < tip {
121 return Err(ZyncError::InvalidProof(format!(
122 "header proof too stale: covers to {} but tip is {} (>{} blocks behind)",
123 outputs.end_height, tip, EPOCH_SIZE,
124 )));
125 }
126
127 Ok(ProvenRoots {
128 tree_root: outputs.tip_tree_root,
129 nullifier_root: outputs.tip_nullifier_root,
130 actions_commitment: outputs.final_actions_commitment,
131 })
132}
133
134pub fn verify_actions_commitment(
139 running: &[u8; 32],
140 proven: &[u8; 32],
141 has_saved_commitment: bool,
142) -> Result<[u8; 32], ZyncError> {
143 if !has_saved_commitment {
144 Ok(*proven)
147 } else if running != proven {
148 Err(ZyncError::StateMismatch(format!(
149 "actions commitment mismatch: server tampered with block actions (computed={} proven={})",
150 hex::encode(&running[..8]),
151 hex::encode(&proven[..8]),
152 )))
153 } else {
154 Ok(*running)
155 }
156}
157
158pub struct CommitmentProofData {
160 pub cmx: [u8; 32],
161 pub tree_root: [u8; 32],
162 pub path_proof_raw: Vec<u8>,
163 pub value_hash: [u8; 32],
164}
165
166impl CommitmentProofData {
167 pub fn verify(&self) -> Result<bool, crate::nomt::NomtVerifyError> {
168 crate::nomt::verify_commitment_proof(
169 &self.cmx,
170 self.tree_root,
171 &self.path_proof_raw,
172 self.value_hash,
173 )
174 }
175}
176
177pub struct NullifierProofData {
179 pub nullifier: [u8; 32],
180 pub nullifier_root: [u8; 32],
181 pub is_spent: bool,
182 pub path_proof_raw: Vec<u8>,
183 pub value_hash: [u8; 32],
184}
185
186impl NullifierProofData {
187 pub fn verify(&self) -> Result<bool, crate::nomt::NomtVerifyError> {
188 crate::nomt::verify_nullifier_proof(
189 &self.nullifier,
190 self.nullifier_root,
191 self.is_spent,
192 &self.path_proof_raw,
193 self.value_hash,
194 )
195 }
196}
197
198pub fn verify_commitment_proofs(
202 proofs: &[CommitmentProofData],
203 requested_cmxs: &[[u8; 32]],
204 proven: &ProvenRoots,
205 server_root: &[u8; 32],
206) -> Result<(), ZyncError> {
207 if server_root != &proven.tree_root {
209 return Err(ZyncError::VerificationFailed(format!(
210 "commitment tree root mismatch: server={} proven={}",
211 hex::encode(server_root),
212 hex::encode(proven.tree_root),
213 )));
214 }
215
216 if proofs.len() != requested_cmxs.len() {
218 return Err(ZyncError::VerificationFailed(format!(
219 "commitment proof count mismatch: requested {} but got {}",
220 requested_cmxs.len(),
221 proofs.len(),
222 )));
223 }
224
225 let cmx_set: std::collections::HashSet<[u8; 32]> = requested_cmxs.iter().copied().collect();
227 for proof in proofs {
228 if !cmx_set.contains(&proof.cmx) {
229 return Err(ZyncError::VerificationFailed(format!(
230 "server returned commitment proof for unrequested cmx {}",
231 hex::encode(proof.cmx),
232 )));
233 }
234
235 match proof.verify() {
237 Ok(true) => {}
238 Ok(false) => {
239 return Err(ZyncError::VerificationFailed(format!(
240 "commitment proof invalid for cmx {}",
241 hex::encode(proof.cmx),
242 )))
243 }
244 Err(e) => {
245 return Err(ZyncError::VerificationFailed(format!(
246 "commitment proof verification error: {}",
247 e,
248 )))
249 }
250 }
251
252 if proof.tree_root != proven.tree_root {
254 return Err(ZyncError::VerificationFailed(format!(
255 "commitment proof root mismatch for cmx {}",
256 hex::encode(proof.cmx),
257 )));
258 }
259 }
260
261 Ok(())
262}
263
264pub fn verify_nullifier_proofs(
268 proofs: &[NullifierProofData],
269 requested_nullifiers: &[[u8; 32]],
270 proven: &ProvenRoots,
271 server_root: &[u8; 32],
272) -> Result<Vec<[u8; 32]>, ZyncError> {
273 if server_root != &proven.nullifier_root {
275 return Err(ZyncError::VerificationFailed(format!(
276 "nullifier root mismatch: server={} proven={}",
277 hex::encode(server_root),
278 hex::encode(proven.nullifier_root),
279 )));
280 }
281
282 if proofs.len() != requested_nullifiers.len() {
283 return Err(ZyncError::VerificationFailed(format!(
284 "nullifier proof count mismatch: requested {} but got {}",
285 requested_nullifiers.len(),
286 proofs.len(),
287 )));
288 }
289
290 let nf_set: std::collections::HashSet<[u8; 32]> =
291 requested_nullifiers.iter().copied().collect();
292 let mut spent = Vec::new();
293
294 for proof in proofs {
295 if !nf_set.contains(&proof.nullifier) {
296 return Err(ZyncError::VerificationFailed(format!(
297 "server returned nullifier proof for unrequested nullifier {}",
298 hex::encode(proof.nullifier),
299 )));
300 }
301
302 match proof.verify() {
303 Ok(true) => {
304 if proof.is_spent {
305 spent.push(proof.nullifier);
306 }
307 }
308 Ok(false) => {
309 return Err(ZyncError::VerificationFailed(format!(
310 "nullifier proof invalid for {}",
311 hex::encode(proof.nullifier),
312 )))
313 }
314 Err(e) => {
315 return Err(ZyncError::VerificationFailed(format!(
316 "nullifier proof verification error: {}",
317 e,
318 )))
319 }
320 }
321
322 if proof.nullifier_root != proven.nullifier_root {
323 return Err(ZyncError::VerificationFailed(format!(
324 "nullifier proof root mismatch for {}: server={} proven={}",
325 hex::encode(proof.nullifier),
326 hex::encode(proof.nullifier_root),
327 hex::encode(proven.nullifier_root),
328 )));
329 }
330 }
331
332 Ok(spent)
333}
334
335pub fn extract_enc_ciphertext(
340 raw_tx: &[u8],
341 cmx: &[u8; 32],
342 epk: &[u8; 32],
343) -> Option<[u8; ENC_CIPHERTEXT_SIZE]> {
344 for i in 0..raw_tx.len().saturating_sub(64 + ENC_CIPHERTEXT_SIZE) {
345 if &raw_tx[i..i + 32] == cmx && &raw_tx[i + 32..i + 64] == epk {
346 let start = i + 64;
347 let end = start + ENC_CIPHERTEXT_SIZE;
348 if end <= raw_tx.len() {
349 let mut enc = [0u8; ENC_CIPHERTEXT_SIZE];
350 enc.copy_from_slice(&raw_tx[start..end]);
351 return Some(enc);
352 }
353 }
354 }
355 None
356}
357
358pub fn chain_actions_commitment(
363 initial: &[u8; 32],
364 blocks: &[(u32, Vec<([u8; 32], [u8; 32], [u8; 32])>)], ) -> [u8; 32] {
366 let mut running = *initial;
367 for (height, block_actions) in blocks {
368 let actions_root = actions::compute_actions_root(block_actions);
369 running = actions::update_actions_commitment(&running, &actions_root, *height);
370 }
371 running
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn test_hashes_match_same() {
380 let h = [1u8; 32];
381 assert!(hashes_match(&h, &h));
382 }
383
384 #[test]
385 fn test_hashes_match_reversed() {
386 let a: Vec<u8> = (0..32).collect();
387 let b: Vec<u8> = (0..32).rev().collect();
388 assert!(hashes_match(&a, &b));
389 }
390
391 #[test]
392 fn test_hashes_match_empty() {
393 assert!(hashes_match(&[], &[1u8; 32]));
394 assert!(hashes_match(&[1u8; 32], &[]));
395 }
396
397 #[test]
398 fn test_hashes_no_match() {
399 let a = [1u8; 32];
400 let b = [2u8; 32];
401 assert!(!hashes_match(&a, &b));
402 }
403
404 #[test]
405 fn test_cross_verify_tally_majority() {
406 let tally = CrossVerifyTally {
407 agree: 3,
408 disagree: 1,
409 };
410 assert!(tally.has_majority()); let tally = CrossVerifyTally {
413 agree: 1,
414 disagree: 2,
415 };
416 assert!(!tally.has_majority()); }
418
419 #[test]
420 fn test_cross_verify_tally_empty() {
421 let tally = CrossVerifyTally {
422 agree: 0,
423 disagree: 0,
424 };
425 assert!(!tally.has_majority());
426 }
427
428 #[test]
429 fn test_actions_commitment_legacy() {
430 let proven = [42u8; 32];
431 let result = verify_actions_commitment(&[0u8; 32], &proven, false).unwrap();
432 assert_eq!(result, proven);
433 }
434
435 #[test]
436 fn test_actions_commitment_match() {
437 let commitment = [42u8; 32];
438 let result = verify_actions_commitment(&commitment, &commitment, true).unwrap();
439 assert_eq!(result, commitment);
440 }
441
442 #[test]
443 fn test_actions_commitment_mismatch() {
444 let running = [1u8; 32];
445 let proven = [2u8; 32];
446 assert!(verify_actions_commitment(&running, &proven, true).is_err());
447 }
448
449 #[test]
450 fn test_extract_enc_ciphertext_not_found() {
451 let raw = vec![0u8; 100];
452 let cmx = [1u8; 32];
453 let epk = [2u8; 32];
454 assert!(extract_enc_ciphertext(&raw, &cmx, &epk).is_none());
455 }
456
457 #[test]
458 fn test_chain_actions_commitment_empty() {
459 let initial = [0u8; 32];
460 let result = chain_actions_commitment(&initial, &[]);
461 assert_eq!(result, initial);
462 }
463}