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, 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
36/// The SHA-256 fingerprint of the `OPRFQuery` `ZKey`.
37pub const QUERY_ZKEY_FINGERPRINT: &str =
38    "616c98c6ba024b5a4015d3ebfd20f6cab12e1e33486080c5167a4bcfac111798";
39/// The SHA-256 fingerprint of the `OPRFNullifier` `ZKey`.
40pub const NULLIFIER_ZKEY_FINGERPRINT: &str =
41    "4247e6bfe1af211e72d3657346802e1af00e6071fb32429a200f9fc0a25a36f9";
42
43/// The SHA-256 fingerprint of the `OPRFQuery` witness graph.
44pub const QUERY_GRAPH_FINGERPRINT: &str =
45    "6b0cb90304c510f9142a555fe2b7cf31b9f68f6f37286f4471fd5d03e91da311";
46/// The SHA-256 fingerprint of the `OPRFNullifier` witness graph.
47pub 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    /// Embedded query witness graph bytes.
69    pub query_graph: Vec<u8>,
70    /// Embedded nullifier witness graph bytes.
71    pub nullifier_graph: Vec<u8>,
72    /// Embedded query zkey bytes (decompressed if `compress-zkeys` is enabled).
73    pub query_zkey: Vec<u8>,
74    /// Embedded nullifier zkey bytes (decompressed if `compress-zkeys` is enabled).
75    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/// Error type for OPRF operations and proof generation.
83#[derive(Debug, thiserror::Error)]
84pub enum ProofError {
85    /// Error originating from `oprf_client`.
86    #[error(transparent)]
87    OprfError(#[from] taceo_oprf::client::Error),
88    /// Errors originating from proof inputs
89    #[error(transparent)]
90    ProofInputError(#[from] errors::ProofInputError),
91    /// Errors originating from Groth16 proof generation or verification.
92    #[error(transparent)]
93    ZkError(#[from] Groth16Error),
94    /// Catch-all for other internal errors.
95    #[error(transparent)]
96    InternalError(#[from] eyre::Report),
97}
98
99// ============================================================================
100// Circuit Material Loaders
101// ============================================================================
102
103/// Loads the [`CircomGroth16Material`] for the uniqueness proof (internally also nullifier proof)
104/// from the embedded keys in the binary without caching.
105///
106/// # Returns
107/// The nullifier material.
108///
109/// # Errors
110/// Will return an error if the zkey file cannot be loaded.
111#[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/// Loads the [`CircomGroth16Material`] for the uniqueness proof (internally also query proof)
121/// from the optional zkey file or from embedded keys in the binary.
122///
123/// # Returns
124/// The query material
125///
126/// # Errors
127/// Will return an error if the zkey file cannot be loaded.
128#[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
134/// Loads the [`CircomGroth16Material`] for the nullifier proof from the provided reader.
135///
136/// # Errors
137/// Will return an error if the material cannot be loaded or verified.
138pub 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
145/// Loads the [`CircomGroth16Material`] for the query proof from the provided reader.
146///
147/// # Errors
148/// Will return an error if the material cannot be loaded or verified.
149pub 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
156/// Loads the [`CircomGroth16Material`] for the nullifier proof from the provided paths.
157///
158/// # Panics
159/// Will panic if the material cannot be loaded or verified.
160pub 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
169/// Loads the [`CircomGroth16Material`] for the query proof from the provided paths.
170///
171/// # Errors
172/// Will return an error if the material cannot be loaded or verified.
173pub 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    // Step 1: Decode archive bytes (optional zstd decompression)
202    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    // Step 2: Untar — extract 4 entries by filename
214    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    // Step 3: ARK decompress zkeys if compress-zkeys is active
248    #[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/// ARK-decompresses a zkey.
267#[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// ============================================================================
307// Uniqueness Proof (internally also called nullifier proof) Generation
308// ============================================================================
309
310/// FIXME. Description.
311/// Generates the Groth16 nullifier proof ....
312///
313/// Internally can be a Session Proof or Uniqueness Proof
314///
315/// # Errors
316///
317/// Returns [`ProofError`] if proof generation or verification fails.
318#[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        // The `current_timestamp` constraint in the circuit is used to specify the minimum expiration time for the credential.
365        // The circuit verifies that `current_timestamp < cred_expires_at`.
366        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    // Verify that the computed nullifier matches the OPRF output.
375    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}