Skip to main content

zync_core/
verifier.rs

1//! ligerito proof verification with continuity checking
2//!
3//! wire format (combined proof):
4//!   [giga_full_size: u32][giga_full][tip_full]
5//! where each full proof is:
6//!   [public_outputs_len: u32][public_outputs (bincode)][log_size: u8][ligerito_proof (bincode)]
7
8use crate::verifier_config_for_log_size;
9use anyhow::Result;
10use ligerito::{transcript::FiatShamir, verify_with_transcript, FinalizedLigeritoProof};
11use ligerito_binary_fields::{BinaryElem128, BinaryElem32};
12use serde::{Deserialize, Serialize};
13
14#[cfg(not(target_arch = "wasm32"))]
15use std::thread;
16
17/// public outputs embedded in each proof
18#[derive(Clone, Debug, Serialize, Deserialize)]
19pub struct ProofPublicOutputs {
20    pub start_height: u32,
21    pub end_height: u32,
22    pub start_hash: [u8; 32],
23    pub start_prev_hash: [u8; 32],
24    pub tip_hash: [u8; 32],
25    pub tip_prev_hash: [u8; 32],
26    pub cumulative_difficulty: u64,
27    pub final_commitment: [u8; 32],
28    pub final_state_commitment: [u8; 32],
29    pub num_headers: u32,
30    /// Tree root (orchard commitment tree) at proof's end height
31    pub tip_tree_root: [u8; 32],
32    /// Nullifier root (nomt) at proof's end height
33    pub tip_nullifier_root: [u8; 32],
34    /// Final actions commitment (running chain, zeros until populated)
35    pub final_actions_commitment: [u8; 32],
36}
37
38/// result of proof verification
39#[derive(Clone, Debug)]
40pub struct VerifyResult {
41    pub gigaproof_valid: bool,
42    pub tip_valid: bool,
43    pub continuous: bool,
44    pub giga_outputs: ProofPublicOutputs,
45    pub tip_outputs: Option<ProofPublicOutputs>,
46}
47
48/// split a full proof into (public_outputs, raw_proof_bytes)
49fn split_full_proof(full: &[u8]) -> Result<(ProofPublicOutputs, Vec<u8>)> {
50    if full.len() < 4 {
51        anyhow::bail!("proof too short");
52    }
53    let public_len = u32::from_le_bytes([full[0], full[1], full[2], full[3]]) as usize;
54    if full.len() < 4 + public_len + 1 {
55        anyhow::bail!("proof truncated");
56    }
57    let outputs: ProofPublicOutputs = bincode::deserialize(&full[4..4 + public_len])
58        .map_err(|e| anyhow::anyhow!("deserialize public outputs: {}", e))?;
59    let raw = full[4 + public_len..].to_vec();
60    Ok((outputs, raw))
61}
62
63/// deserialize raw proof: [log_size: u8][proof_bytes...]
64fn deserialize_proof(
65    bytes: &[u8],
66) -> Result<(FinalizedLigeritoProof<BinaryElem32, BinaryElem128>, u8)> {
67    if bytes.is_empty() {
68        anyhow::bail!("empty proof bytes");
69    }
70    let log_size = bytes[0];
71    let proof = bincode::deserialize(&bytes[1..])
72        .map_err(|e| anyhow::anyhow!("failed to deserialize proof: {}", e))?;
73    Ok((proof, log_size))
74}
75
76/// verify a single raw proof (sha256 transcript to match prover)
77fn verify_single(proof_bytes: &[u8]) -> Result<bool> {
78    let (proof, log_size) = deserialize_proof(proof_bytes)?;
79    let config = verifier_config_for_log_size(log_size as u32);
80    let transcript = FiatShamir::new_sha256(0);
81    verify_with_transcript(&config, &proof, transcript)
82        .map_err(|e| anyhow::anyhow!("verification error: {}", e))
83}
84
85/// verify combined gigaproof + tip proof with continuity checking
86///
87/// format: [giga_full_size: u32][giga_full][tip_full]
88/// each full proof: [public_outputs_len: u32][public_outputs][log_size: u8][proof]
89///
90/// checks:
91/// 1. both proofs verify cryptographically
92/// 2. tip_proof.start_prev_hash == gigaproof.tip_hash (chain continuity)
93#[cfg(not(target_arch = "wasm32"))]
94pub fn verify_proofs(combined_proof: &[u8]) -> Result<(bool, bool)> {
95    let result = verify_proofs_full(combined_proof)?;
96    Ok((
97        result.gigaproof_valid,
98        result.tip_valid && result.continuous,
99    ))
100}
101
102/// full verification with detailed result
103#[cfg(not(target_arch = "wasm32"))]
104pub fn verify_proofs_full(combined_proof: &[u8]) -> Result<VerifyResult> {
105    if combined_proof.len() < 4 {
106        anyhow::bail!("proof too small");
107    }
108
109    let giga_full_size = u32::from_le_bytes([
110        combined_proof[0],
111        combined_proof[1],
112        combined_proof[2],
113        combined_proof[3],
114    ]) as usize;
115
116    if combined_proof.len() < 4 + giga_full_size {
117        anyhow::bail!("invalid proof format");
118    }
119
120    let giga_full = &combined_proof[4..4 + giga_full_size];
121    let tip_full = &combined_proof[4 + giga_full_size..];
122
123    // parse public outputs from both proofs
124    let (giga_outputs, giga_raw) = split_full_proof(giga_full)?;
125    let (tip_outputs, tip_raw) = if !tip_full.is_empty() {
126        let (o, r) = split_full_proof(tip_full)?;
127        (Some(o), r)
128    } else {
129        (None, vec![])
130    };
131
132    // verify both proofs in parallel
133    let giga_raw_clone = giga_raw;
134    let tip_raw_clone = tip_raw;
135    let giga_handle = thread::spawn(move || verify_single(&giga_raw_clone));
136    let tip_handle = if !tip_raw_clone.is_empty() {
137        Some(thread::spawn(move || verify_single(&tip_raw_clone)))
138    } else {
139        None
140    };
141
142    let gigaproof_valid = giga_handle
143        .join()
144        .map_err(|_| anyhow::anyhow!("gigaproof thread panicked"))??;
145    let tip_valid = match tip_handle {
146        Some(h) => h
147            .join()
148            .map_err(|_| anyhow::anyhow!("tip thread panicked"))??,
149        None => true,
150    };
151
152    // check continuity: tip starts where gigaproof ends
153    let continuous = match &tip_outputs {
154        Some(tip) => tip.start_prev_hash == giga_outputs.tip_hash,
155        None => true, // no tip = gigaproof covers everything
156    };
157
158    Ok(VerifyResult {
159        gigaproof_valid,
160        tip_valid,
161        continuous,
162        giga_outputs,
163        tip_outputs,
164    })
165}
166
167/// wasm variant
168#[cfg(target_arch = "wasm32")]
169pub fn verify_proofs(combined_proof: &[u8]) -> Result<(bool, bool)> {
170    let result = verify_proofs_full(combined_proof)?;
171    Ok((
172        result.gigaproof_valid,
173        result.tip_valid && result.continuous,
174    ))
175}
176
177#[cfg(target_arch = "wasm32")]
178pub fn verify_proofs_full(combined_proof: &[u8]) -> Result<VerifyResult> {
179    if combined_proof.len() < 4 {
180        anyhow::bail!("proof too small");
181    }
182
183    let giga_full_size = u32::from_le_bytes([
184        combined_proof[0],
185        combined_proof[1],
186        combined_proof[2],
187        combined_proof[3],
188    ]) as usize;
189
190    if combined_proof.len() < 4 + giga_full_size {
191        anyhow::bail!("invalid proof format");
192    }
193
194    let giga_full = &combined_proof[4..4 + giga_full_size];
195    let tip_full = &combined_proof[4 + giga_full_size..];
196
197    let (giga_outputs, giga_raw) = split_full_proof(giga_full)?;
198    let (tip_outputs, tip_raw) = if !tip_full.is_empty() {
199        let (o, r) = split_full_proof(tip_full)?;
200        (Some(o), r)
201    } else {
202        (None, vec![])
203    };
204
205    let gigaproof_valid = verify_single(&giga_raw)?;
206    let tip_valid = if !tip_raw.is_empty() {
207        verify_single(&tip_raw)?
208    } else {
209        true
210    };
211
212    let continuous = match &tip_outputs {
213        Some(tip) => tip.start_prev_hash == giga_outputs.tip_hash,
214        None => true,
215    };
216
217    Ok(VerifyResult {
218        gigaproof_valid,
219        tip_valid,
220        continuous,
221        giga_outputs,
222        tip_outputs,
223    })
224}
225
226/// verify just tip proof (for incremental sync)
227pub fn verify_tip(tip_proof: &[u8]) -> Result<bool> {
228    verify_single(tip_proof)
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn test_empty_proof_fails() {
237        let result = verify_proofs(&[]);
238        assert!(result.is_err());
239    }
240
241    #[test]
242    fn test_too_small_proof_fails() {
243        let result = verify_proofs(&[1, 2, 3]);
244        assert!(result.is_err());
245    }
246}