1use ark_bn254::Bn254;
15use groth16_material::Groth16Error;
16use rand::{CryptoRng, Rng};
17use std::{io::Read, path::Path};
18use world_id_primitives::{
19 Credential, FieldElement, Nullifier, RequestItem, TREE_DEPTH,
20 circuit_inputs::NullifierProofCircuitInput,
21};
22
23pub use groth16_material::circom::{
24 CircomGroth16Material, CircomGroth16MaterialBuilder, ZkeyError,
25};
26
27#[expect(unused_imports, reason = "used for docs")]
28use world_id_primitives::SessionId;
29
30use crate::oprf_query::FullOprfOutput;
31
32pub mod errors;
33
34pub(crate) const OPRF_PROOF_DS: &[u8] = b"World ID Proof";
35
36pub const QUERY_ZKEY_FINGERPRINT: &str =
38 "616c98c6ba024b5a4015d3ebfd20f6cab12e1e33486080c5167a4bcfac111798";
39pub const NULLIFIER_ZKEY_FINGERPRINT: &str =
41 "4247e6bfe1af211e72d3657346802e1af00e6071fb32429a200f9fc0a25a36f9";
42
43pub const QUERY_GRAPH_FINGERPRINT: &str =
45 "6b0cb90304c510f9142a555fe2b7cf31b9f68f6f37286f4471fd5d03e91da311";
46pub const NULLIFIER_GRAPH_FINGERPRINT: &str =
48 "c1d951716e3b74b72e4ea0429986849cadc43cccc630a7ee44a56a6199a66b9a";
49
50#[cfg(all(feature = "embed-zkeys", not(docsrs)))]
51const CIRCUIT_ARCHIVE: &[u8] = {
52 #[cfg(feature = "zstd-compress-zkeys")]
53 {
54 include_bytes!(concat!(env!("OUT_DIR"), "/circuit_files.tar.zst"))
55 }
56 #[cfg(not(feature = "zstd-compress-zkeys"))]
57 {
58 include_bytes!(concat!(env!("OUT_DIR"), "/circuit_files.tar"))
59 }
60};
61
62#[cfg(all(feature = "embed-zkeys", docsrs))]
63const CIRCUIT_ARCHIVE: &[u8] = &[];
64
65#[cfg(feature = "embed-zkeys")]
66#[derive(Clone, Debug)]
67pub struct EmbeddedCircuitFiles {
68 pub query_graph: Vec<u8>,
70 pub nullifier_graph: Vec<u8>,
72 pub query_zkey: Vec<u8>,
74 pub nullifier_zkey: Vec<u8>,
76}
77
78#[cfg(feature = "embed-zkeys")]
79static CIRCUIT_FILES: std::sync::OnceLock<Result<EmbeddedCircuitFiles, String>> =
80 std::sync::OnceLock::new();
81
82#[derive(Debug, thiserror::Error)]
84pub enum ProofError {
85 #[error(transparent)]
87 OprfError(#[from] taceo_oprf::client::Error),
88 #[error(transparent)]
90 ProofInputError(#[from] errors::ProofInputError),
91 #[error(transparent)]
93 ZkError(#[from] Groth16Error),
94 #[error(transparent)]
96 InternalError(#[from] eyre::Report),
97}
98
99#[cfg(feature = "embed-zkeys")]
112pub fn load_embedded_nullifier_material() -> eyre::Result<CircomGroth16Material> {
113 let files = load_embedded_circuit_files()?;
114 load_nullifier_material_from_reader(
115 files.nullifier_zkey.as_slice(),
116 files.nullifier_graph.as_slice(),
117 )
118}
119
120#[cfg(feature = "embed-zkeys")]
129pub fn load_embedded_query_material() -> eyre::Result<CircomGroth16Material> {
130 let files = load_embedded_circuit_files()?;
131 load_query_material_from_reader(files.query_zkey.as_slice(), files.query_graph.as_slice())
132}
133
134pub fn load_nullifier_material_from_reader(
139 zkey: impl Read,
140 graph: impl Read,
141) -> eyre::Result<CircomGroth16Material> {
142 Ok(build_nullifier_builder().build_from_reader(zkey, graph)?)
143}
144
145pub fn load_query_material_from_reader(
150 zkey: impl Read,
151 graph: impl Read,
152) -> eyre::Result<CircomGroth16Material> {
153 Ok(build_query_builder().build_from_reader(zkey, graph)?)
154}
155
156pub fn load_nullifier_material_from_paths(
161 zkey: impl AsRef<Path>,
162 graph: impl AsRef<Path>,
163) -> CircomGroth16Material {
164 build_nullifier_builder()
165 .build_from_paths(zkey, graph)
166 .expect("works when loading embedded groth16-material")
167}
168
169pub fn load_query_material_from_paths(
174 zkey: impl AsRef<Path>,
175 graph: impl AsRef<Path>,
176) -> eyre::Result<CircomGroth16Material> {
177 Ok(build_query_builder().build_from_paths(zkey, graph)?)
178}
179
180#[cfg(feature = "embed-zkeys")]
181pub fn load_embedded_circuit_files() -> eyre::Result<EmbeddedCircuitFiles> {
182 let files = get_circuit_files()?;
183 Ok(files.clone())
184}
185
186#[cfg(feature = "embed-zkeys")]
187fn get_circuit_files() -> eyre::Result<&'static EmbeddedCircuitFiles> {
188 let files = CIRCUIT_FILES.get_or_init(|| init_circuit_files().map_err(|e| e.to_string()));
189 match files {
190 Ok(files) => Ok(files),
191 Err(err) => Err(eyre::eyre!(err.clone())),
192 }
193}
194
195#[cfg(feature = "embed-zkeys")]
196fn init_circuit_files() -> eyre::Result<EmbeddedCircuitFiles> {
197 use std::io::Read as _;
198
199 use eyre::ContextCompat;
200
201 let tar_bytes: Vec<u8> = {
203 #[cfg(feature = "zstd-compress-zkeys")]
204 {
205 zstd::stream::decode_all(CIRCUIT_ARCHIVE)?
206 }
207 #[cfg(not(feature = "zstd-compress-zkeys"))]
208 {
209 CIRCUIT_ARCHIVE.to_vec()
210 }
211 };
212
213 let mut query_graph = None;
215 let mut nullifier_graph = None;
216 let mut query_zkey = None;
217 let mut nullifier_zkey = None;
218
219 let mut archive = tar::Archive::new(tar_bytes.as_slice());
220 for entry in archive.entries()? {
221 let mut entry = entry?;
222 let path = entry.path()?.to_path_buf();
223 let name = path
224 .file_name()
225 .and_then(|n| n.to_str())
226 .unwrap_or_default();
227
228 let mut buf = Vec::with_capacity(entry.size() as usize);
229 entry.read_to_end(&mut buf)?;
230
231 match name {
232 "OPRFQueryGraph.bin" => query_graph = Some(buf),
233 "OPRFNullifierGraph.bin" => nullifier_graph = Some(buf),
234 "OPRFQuery.arks.zkey" => query_zkey = Some(buf),
235 "OPRFNullifier.arks.zkey" => nullifier_zkey = Some(buf),
236 _ => {}
237 }
238 }
239
240 let query_graph = query_graph.context("OPRFQueryGraph.bin not found in archive")?;
241 let nullifier_graph = nullifier_graph.context("OPRFNullifierGraph.bin not found in archive")?;
242 #[allow(unused_mut)]
243 let mut query_zkey = query_zkey.context("OPRFQuery zkey not found in archive")?;
244 #[allow(unused_mut)]
245 let mut nullifier_zkey = nullifier_zkey.context("OPRFNullifier zkey not found in archive")?;
246
247 #[cfg(feature = "compress-zkeys")]
249 {
250 if let Ok(decompressed) = ark_decompress_zkey(&query_zkey) {
251 query_zkey = decompressed;
252 }
253 if let Ok(decompressed) = ark_decompress_zkey(&nullifier_zkey) {
254 nullifier_zkey = decompressed;
255 }
256 }
257
258 Ok(EmbeddedCircuitFiles {
259 query_graph,
260 nullifier_graph,
261 query_zkey,
262 nullifier_zkey,
263 })
264}
265
266#[cfg(feature = "compress-zkeys")]
268pub fn ark_decompress_zkey(compressed: &[u8]) -> eyre::Result<Vec<u8>> {
269 let zkey = <circom_types::groth16::ArkZkey<Bn254> as ark_serialize::CanonicalDeserialize>::deserialize_with_mode(
270 compressed,
271 ark_serialize::Compress::Yes,
272 ark_serialize::Validate::Yes,
273 )?;
274
275 let mut uncompressed = Vec::new();
276 ark_serialize::CanonicalSerialize::serialize_with_mode(
277 &zkey,
278 &mut uncompressed,
279 ark_serialize::Compress::No,
280 )?;
281 Ok(uncompressed)
282}
283
284fn build_nullifier_builder() -> CircomGroth16MaterialBuilder {
285 CircomGroth16MaterialBuilder::new()
286 .fingerprint_zkey(NULLIFIER_ZKEY_FINGERPRINT.into())
287 .fingerprint_graph(NULLIFIER_GRAPH_FINGERPRINT.into())
288 .bbf_num_2_bits_helper()
289 .bbf_inv()
290 .bbf_legendre()
291 .bbf_sqrt_input()
292 .bbf_sqrt_unchecked()
293}
294
295fn build_query_builder() -> CircomGroth16MaterialBuilder {
296 CircomGroth16MaterialBuilder::new()
297 .fingerprint_zkey(QUERY_ZKEY_FINGERPRINT.into())
298 .fingerprint_graph(QUERY_GRAPH_FINGERPRINT.into())
299 .bbf_num_2_bits_helper()
300 .bbf_inv()
301 .bbf_legendre()
302 .bbf_sqrt_input()
303 .bbf_sqrt_unchecked()
304}
305
306#[allow(clippy::too_many_arguments)]
319pub fn generate_nullifier_proof<R: Rng + CryptoRng>(
320 nullifier_material: &CircomGroth16Material,
321 rng: &mut R,
322 credential: &Credential,
323 credential_sub_blinding_factor: FieldElement,
324 oprf_output: FullOprfOutput,
325 request_item: &RequestItem,
326 session_id: Option<FieldElement>,
327 session_id_r_seed: FieldElement,
328 expires_at_min: u64,
329) -> Result<
330 (
331 ark_groth16::Proof<Bn254>,
332 Vec<ark_babyjubjub::Fq>,
333 Nullifier,
334 ),
335 ProofError,
336> {
337 let cred_signature = credential
338 .signature
339 .clone()
340 .ok_or_else(|| ProofError::InternalError(eyre::eyre!("Credential not signed")))?;
341
342 let nullifier_from_oprf_output = oprf_output.verifiable_oprf_output.output;
343
344 let nullifier_input = NullifierProofCircuitInput::<TREE_DEPTH> {
345 query_input: oprf_output.query_proof_input,
346 issuer_schema_id: credential.issuer_schema_id.into(),
347 cred_pk: credential.issuer.pk,
348 cred_hashes: [*credential.claims_hash()?, *credential.associated_data_hash],
349 cred_genesis_issued_at: credential.genesis_issued_at.into(),
350 cred_genesis_issued_at_min: request_item.genesis_issued_at_min.unwrap_or(0).into(),
351 cred_expires_at: credential.expires_at.into(),
352 cred_id: credential.id.into(),
353 cred_sub_blinding_factor: *credential_sub_blinding_factor,
354 cred_s: cred_signature.s,
355 cred_r: cred_signature.r,
356 id_commitment_r: *session_id_r_seed,
357 id_commitment: *session_id.unwrap_or(FieldElement::ZERO),
358 dlog_e: oprf_output.verifiable_oprf_output.dlog_proof.e(),
359 dlog_s: oprf_output.verifiable_oprf_output.dlog_proof.s(),
360 oprf_pk: oprf_output.verifiable_oprf_output.oprf_public_key.inner(),
361 oprf_response_blinded: oprf_output.verifiable_oprf_output.blinded_response,
362 oprf_response: oprf_output.verifiable_oprf_output.unblinded_response,
363 signal_hash: *request_item.signal_hash(),
364 current_timestamp: expires_at_min.into(),
367 };
368
369 let _ = errors::check_nullifier_input_validity(&nullifier_input)?;
370
371 let (proof, public) = nullifier_material.generate_proof(&nullifier_input, rng)?;
372 nullifier_material.verify_proof(&proof, &public)?;
373
374 if public[0] != nullifier_from_oprf_output {
376 return Err(ProofError::InternalError(eyre::eyre!(
377 "Computed nullifier does not match OPRF output"
378 )));
379 }
380
381 let nullifier: Nullifier = FieldElement::from(public[0]).into();
382
383 Ok((proof, public, nullifier))
384}
385
386#[cfg(all(test, feature = "embed-zkeys"))]
387mod tests {
388 use super::*;
389
390 #[test]
391 fn loads_embedded_circuit_files() {
392 let files = load_embedded_circuit_files().unwrap();
393 assert!(!files.query_graph.is_empty());
394 assert!(!files.nullifier_graph.is_empty());
395 assert!(!files.query_zkey.is_empty());
396 assert!(!files.nullifier_zkey.is_empty());
397 }
398
399 #[test]
400 fn builds_materials_from_embedded_readers() {
401 let files = load_embedded_circuit_files().unwrap();
402 load_query_material_from_reader(files.query_zkey.as_slice(), files.query_graph.as_slice())
403 .unwrap();
404 load_nullifier_material_from_reader(
405 files.nullifier_zkey.as_slice(),
406 files.nullifier_graph.as_slice(),
407 )
408 .unwrap();
409 }
410
411 #[test]
412 fn convenience_embedded_material_loaders_work() {
413 load_embedded_query_material().unwrap();
414 load_embedded_nullifier_material().unwrap();
415 }
416
417 #[cfg(feature = "compress-zkeys")]
418 #[test]
419 fn ark_decompress_zkey_roundtrip() {
420 use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Compress, Validate};
421 use circom_types::{ark_bn254::Bn254, groth16::ArkZkey};
422
423 let files = load_embedded_circuit_files().unwrap();
424 let zkey = ArkZkey::<Bn254>::deserialize_with_mode(
425 files.query_zkey.as_slice(),
426 Compress::No,
427 Validate::Yes,
428 )
429 .unwrap();
430 let mut compressed = Vec::new();
431 zkey.serialize_with_mode(&mut compressed, Compress::Yes)
432 .unwrap();
433
434 let decompressed = ark_decompress_zkey(&compressed).unwrap();
435 assert_eq!(decompressed, files.query_zkey);
436 }
437}