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    "292483d5631c28f15613b26bee6cf62a8cc9bbd74a97f375aea89e4dfbf7a10f";
33/// The SHA-256 fingerprint of the `OPRFNullifier` `ZKey`.
34pub const NULLIFIER_ZKEY_FINGERPRINT: &str =
35    "14bd468c7fc6e91e48fa776995c267493845d93648a4c1ee24c2567b18b1795a";
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 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/// Error type for OPRF operations and proof generation.
81#[derive(Debug, thiserror::Error)]
82pub enum ProofError {
83    /// Error originating from `oprf_client`.
84    #[error(transparent)]
85    OprfError(#[from] taceo_oprf::client::Error),
86    /// Errors originating from Groth16 proof generation or verification.
87    #[error(transparent)]
88    ZkError(#[from] Groth16Error),
89    /// Catch-all for other internal errors.
90    #[error(transparent)]
91    InternalError(#[from] eyre::Report),
92}
93
94// ============================================================================
95// Circuit Material Loaders
96// ============================================================================
97
98/// Loads the [`CircomGroth16Material`] for the uniqueness proof (internally also nullifier proof)
99/// from the embedded keys in the binary without caching.
100///
101/// # Returns
102/// The nullifier material.
103///
104/// # Errors
105/// Will return an error if the zkey file cannot be loaded.
106#[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/// Loads the [`CircomGroth16Material`] for the uniqueness proof (internally also query proof)
115/// from the optional zkey file or from embedded keys in the binary.
116///
117/// # Returns
118/// The query material
119///
120/// # Errors
121/// Will return an error if the zkey file cannot be loaded.
122#[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
130/// Loads the [`CircomGroth16Material`] for the nullifier proof from the provided reader.
131///
132/// # Errors
133/// Will return an error if the material cannot be loaded or verified.
134pub 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
141/// Loads the [`CircomGroth16Material`] for the query proof from the provided reader.
142///
143/// # Errors
144/// Will return an error if the material cannot be loaded or verified.
145pub 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
152/// Loads the [`CircomGroth16Material`] for the nullifier proof from the provided paths.
153///
154/// # Panics
155/// Will panic if the material cannot be loaded or verified.
156pub 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
165/// Loads the [`CircomGroth16Material`] for the query proof from the provided paths.
166///
167/// # Errors
168/// Will return an error if the material cannot be loaded or verified.
169pub 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/// Loads the query zkey from embedded bytes,
177/// decompressing and caching it on disk if necessary.
178///
179/// # Arguments
180/// * `query_zkey` - Optional path to the query zkey file. If `None`, will attempt to load from embedded bytes.
181/// * `cache_dir` - Optional directory to cache the uncompressed zkey.
182///
183/// # Returns
184/// The uncompressed query zkey bytes.
185///
186/// # Errors
187/// Will return an error if the zkey file cannot be loaded.
188#[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/// Loads the nullifier zkey from the provided path, or from embedded bytes,
203/// decompressing and caching it on disk if necessary.
204///
205/// # Arguments
206/// * `nullifier_zkey` - Optional path to the nullifier zkey file. If `None`, will attempt to load from embedded bytes.
207/// * `cache_dir` - Optional directory to cache the uncompressed zkey.
208///
209/// # Returns
210/// The uncompressed nullifier zkey bytes.
211///
212/// # Errors
213/// Will return an error if the zkey file cannot be loaded.
214#[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/// Loads an embedded compressed zkey, decompressing and caching it on disk if necessary.
229///
230/// # Arguments
231/// * `cache_dir` - Optional directory to cache the uncompressed zkey.
232/// * `file_name` - The file name to use for caching.
233/// * `bytes` - The compressed zkey bytes.
234///
235/// # Returns
236/// The uncompressed zkey bytes.
237///
238/// # Errors
239/// Will return an error if decompression or file operations fail.
240#[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            // Decompress and cache
263            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// ============================================================================
306// Uniqueness Proof (internally also called nullifier proof) Generation
307// ============================================================================
308
309/// FIXME. Description.
310/// Generates the Groth16 nullifier proof ....
311///
312/// Internally can be a Session Proof or Uniqueness Proof
313///
314/// # Errors
315///
316/// Returns [`ProofError`] if proof generation or verification fails.
317#[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        // 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 (proof, public) = nullifier_material.generate_proof(&nullifier_input, rng)?;
370    nullifier_material.verify_proof(&proof, &public)?;
371
372    let nullifier = public[0];
373
374    // Verify that the computed nullifier matches the OPRF output.
375    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}