1use crate::{verifier_config_for_log_size, FIELDS_PER_HEADER, TIP_SENTINEL_SIZE};
9use anyhow::Result;
10use ligerito::{
11 transcript::{FiatShamir, Transcript},
12 verify_with_transcript, FinalizedLigeritoProof,
13};
14use ligerito_binary_fields::{BinaryElem128, BinaryElem32};
15use serde::{Deserialize, Serialize};
16
17#[cfg(not(target_arch = "wasm32"))]
18use std::thread;
19
20#[derive(Clone, Debug, Serialize, Deserialize)]
22pub struct ProofPublicOutputs {
23 pub start_height: u32,
24 pub end_height: u32,
25 pub start_hash: [u8; 32],
26 pub start_prev_hash: [u8; 32],
27 pub tip_hash: [u8; 32],
28 pub tip_prev_hash: [u8; 32],
29 pub cumulative_difficulty: u64,
30 pub final_commitment: [u8; 32],
31 pub final_state_commitment: [u8; 32],
32 pub num_headers: u32,
33 pub tip_tree_root: [u8; 32],
35 pub tip_nullifier_root: [u8; 32],
37 pub final_actions_commitment: [u8; 32],
39}
40
41#[derive(Clone, Debug)]
43pub struct VerifyResult {
44 pub epoch_proof_valid: bool,
45 pub tip_valid: bool,
46 pub continuous: bool,
47 pub epoch_outputs: ProofPublicOutputs,
48 pub tip_outputs: Option<ProofPublicOutputs>,
49}
50
51fn split_full_proof(full: &[u8]) -> Result<(ProofPublicOutputs, Vec<u8>)> {
53 if full.len() < 4 {
54 anyhow::bail!("proof too short");
55 }
56 let public_len = u32::from_le_bytes([full[0], full[1], full[2], full[3]]) as usize;
57 if full.len() < 4 + public_len + 1 {
58 anyhow::bail!("proof truncated");
59 }
60 let outputs: ProofPublicOutputs = bincode::deserialize(&full[4..4 + public_len])
61 .map_err(|e| anyhow::anyhow!("deserialize public outputs: {}", e))?;
62 let raw = full[4 + public_len..].to_vec();
63 Ok((outputs, raw))
64}
65
66fn deserialize_proof(
68 bytes: &[u8],
69) -> Result<(FinalizedLigeritoProof<BinaryElem32, BinaryElem128>, u8)> {
70 if bytes.is_empty() {
71 anyhow::bail!("empty proof bytes");
72 }
73 let log_size = bytes[0];
74 let proof = bincode::deserialize(&bytes[1..])
75 .map_err(|e| anyhow::anyhow!("failed to deserialize proof: {}", e))?;
76 Ok((proof, log_size))
77}
78
79fn verify_single(proof_bytes: &[u8], public_outputs: &ProofPublicOutputs) -> Result<bool> {
84 let (proof, log_size) = deserialize_proof(proof_bytes)?;
85
86 let expected_trace_elements =
89 (public_outputs.num_headers as usize) * FIELDS_PER_HEADER + TIP_SENTINEL_SIZE;
90 let expected_padded = expected_trace_elements.next_power_of_two();
91 let min_log_size = expected_padded.trailing_zeros() as u8;
92 if log_size < min_log_size {
93 anyhow::bail!(
94 "log_size too small: proof claims {} but num_headers={} requires at least {}",
95 log_size,
96 public_outputs.num_headers,
97 min_log_size,
98 );
99 }
100
101 let config = verifier_config_for_log_size(log_size as u32);
102 let mut transcript = FiatShamir::new_sha256(0);
103
104 let public_bytes = bincode::serialize(public_outputs)
106 .map_err(|e| anyhow::anyhow!("serialize public outputs: {}", e))?;
107 transcript.absorb_bytes(b"public_outputs", &public_bytes);
108
109 verify_with_transcript(&config, &proof, transcript)
110 .map_err(|e| anyhow::anyhow!("verification error: {}", e))
111}
112
113#[cfg(not(target_arch = "wasm32"))]
122pub fn verify_proofs(combined_proof: &[u8]) -> Result<(bool, bool)> {
123 let result = verify_proofs_full(combined_proof)?;
124 Ok((
125 result.epoch_proof_valid,
126 result.tip_valid && result.continuous,
127 ))
128}
129
130#[cfg(not(target_arch = "wasm32"))]
132pub fn verify_proofs_full(combined_proof: &[u8]) -> Result<VerifyResult> {
133 if combined_proof.len() < 4 {
134 anyhow::bail!("proof too small");
135 }
136
137 let epoch_full_size = u32::from_le_bytes([
138 combined_proof[0],
139 combined_proof[1],
140 combined_proof[2],
141 combined_proof[3],
142 ]) as usize;
143
144 if combined_proof.len() < 4 + epoch_full_size {
145 anyhow::bail!("invalid proof format");
146 }
147
148 let epoch_full = &combined_proof[4..4 + epoch_full_size];
149 let tip_full = &combined_proof[4 + epoch_full_size..];
150
151 let (epoch_outputs, epoch_raw) = split_full_proof(epoch_full)?;
153 let (tip_outputs, tip_raw) = if !tip_full.is_empty() {
154 let (o, r) = split_full_proof(tip_full)?;
155 (Some(o), r)
156 } else {
157 (None, vec![])
158 };
159
160 let epoch_raw_clone = epoch_raw;
162 let epoch_outputs_clone = epoch_outputs.clone();
163 let tip_raw_clone = tip_raw;
164 let tip_outputs_clone = tip_outputs.clone();
165 let epoch_handle =
166 thread::spawn(move || verify_single(&epoch_raw_clone, &epoch_outputs_clone));
167 let tip_handle = if !tip_raw_clone.is_empty() {
168 let tip_out = tip_outputs_clone.unwrap();
169 Some(thread::spawn(move || {
170 verify_single(&tip_raw_clone, &tip_out)
171 }))
172 } else {
173 None
174 };
175
176 let epoch_proof_valid = epoch_handle
177 .join()
178 .map_err(|_| anyhow::anyhow!("epoch proof thread panicked"))??;
179 let tip_valid = match tip_handle {
180 Some(h) => h
181 .join()
182 .map_err(|_| anyhow::anyhow!("tip thread panicked"))??,
183 None => true,
184 };
185
186 let continuous = match &tip_outputs {
188 Some(tip) => tip.start_prev_hash == epoch_outputs.tip_hash,
189 None => true, };
191
192 Ok(VerifyResult {
193 epoch_proof_valid,
194 tip_valid,
195 continuous,
196 epoch_outputs,
197 tip_outputs,
198 })
199}
200
201#[cfg(target_arch = "wasm32")]
203pub fn verify_proofs(combined_proof: &[u8]) -> Result<(bool, bool)> {
204 let result = verify_proofs_full(combined_proof)?;
205 Ok((
206 result.epoch_proof_valid,
207 result.tip_valid && result.continuous,
208 ))
209}
210
211#[cfg(target_arch = "wasm32")]
212pub fn verify_proofs_full(combined_proof: &[u8]) -> Result<VerifyResult> {
213 if combined_proof.len() < 4 {
214 anyhow::bail!("proof too small");
215 }
216
217 let epoch_full_size = u32::from_le_bytes([
218 combined_proof[0],
219 combined_proof[1],
220 combined_proof[2],
221 combined_proof[3],
222 ]) as usize;
223
224 if combined_proof.len() < 4 + epoch_full_size {
225 anyhow::bail!("invalid proof format");
226 }
227
228 let epoch_full = &combined_proof[4..4 + epoch_full_size];
229 let tip_full = &combined_proof[4 + epoch_full_size..];
230
231 let (epoch_outputs, epoch_raw) = split_full_proof(epoch_full)?;
232 let (tip_outputs, tip_raw) = if !tip_full.is_empty() {
233 let (o, r) = split_full_proof(tip_full)?;
234 (Some(o), r)
235 } else {
236 (None, vec![])
237 };
238
239 let epoch_proof_valid = verify_single(&epoch_raw, &epoch_outputs)?;
240 let tip_valid = if !tip_raw.is_empty() {
241 verify_single(&tip_raw, tip_outputs.as_ref().unwrap())?
242 } else {
243 true
244 };
245
246 let continuous = match &tip_outputs {
247 Some(tip) => tip.start_prev_hash == epoch_outputs.tip_hash,
248 None => true,
249 };
250
251 Ok(VerifyResult {
252 epoch_proof_valid,
253 tip_valid,
254 continuous,
255 epoch_outputs,
256 tip_outputs,
257 })
258}
259
260pub fn verify_tip(tip_proof: &[u8]) -> Result<bool> {
263 let (outputs, raw) = split_full_proof(tip_proof)?;
264 verify_single(&raw, &outputs)
265}
266
267#[derive(Clone, Debug)]
269pub struct ChainVerifyResult {
270 pub all_proofs_valid: bool,
272 pub chain_continuous: bool,
274 pub start_outputs: ProofPublicOutputs,
276 pub tip_outputs: ProofPublicOutputs,
278 pub num_segments: usize,
280}
281
282pub fn verify_chain(segments: &[&[u8]]) -> Result<ChainVerifyResult> {
310 if segments.is_empty() {
311 anyhow::bail!("empty chain");
312 }
313
314 let mut all_outputs: Vec<ProofPublicOutputs> = Vec::with_capacity(segments.len());
315
316 let mut all_valid = true;
318 for (i, segment) in segments.iter().enumerate() {
319 let (outputs, raw) = split_full_proof(segment)
320 .map_err(|e| anyhow::anyhow!("segment {}: {}", i, e))?;
321 let valid = verify_single(&raw, &outputs)
322 .map_err(|e| anyhow::anyhow!("segment {} verification: {}", i, e))?;
323 if !valid {
324 all_valid = false;
325 }
326 all_outputs.push(outputs);
327 }
328
329 let mut continuous = true;
331 for i in 0..all_outputs.len() - 1 {
332 let prev = &all_outputs[i];
333 let next = &all_outputs[i + 1];
334
335 if prev.tip_hash != next.start_prev_hash {
337 continuous = false;
338 break;
339 }
340
341 if prev.end_height + 1 != next.start_height {
343 continuous = false;
344 break;
345 }
346 }
347
348 Ok(ChainVerifyResult {
349 all_proofs_valid: all_valid,
350 chain_continuous: continuous,
351 start_outputs: all_outputs[0].clone(),
352 tip_outputs: all_outputs.last().unwrap().clone(),
353 num_segments: segments.len(),
354 })
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn test_empty_proof_fails() {
363 let result = verify_proofs(&[]);
364 assert!(result.is_err());
365 }
366
367 #[test]
368 fn test_too_small_proof_fails() {
369 let result = verify_proofs(&[1, 2, 3]);
370 assert!(result.is_err());
371 }
372}