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    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
33/// The SHA-256 fingerprint of the `OPRFQuery` `ZKey`.
34pub const QUERY_ZKEY_FINGERPRINT: &str =
35    "616c98c6ba024b5a4015d3ebfd20f6cab12e1e33486080c5167a4bcfac111798";
36/// The SHA-256 fingerprint of the `OPRFNullifier` `ZKey`.
37pub const NULLIFIER_ZKEY_FINGERPRINT: &str =
38    "4247e6bfe1af211e72d3657346802e1af00e6071fb32429a200f9fc0a25a36f9";
39
40/// The SHA-256 fingerprint of the `OPRFQuery` witness graph.
41pub const QUERY_GRAPH_FINGERPRINT: &str =
42    "6b0cb90304c510f9142a555fe2b7cf31b9f68f6f37286f4471fd5d03e91da311";
43/// The SHA-256 fingerprint of the `OPRFNullifier` witness graph.
44pub 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    /// Embedded query witness graph bytes.
66    pub query_graph: Vec<u8>,
67    /// Embedded nullifier witness graph bytes.
68    pub nullifier_graph: Vec<u8>,
69    /// Embedded query zkey bytes (decompressed if `compress-zkeys` is enabled).
70    pub query_zkey: Vec<u8>,
71    /// Embedded nullifier zkey bytes (decompressed if `compress-zkeys` is enabled).
72    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/// Error type for OPRF operations and proof generation.
80#[derive(Debug, thiserror::Error)]
81pub enum ProofError {
82    /// Error originating from `oprf_client`.
83    #[error(transparent)]
84    OprfError(#[from] taceo_oprf::client::Error),
85    /// Errors originating from proof inputs
86    #[error(transparent)]
87    ProofInputError(#[from] errors::ProofInputError),
88    /// Errors originating from Groth16 proof generation or verification.
89    #[error(transparent)]
90    ZkError(#[from] Groth16Error),
91    /// Catch-all for other internal errors.
92    #[error(transparent)]
93    InternalError(#[from] eyre::Report),
94}
95
96// ============================================================================
97// Circuit Material Loaders
98// ============================================================================
99
100/// Loads the [`CircomGroth16Material`] for the uniqueness proof (internally also nullifier proof)
101/// from the embedded keys in the binary without caching.
102///
103/// # Returns
104/// The nullifier material.
105///
106/// # Errors
107/// Will return an error if the zkey file cannot be loaded.
108#[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/// Loads the [`CircomGroth16Material`] for the uniqueness proof (internally also query proof)
118/// from the optional zkey file or from embedded keys in the binary.
119///
120/// # Returns
121/// The query material
122///
123/// # Errors
124/// Will return an error if the zkey file cannot be loaded.
125#[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
131/// Loads the [`CircomGroth16Material`] for the nullifier proof from the provided reader.
132///
133/// # Errors
134/// Will return an error if the material cannot be loaded or verified.
135pub 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
142/// Loads the [`CircomGroth16Material`] for the query proof from the provided reader.
143///
144/// # Errors
145/// Will return an error if the material cannot be loaded or verified.
146pub 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
153/// Loads the [`CircomGroth16Material`] for the nullifier proof from the provided paths.
154///
155/// # Panics
156/// Will panic if the material cannot be loaded or verified.
157pub 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
166/// Loads the [`CircomGroth16Material`] for the query proof from the provided paths.
167///
168/// # Errors
169/// Will return an error if the material cannot be loaded or verified.
170pub 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    // Step 1: Decode archive bytes (optional zstd decompression)
199    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    // Step 2: Untar — extract 4 entries by filename
211    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    // Step 3: ARK decompress zkeys if compress-zkeys is active
245    #[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/// ARK-decompresses a zkey.
264#[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// ============================================================================
304// Uniqueness Proof (internally also called nullifier proof) Generation
305// ============================================================================
306
307/// FIXME. Description.
308/// Generates the Groth16 nullifier proof ....
309///
310/// Internally can be a Session Proof or Uniqueness Proof
311///
312/// # Errors
313///
314/// Returns [`ProofError`] if proof generation or verification fails.
315#[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        // The `current_timestamp` constraint in the circuit is used to specify the minimum expiration time for the credential.
363        // The circuit verifies that `current_timestamp < cred_expires_at`.
364        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    // Verify that the computed nullifier matches the OPRF output.
375    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}