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 "292483d5631c28f15613b26bee6cf62a8cc9bbd74a97f375aea89e4dfbf7a10f";
33pub const NULLIFIER_ZKEY_FINGERPRINT: &str =
35 "14bd468c7fc6e91e48fa776995c267493845d93648a4c1ee24c2567b18b1795a";
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 QUERY_GRAPH_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/OPRFQueryGraph.bin"));
46
47#[cfg(all(feature = "embed-zkeys", docsrs))]
48const QUERY_GRAPH_BYTES: &[u8] = &[];
49
50#[cfg(all(feature = "embed-zkeys", not(docsrs)))]
51const NULLIFIER_GRAPH_BYTES: &[u8] =
52 include_bytes!(concat!(env!("OUT_DIR"), "/OPRFNullifierGraph.bin"));
53
54#[cfg(all(feature = "embed-zkeys", docsrs))]
55const NULLIFIER_GRAPH_BYTES: &[u8] = &[];
56
57#[cfg(all(feature = "embed-zkeys", not(feature = "compress-zkeys"), not(docsrs)))]
58const QUERY_ZKEY_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/OPRFQuery.arks.zkey"));
59
60#[cfg(all(feature = "compress-zkeys", not(docsrs)))]
61const QUERY_ZKEY_BYTES: &[u8] =
62 include_bytes!(concat!(env!("OUT_DIR"), "/OPRFQuery.arks.zkey.compressed"));
63
64#[cfg(docsrs)]
65const QUERY_ZKEY_BYTES: &[u8] = &[];
66
67#[cfg(all(feature = "embed-zkeys", not(feature = "compress-zkeys"), not(docsrs)))]
68const NULLIFIER_ZKEY_BYTES: &[u8] =
69 include_bytes!(concat!(env!("OUT_DIR"), "/OPRFNullifier.arks.zkey"));
70
71#[cfg(all(feature = "compress-zkeys", not(docsrs)))]
72const NULLIFIER_ZKEY_BYTES: &[u8] = include_bytes!(concat!(
73 env!("OUT_DIR"),
74 "/OPRFNullifier.arks.zkey.compressed"
75));
76
77#[cfg(docsrs)]
78const NULLIFIER_ZKEY_BYTES: &[u8] = &[];
79
80#[derive(Debug, thiserror::Error)]
82pub enum ProofError {
83 #[error(transparent)]
85 OprfError(#[from] taceo_oprf::client::Error),
86 #[error(transparent)]
88 ZkError(#[from] Groth16Error),
89 #[error(transparent)]
91 InternalError(#[from] eyre::Report),
92}
93
94#[cfg(feature = "embed-zkeys")]
107pub fn load_embedded_nullifier_material(
108 cache_dir: Option<impl AsRef<Path>>,
109) -> eyre::Result<CircomGroth16Material> {
110 let nullifier_zkey_bytes = load_embedded_nullifier_zkey(cache_dir)?;
111 Ok(build_nullifier_builder().build_from_bytes(&nullifier_zkey_bytes, NULLIFIER_GRAPH_BYTES)?)
112}
113
114#[cfg(feature = "embed-zkeys")]
123pub fn load_embedded_query_material(
124 cache_dir: Option<impl AsRef<Path>>,
125) -> eyre::Result<CircomGroth16Material> {
126 let query_zkey_bytes = load_embedded_query_zkey(cache_dir)?;
127 Ok(build_query_builder().build_from_bytes(&query_zkey_bytes, QUERY_GRAPH_BYTES)?)
128}
129
130pub fn load_nullifier_material_from_reader(
135 zkey: impl Read,
136 graph: impl Read,
137) -> eyre::Result<CircomGroth16Material> {
138 Ok(build_nullifier_builder().build_from_reader(zkey, graph)?)
139}
140
141pub fn load_query_material_from_reader(
146 zkey: impl Read,
147 graph: impl Read,
148) -> eyre::Result<CircomGroth16Material> {
149 Ok(build_query_builder().build_from_reader(zkey, graph)?)
150}
151
152pub fn load_nullifier_material_from_paths(
157 zkey: impl AsRef<Path>,
158 graph: impl AsRef<Path>,
159) -> CircomGroth16Material {
160 build_nullifier_builder()
161 .build_from_paths(zkey, graph)
162 .expect("works when loading embedded groth16-material")
163}
164
165pub fn load_query_material_from_paths(
170 zkey: impl AsRef<Path>,
171 graph: impl AsRef<Path>,
172) -> eyre::Result<CircomGroth16Material> {
173 Ok(build_query_builder().build_from_paths(zkey, graph)?)
174}
175
176#[allow(unused_variables)]
189#[cfg(feature = "embed-zkeys")]
190fn load_embedded_query_zkey(cache_dir: Option<impl AsRef<Path>>) -> eyre::Result<Vec<u8>> {
191 #[cfg(feature = "compress-zkeys")]
192 {
193 load_embedded_compressed_zkey(cache_dir, "OPRFQuery.arks.zkey", QUERY_ZKEY_BYTES)
194 }
195
196 #[cfg(all(feature = "embed-zkeys", not(feature = "compress-zkeys")))]
197 {
198 Ok(QUERY_ZKEY_BYTES.to_vec())
199 }
200}
201
202#[allow(unused_variables)]
215#[cfg(feature = "embed-zkeys")]
216fn load_embedded_nullifier_zkey(cache_dir: Option<impl AsRef<Path>>) -> eyre::Result<Vec<u8>> {
217 #[cfg(feature = "compress-zkeys")]
218 {
219 load_embedded_compressed_zkey(cache_dir, "OPRFNullifier.arks.zkey", NULLIFIER_ZKEY_BYTES)
220 }
221
222 #[cfg(all(feature = "embed-zkeys", not(feature = "compress-zkeys")))]
223 {
224 Ok(NULLIFIER_ZKEY_BYTES.to_vec())
225 }
226}
227
228#[cfg(feature = "compress-zkeys")]
241fn load_embedded_compressed_zkey(
242 cache_dir: Option<impl AsRef<Path>>,
243 file_name: &str,
244 bytes: &[u8],
245) -> eyre::Result<Vec<u8>> {
246 let compressed = bytes.to_vec();
247 let cache_dir = match cache_dir {
248 Some(dir) => dir.as_ref().to_path_buf(),
249 None => {
250 tracing::warn!(
251 "No cache directory provided for uncompressed zkey, using system temp directory"
252 );
253 let mut dir = std::env::temp_dir();
254 dir.push("world-id-zkey-cache");
255 dir
256 }
257 };
258 let path = cache_dir.join(file_name);
259 match std::fs::read(&path) {
260 Ok(bytes) => Ok(bytes),
261 Err(_) => {
262 let zkey =
264 <circom_types::groth16::ArkZkey<Bn254> as ark_serialize::CanonicalDeserialize>::deserialize_with_mode(
265 compressed.as_slice(),
266 ark_serialize::Compress::Yes,
267 ark_serialize::Validate::Yes,
268 )?;
269
270 let mut uncompressed = Vec::new();
271 ark_serialize::CanonicalSerialize::serialize_with_mode(
272 &zkey,
273 &mut uncompressed,
274 ark_serialize::Compress::No,
275 )?;
276 std::fs::create_dir_all(&cache_dir)?;
277 std::fs::write(&path, &uncompressed)?;
278 Ok(uncompressed)
279 }
280 }
281}
282
283fn build_nullifier_builder() -> CircomGroth16MaterialBuilder {
284 CircomGroth16MaterialBuilder::new()
285 .fingerprint_zkey(NULLIFIER_ZKEY_FINGERPRINT.into())
286 .fingerprint_graph(NULLIFIER_GRAPH_FINGERPRINT.into())
287 .bbf_num_2_bits_helper()
288 .bbf_inv()
289 .bbf_legendre()
290 .bbf_sqrt_input()
291 .bbf_sqrt_unchecked()
292}
293
294fn build_query_builder() -> CircomGroth16MaterialBuilder {
295 CircomGroth16MaterialBuilder::new()
296 .fingerprint_zkey(QUERY_ZKEY_FINGERPRINT.into())
297 .fingerprint_graph(QUERY_GRAPH_FINGERPRINT.into())
298 .bbf_num_2_bits_helper()
299 .bbf_inv()
300 .bbf_legendre()
301 .bbf_sqrt_input()
302 .bbf_sqrt_unchecked()
303}
304
305#[allow(clippy::too_many_arguments)]
318pub fn generate_nullifier_proof<R: Rng + CryptoRng>(
319 nullifier_material: &CircomGroth16Material,
320 rng: &mut R,
321 credential: &Credential,
322 credential_sub_blinding_factor: FieldElement,
323 oprf_nullifier: OprfNullifier,
324 request_item: &RequestItem,
325 session_id: Option<FieldElement>,
326 session_id_r_seed: FieldElement,
327 expires_at_min: u64,
328) -> Result<
329 (
330 ark_groth16::Proof<Bn254>,
331 Vec<ark_babyjubjub::Fq>,
332 ark_babyjubjub::Fq,
333 ),
334 ProofError,
335> {
336 let cred_signature = credential
337 .signature
338 .clone()
339 .ok_or_else(|| ProofError::InternalError(eyre::eyre!("Credential not signed")))?;
340
341 let nullifier_input = NullifierProofCircuitInput::<TREE_DEPTH> {
342 query_input: oprf_nullifier.query_proof_input,
343 issuer_schema_id: credential.issuer_schema_id.into(),
344 cred_pk: credential.issuer.pk,
345 cred_hashes: [*credential.claims_hash()?, *credential.associated_data_hash],
346 cred_genesis_issued_at: credential.genesis_issued_at.into(),
347 cred_genesis_issued_at_min: request_item.genesis_issued_at_min.unwrap_or(0).into(),
348 cred_expires_at: credential.expires_at.into(),
349 cred_id: credential.id.into(),
350 cred_sub_blinding_factor: *credential_sub_blinding_factor,
351 cred_s: cred_signature.s,
352 cred_r: cred_signature.r,
353 id_commitment_r: *session_id_r_seed,
354 id_commitment: *session_id.unwrap_or(FieldElement::ZERO),
355 dlog_e: oprf_nullifier.verifiable_oprf_output.dlog_proof.e,
356 dlog_s: oprf_nullifier.verifiable_oprf_output.dlog_proof.s,
357 oprf_pk: oprf_nullifier
358 .verifiable_oprf_output
359 .oprf_public_key
360 .inner(),
361 oprf_response_blinded: oprf_nullifier.verifiable_oprf_output.blinded_response,
362 oprf_response: oprf_nullifier.verifiable_oprf_output.unblinded_response,
363 signal_hash: *request_item.signal_hash(),
364 current_timestamp: expires_at_min.into(),
367 };
368
369 let (proof, public) = nullifier_material.generate_proof(&nullifier_input, rng)?;
370 nullifier_material.verify_proof(&proof, &public)?;
371
372 let nullifier = public[0];
373
374 if nullifier != oprf_nullifier.verifiable_oprf_output.output {
376 return Err(ProofError::InternalError(eyre::eyre!(
377 "Computed nullifier does not match OPRF output"
378 )));
379 }
380
381 Ok((proof, public, nullifier))
382}