1#![cfg_attr(not(feature = "std"), no_std)]
26
27#[cfg(not(feature = "std"))]
28extern crate alloc;
29
30#[cfg(not(feature = "std"))]
31use alloc::vec::Vec;
32#[cfg(feature = "std")]
33use std::vec::Vec;
34
35use anyhow::anyhow;
36#[cfg(feature = "std")]
37use std::path::Path;
38
39pub use qp_plonky2_verifier::{
41 CircuitConfig, CommonCircuitData, ProofWithPublicInputs, VerifierCircuitData,
42 VerifierOnlyCircuitData, C, D, F,
43};
44
45use qp_plonky2_verifier::field::types::PrimeField64;
46use qp_plonky2_verifier::util::serialization::DefaultGateSerializer;
47use tiny_keccak::{Hasher, Keccak};
48
49pub use qp_wormhole_inputs::{
51 BlockData, BytesDigest, PrivateBatchPublicInputs, PublicBatchPublicInputs, PublicCircuitInputs,
52 PublicInputsByAccount, MIN_LEAF_SECURITY_BITS, PUBLIC_INPUTS_FELTS_LEN,
53};
54
55pub fn parse_public_inputs(
57 proof: &ProofWithPublicInputs<F, C, D>,
58) -> anyhow::Result<PublicCircuitInputs> {
59 let u64s: Vec<u64> = proof
60 .public_inputs
61 .iter()
62 .map(|f| f.to_canonical_u64())
63 .collect();
64 PublicCircuitInputs::try_from_u64_slice(&u64s)
65}
66
67pub fn parse_private_batch_public_inputs(
69 proof: &ProofWithPublicInputs<F, C, D>,
70) -> anyhow::Result<PrivateBatchPublicInputs> {
71 let u64s: Vec<u64> = proof
72 .public_inputs
73 .iter()
74 .map(|f| f.to_canonical_u64())
75 .collect();
76 PrivateBatchPublicInputs::try_from_u64_slice(&u64s)
77}
78
79pub fn parse_public_batch_public_inputs(
81 proof: &ProofWithPublicInputs<F, C, D>,
82 num_private_batch_proofs: usize,
83 num_leaf_proofs: usize,
84) -> anyhow::Result<PublicBatchPublicInputs> {
85 let u64s: Vec<u64> = proof
86 .public_inputs
87 .iter()
88 .map(|f| f.to_canonical_u64())
89 .collect();
90 PublicBatchPublicInputs::try_from_u64_slice(&u64s, num_private_batch_proofs, num_leaf_proofs)
91}
92
93#[derive(Debug)]
97pub struct WormholeVerifier {
98 pub circuit_data: VerifierCircuitData<F, C, D>,
99}
100
101const CANONICAL_LEAF_VERIFIER_KECCAK256: [u8; 32] = [
106 0x21, 0x86, 0xb2, 0x0f, 0x20, 0x02, 0x7d, 0xae, 0x1e, 0x16, 0x23, 0xa0, 0x26, 0xe9, 0x0f, 0xf5,
107 0x50, 0x04, 0x2b, 0x33, 0x02, 0xfb, 0x1a, 0x01, 0x4e, 0x9a, 0xd5, 0x7b, 0x8a, 0x5b, 0x22, 0xb2,
108];
109const CANONICAL_LEAF_COMMON_KECCAK256: [u8; 32] = [
110 0x84, 0x5d, 0x8f, 0xdf, 0x8f, 0xfe, 0x9f, 0xd5, 0xef, 0x3f, 0x8b, 0xe8, 0x74, 0x11, 0x4b, 0xf1,
111 0x57, 0x5d, 0xcf, 0xe2, 0xb0, 0x1d, 0xb4, 0x46, 0x5f, 0x3d, 0x9a, 0x34, 0xf6, 0xb7, 0xb7, 0xe1,
112];
113
114pub const MAX_VERIFIER_ARTIFACT_BYTES: u64 = 1024 * 1024;
128
129fn keccak256(input: &[u8]) -> [u8; 32] {
130 let mut output = [0u8; 32];
131 let mut hasher = Keccak::v256();
132 hasher.update(input);
133 hasher.finalize(&mut output);
134 output
135}
136
137#[cfg(feature = "std")]
143fn read_artifact_file(path: &Path) -> anyhow::Result<Vec<u8>> {
144 use anyhow::Context as _;
145
146 let metadata = std::fs::metadata(path)
147 .with_context(|| format!("failed to stat artifact file {}", path.display()))?;
148 let claimed_len = metadata.len();
149 if claimed_len > MAX_VERIFIER_ARTIFACT_BYTES {
150 return Err(anyhow!(
151 "artifact file {} is {} bytes, which exceeds the {} byte limit for \
152 verifier artifacts; refusing to load it",
153 path.display(),
154 claimed_len,
155 MAX_VERIFIER_ARTIFACT_BYTES
156 ));
157 }
158 std::fs::read(path).with_context(|| format!("failed to read artifact file {}", path.display()))
159}
160
161impl WormholeVerifier {
162 pub fn new_from_bytes(verifier_bytes: &[u8], common_bytes: &[u8]) -> anyhow::Result<Self> {
171 for (label, bytes) in [("verifier-only", verifier_bytes), ("common", common_bytes)] {
172 if bytes.len() as u64 > MAX_VERIFIER_ARTIFACT_BYTES {
173 return Err(anyhow!(
174 "{} artifact is {} bytes, which exceeds the {} byte limit for \
175 verifier artifacts; refusing to load it",
176 label,
177 bytes.len(),
178 MAX_VERIFIER_ARTIFACT_BYTES
179 ));
180 }
181 }
182
183 let verifier_hash = keccak256(verifier_bytes);
184 if verifier_hash != CANONICAL_LEAF_VERIFIER_KECCAK256 {
185 return Err(anyhow!(
186 "loaded verifier-only artifact does not match the canonical Wormhole leaf circuit (keccak256={:02x?})",
187 verifier_hash
188 ));
189 }
190
191 let common_hash = keccak256(common_bytes);
192 if common_hash != CANONICAL_LEAF_COMMON_KECCAK256 {
193 return Err(anyhow!(
194 "loaded common artifact does not match the canonical Wormhole leaf circuit (keccak256={:02x?})",
195 common_hash
196 ));
197 }
198
199 let verifier_only = VerifierOnlyCircuitData::from_bytes(verifier_bytes.to_vec())
200 .map_err(|e| anyhow!("failed to deserialize verifier data: {}", e))?;
201
202 let common = CommonCircuitData::from_bytes(common_bytes.to_vec(), &DefaultGateSerializer)
203 .map_err(|e| anyhow!("failed to deserialize common circuit data: {}", e))?;
204
205 Self::ensure_loaded_matches_canonical_leaf_profile(&common)?;
206
207 let circuit_data = VerifierCircuitData {
208 verifier_only,
209 common,
210 };
211
212 Ok(Self { circuit_data })
213 }
214
215 fn ensure_loaded_matches_canonical_leaf_profile(
216 common: &CommonCircuitData<F, D>,
217 ) -> anyhow::Result<()> {
218 let expected = CircuitConfig::standard_recursion_config();
219
220 if expected.security_bits < MIN_LEAF_SECURITY_BITS {
221 return Err(anyhow!(
222 "canonical recursion config provides only {} security bits, below the required minimum of {}",
223 expected.security_bits,
224 MIN_LEAF_SECURITY_BITS
225 ));
226 }
227
228 if common.config != expected {
229 return Err(anyhow!(
230 "loaded verifier circuit config does not match the canonical Wormhole leaf config \
231 (security_bits loaded={}, expected={})",
232 common.config.security_bits,
233 expected.security_bits
234 ));
235 }
236
237 if common.num_public_inputs != PUBLIC_INPUTS_FELTS_LEN {
238 return Err(anyhow!(
239 "loaded verifier common data has {} public inputs, expected {} for the canonical Wormhole leaf circuit",
240 common.num_public_inputs,
241 PUBLIC_INPUTS_FELTS_LEN
242 ));
243 }
244
245 Ok(())
246 }
247
248 #[cfg(feature = "std")]
255 pub fn new_from_files(
256 verifier_data_path: &Path,
257 common_data_path: &Path,
258 ) -> anyhow::Result<Self> {
259 let verifier_bytes = read_artifact_file(verifier_data_path)?;
260 let common_bytes = read_artifact_file(common_data_path)?;
261
262 Self::new_from_bytes(&verifier_bytes, &common_bytes)
263 }
264
265 pub fn verify_ref(&self, proof: &ProofWithPublicInputs<F, C, D>) -> anyhow::Result<()> {
271 self.circuit_data
272 .verify(proof.clone())
273 .map_err(|e| anyhow!("proof verification failed: {}", e))
274 }
275
276 pub fn verify(&self, proof: ProofWithPublicInputs<F, C, D>) -> anyhow::Result<()> {
282 self.verify_ref(&proof)
283 }
284}
285
286#[cfg(all(test, feature = "std"))]
287mod tests {
288 use super::*;
289
290 fn temp_dir(tag: &str) -> std::path::PathBuf {
291 let dir = std::env::temp_dir().join(format!(
292 "qp-verifier-artifact-test-{}-{}",
293 tag,
294 std::process::id()
295 ));
296 let _ = std::fs::remove_dir_all(&dir);
297 std::fs::create_dir_all(&dir).unwrap();
298 dir
299 }
300
301 #[test]
306 fn new_from_bytes_rejects_oversized_slices_before_hashing() {
307 let oversized = vec![0u8; MAX_VERIFIER_ARTIFACT_BYTES as usize + 1];
308
309 let err = WormholeVerifier::new_from_bytes(&oversized, b"irrelevant").unwrap_err();
310 assert!(
311 format!("{err:#}").contains("exceeds the"),
312 "oversized verifier bytes must be rejected by the size cap, got: {err:#}"
313 );
314
315 let err = WormholeVerifier::new_from_bytes(b"irrelevant", &oversized).unwrap_err();
316 assert!(
317 format!("{err:#}").contains("exceeds the"),
318 "oversized common bytes must be rejected by the size cap, got: {err:#}"
319 );
320 }
321
322 #[test]
328 fn new_from_files_rejects_oversized_artifact_before_reading() {
329 let dir = temp_dir("oversized");
330
331 let verifier_path = dir.join("verifier.bin");
332 let common_path = dir.join("common.bin");
333 std::fs::File::create(&verifier_path)
336 .unwrap()
337 .set_len(MAX_VERIFIER_ARTIFACT_BYTES + 1)
338 .unwrap();
339 std::fs::write(&common_path, b"irrelevant").unwrap();
340
341 let err = WormholeVerifier::new_from_files(&verifier_path, &common_path).unwrap_err();
342 assert!(
343 format!("{err:#}").contains("exceeds the"),
344 "oversized artifact must be rejected by the size cap, got: {err:#}"
345 );
346
347 std::fs::remove_dir_all(&dir).unwrap();
348 }
349}