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