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