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