Skip to main content

world_id_proof/
proof.rs

1//! Internal proof generation for the World ID Protocol.
2//!
3//! Provides functionality for generating ZKPs (zero-knowledge proofs), including
4//! Uniqueness Proofs (internally also called Nullifier Proofs `π2`) and Session Proofs. It
5//! also contains internal proof computation such as Query Proofs `π1`.
6//!
7//! The proof generation workflow for Uniqueness Proofs consists of:
8//! 1. Loading circuit proving material (zkeys and witness graphs)
9//! 2. Signing OPRF queries and generating a Query Proof `π1`
10//! 3. Interacting with OPRF services to obtain challenge responses
11//! 4. Verifying `DLog` equality proofs from OPRF nodes
12//! 5. Generating the final Uniqueness Proof `π2`
13
14use 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
30/// The SHA-256 fingerprint of the `OPRFQuery` `ZKey`.
31pub const QUERY_ZKEY_FINGERPRINT: &str =
32    "616c98c6ba024b5a4015d3ebfd20f6cab12e1e33486080c5167a4bcfac111798";
33/// The SHA-256 fingerprint of the `OPRFNullifier` `ZKey`.
34pub const NULLIFIER_ZKEY_FINGERPRINT: &str =
35    "4247e6bfe1af211e72d3657346802e1af00e6071fb32429a200f9fc0a25a36f9";
36
37/// The SHA-256 fingerprint of the `OPRFQuery` witness graph.
38pub const QUERY_GRAPH_FINGERPRINT: &str =
39    "6b0cb90304c510f9142a555fe2b7cf31b9f68f6f37286f4471fd5d03e91da311";
40/// The SHA-256 fingerprint of the `OPRFNullifier` witness graph.
41pub 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    /// Embedded query witness graph bytes.
63    pub query_graph: Vec<u8>,
64    /// Embedded nullifier witness graph bytes.
65    pub nullifier_graph: Vec<u8>,
66    /// Embedded query zkey bytes (decompressed if `compress-zkeys` is enabled).
67    pub query_zkey: Vec<u8>,
68    /// Embedded nullifier zkey bytes (decompressed if `compress-zkeys` is enabled).
69    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/// Error type for OPRF operations and proof generation.
77#[derive(Debug, thiserror::Error)]
78pub enum ProofError {
79    /// Error originating from `oprf_client`.
80    #[error(transparent)]
81    OprfError(#[from] taceo_oprf::client::Error),
82    /// Errors originating from Groth16 proof generation or verification.
83    #[error(transparent)]
84    ZkError(#[from] Groth16Error),
85    /// Catch-all for other internal errors.
86    #[error(transparent)]
87    InternalError(#[from] eyre::Report),
88}
89
90// ============================================================================
91// Circuit Material Loaders
92// ============================================================================
93
94/// Loads the [`CircomGroth16Material`] for the uniqueness proof (internally also nullifier proof)
95/// from the embedded keys in the binary without caching.
96///
97/// # Returns
98/// The nullifier material.
99///
100/// # Errors
101/// Will return an error if the zkey file cannot be loaded.
102#[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/// Loads the [`CircomGroth16Material`] for the uniqueness proof (internally also query proof)
112/// from the optional zkey file or from embedded keys in the binary.
113///
114/// # Returns
115/// The query material
116///
117/// # Errors
118/// Will return an error if the zkey file cannot be loaded.
119#[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
125/// Loads the [`CircomGroth16Material`] for the nullifier proof from the provided reader.
126///
127/// # Errors
128/// Will return an error if the material cannot be loaded or verified.
129pub 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
136/// Loads the [`CircomGroth16Material`] for the query proof from the provided reader.
137///
138/// # Errors
139/// Will return an error if the material cannot be loaded or verified.
140pub 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
147/// Loads the [`CircomGroth16Material`] for the nullifier proof from the provided paths.
148///
149/// # Panics
150/// Will panic if the material cannot be loaded or verified.
151pub 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
160/// Loads the [`CircomGroth16Material`] for the query proof from the provided paths.
161///
162/// # Errors
163/// Will return an error if the material cannot be loaded or verified.
164pub 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    // Step 1: Decode archive bytes (optional zstd decompression)
193    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    // Step 2: Untar — extract 4 entries by filename
205    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    // Step 3: ARK decompress zkeys if compress-zkeys is active
239    #[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/// ARK-decompresses a zkey.
258#[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// ============================================================================
298// Uniqueness Proof (internally also called nullifier proof) Generation
299// ============================================================================
300
301/// FIXME. Description.
302/// Generates the Groth16 nullifier proof ....
303///
304/// Internally can be a Session Proof or Uniqueness Proof
305///
306/// # Errors
307///
308/// Returns [`ProofError`] if proof generation or verification fails.
309#[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        // The `current_timestamp` constraint in the circuit is used to specify the minimum expiration time for the credential.
357        // The circuit verifies that `current_timestamp < cred_expires_at`.
358        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    // Verify that the computed nullifier matches the OPRF output.
367    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}