Skip to main content

world_id_proof/proof/
errors.rs

1//! This module contains error types and validation functions for World ID proof inputs.
2//!
3//! These are intended to assist in producing more helpful error messages for a given proof.
4//! If the circuits change in any way, these checks may also need to be updated to match the new logic.
5use ark_bn254::Fr;
6use ark_ec::{AffineRepr, CurveGroup};
7use ark_ff::{PrimeField, Zero};
8use eddsa_babyjubjub::EdDSAPublicKey;
9use taceo_oprf::core::{dlog_equality::DLogEqualityProof, oprf::BlindingFactor};
10use world_id_primitives::{
11    AuthenticatorPublicKeySet, FieldElement, MAX_AUTHENTICATOR_KEYS, merkle::MerkleInclusionProof,
12};
13
14use crate::circuit_inputs::{NullifierProofCircuitInput, QueryProofCircuitInput};
15
16type BaseField = ark_babyjubjub::Fq;
17type Affine = ark_babyjubjub::EdwardsAffine;
18
19#[derive(Debug, thiserror::Error)]
20/// Errors that can occur when validating the inputs for a single World ID proof.
21pub enum ProofInputError {
22    /// The specified Merkle tree depth is invalid.
23    #[error("The specified Merkle tree depth is invalid (expected: {expected}, got: {is}).")]
24    InvalidMerkleTreeDepth {
25        /// Expected depth.
26        expected: usize,
27        /// Actual depth.
28        is: BaseField,
29    },
30    /// The set of authenticator public keys is invalid.
31    #[error("The set of authenticator public keys is invalid.")]
32    InvalidAuthenticatorPublicKeySet,
33    /// The provided Merkle tree **inclusion proof** is invalid (the root may or may not be valid for the `WorldIDRegistry` tree)
34    #[error("The provided Merkle tree inclusion proof is invalid.")]
35    InvalidMerkleTreeInclusionProof,
36    /// The signature from the authenticator for the request is invalid.
37    #[error("The signature over the nonce and RP ID is invalid.")]
38    InvalidQuerySignature,
39    /// The provided blinding factor is invalid, or the sub is incorrect.
40    #[error("The provided blinding factor is invalid.")]
41    InvalidBlindingFactor,
42    /// The provided credential has expired.
43    #[error(
44        "The provided credential has expired (expires_at: {expires_at}, check_timestamp: {current_timestamp})."
45    )]
46    CredentialExpired {
47        /// Current timestamp.
48        current_timestamp: u64,
49        /// Expiration timestamp.
50        expires_at: u64,
51    },
52    /// The provided credential genesis issue timestamp is expired.
53    #[error(
54        "The provided credential has a genesis issued at date that is too old (genesis_issued_at: {genesis_issued_at}, check_timestamp: {genesis_issued_at_min})."
55    )]
56    CredentialGenesisExpired {
57        /// Minimum Issue date.
58        genesis_issued_at_min: u64,
59        /// Genesis issue timestamp.
60        genesis_issued_at: u64,
61    },
62    /// A value is out of bounds.
63    #[error("The value '{name}' is out of bounds (got: {is}, limit: {limit}).")]
64    ValueOutOfBounds {
65        /// Name of the value for error message.
66        name: &'static str,
67        /// Actual value.
68        is: BaseField,
69        /// Upper limit, not inclusive.
70        limit: BaseField,
71    },
72    /// The credential signature is invalid. This signals the issued credential is invalid.
73    #[error("The credential signature is invalid for the given issuer public key.")]
74    InvalidCredentialSignature,
75    /// The provided point is not a valid point in the prime-order subgroup of the `BabyJubJub` curve.
76    #[error(
77        "The provided point '{name}' is not a valid point in the prime-order subgroup of the BabyJubJub curve."
78    )]
79    InvalidBabyJubJubPoint {
80        /// Name of the point for error message.
81        name: &'static str,
82    },
83    /// The provided OPRF proof is invalid.
84    #[error("The provided OPRF DlogEquality proof is invalid.")]
85    InvalidOprfProof,
86    /// The provided unblinded OPRF response point is invalid.
87    #[error("The provided unblinded OPRF response point is invalid.")]
88    InvalidOprfResponse,
89    /// The provided session ID commitment is invalid.
90    #[error(
91        "The provided session ID commitment is invalid for the given id and session id randomness."
92    )]
93    InvalidSessionId,
94    /// The proof request is expired.
95    #[error(
96        "The provided proof request has expired (expires_at: {expires_at}, check_timestamp: {current_timestamp})."
97    )]
98    ProofRequestExpired {
99        /// Current timestamp.
100        current_timestamp: u64,
101        /// Expiration timestamp.
102        expires_at: u64,
103    },
104    /// The proof's expires_at is greater than the created_at.
105    #[error("The proof's expires_at {expires_at} happens before the created_at {created_at}.")]
106    InvalidExpiresAt { created_at: u64, expires_at: u64 },
107}
108
109/// This method checks the validity of the input parameters by emulating the operations that are proved in ZK and raising Errors that would result in an invalid proof.
110///
111/// Returns the blinded OPRF query point if everything is ok.
112///
113/// # Errors
114/// This function will return a [`ProofInputError`] if any of the checks fail.
115/// The `Display` implementation of this error can be used to get a human-readable error message on which parts of the input were invalid.
116pub fn check_query_input_validity<const TREE_DEPTH: usize>(
117    inputs: &QueryProofCircuitInput<TREE_DEPTH>,
118) -> Result<Affine, ProofInputError> {
119    // 1. Check that the depth is within bounds.
120    if inputs.depth != BaseField::new((TREE_DEPTH as u64).into()) {
121        return Err(ProofInputError::InvalidMerkleTreeDepth {
122            expected: TREE_DEPTH,
123            is: inputs.depth,
124        });
125    }
126    // 2. Check the merkle proof is valid
127    // Check the Merkle tree idx is valid.
128    let idx_u64 = u64::try_from(FieldElement::from(inputs.mt_index)).map_err(|_| {
129        ProofInputError::ValueOutOfBounds {
130            name: "Merkle tree index",
131            is: inputs.mt_index,
132            limit: BaseField::new((1u64 << TREE_DEPTH).into()),
133        }
134    })?;
135    if idx_u64 >= (1u64 << TREE_DEPTH) {
136        return Err(ProofInputError::ValueOutOfBounds {
137            name: "Merkle tree index",
138            is: inputs.mt_index,
139            limit: BaseField::new((1u64 << TREE_DEPTH).into()),
140        });
141    }
142
143    // Build the leaf from the PKs.
144    let pk_set = AuthenticatorPublicKeySet::new(
145        inputs
146            .pk
147            .iter()
148            .map(|&x| EdDSAPublicKey { pk: x })
149            .collect(),
150    )
151    .map_err(|_| ProofInputError::InvalidAuthenticatorPublicKeySet)?;
152    let pk_set_hash = pk_set.leaf_hash();
153    let merkle_tree_inclusion_proof = MerkleInclusionProof::new(
154        FieldElement::from(inputs.merkle_root),
155        idx_u64,
156        inputs.siblings.map(FieldElement::from),
157    );
158    if !merkle_tree_inclusion_proof.is_valid(FieldElement::from(pk_set_hash)) {
159        return Err(ProofInputError::InvalidMerkleTreeInclusionProof);
160    }
161
162    // 3. Check that the signature is valid.
163    let pk_index_usize = usize::try_from(FieldElement::from(inputs.pk_index)).map_err(|_| {
164        ProofInputError::ValueOutOfBounds {
165            name: "Authenticator PubKey index",
166            is: inputs.pk_index,
167            limit: BaseField::new((MAX_AUTHENTICATOR_KEYS as u64).into()),
168        }
169    })?;
170    let pk = pk_set
171        .get(pk_index_usize)
172        .ok_or_else(|| ProofInputError::ValueOutOfBounds {
173            name: "Authenticator PubKey index",
174            is: inputs.pk_index,
175            limit: BaseField::new((MAX_AUTHENTICATOR_KEYS as u64).into()),
176        })?;
177
178    if !inputs.r.is_on_curve() || !inputs.r.is_in_correct_subgroup_assuming_on_curve() {
179        return Err(ProofInputError::InvalidBabyJubJubPoint {
180            name: "Query Signature R",
181        });
182    }
183    if !pk.pk.is_on_curve() || !pk.pk.is_in_correct_subgroup_assuming_on_curve() {
184        return Err(ProofInputError::InvalidBabyJubJubPoint {
185            name: "Authenticator Public Key",
186        });
187    }
188
189    let _rp_id_u64 = u64::try_from(FieldElement::from(inputs.rp_id)).map_err(|_| {
190        ProofInputError::ValueOutOfBounds {
191            name: "RP Id",
192            is: inputs.rp_id,
193            limit: BaseField::new(u64::MAX.into()),
194        }
195    })?;
196    let query = world_id_primitives::authenticator::oprf_query_digest(
197        idx_u64,
198        FieldElement::from(inputs.action),
199        FieldElement::from(inputs.rp_id),
200    );
201    let signature = eddsa_babyjubjub::EdDSASignature {
202        r: inputs.r,
203        s: inputs.s,
204    };
205
206    if !pk.verify(*query, &signature) {
207        return Err(ProofInputError::InvalidQuerySignature);
208    }
209
210    let blinding_factor = BlindingFactor::from_scalar(inputs.beta)
211        .map_err(|_| ProofInputError::InvalidBlindingFactor)?;
212    let query_point = taceo_oprf::core::oprf::client::blind_query(*query, blinding_factor);
213
214    Ok(query_point.blinded_query())
215}
216
217/// This method checks the validity of the input parameters by emulating the operations that are proved in ZK and raising Errors that would result in an invalid proof.
218///
219/// Returns the computed nullifier if everything is ok.
220///
221/// # Errors
222/// This function will return a [`ProofInputError`] if any of the checks fail.
223/// The `Display` implementation of this error can be used to get a human-readable error message on which parts of the input were invalid.
224#[expect(
225    clippy::too_many_lines,
226    reason = "necessary checks for input validity should be in one function"
227)]
228pub fn check_nullifier_input_validity<const TREE_DEPTH: usize>(
229    inputs: &NullifierProofCircuitInput<TREE_DEPTH>,
230) -> Result<FieldElement, ProofInputError> {
231    // 1. Check the validity of the query input.
232    let blinded_query = check_query_input_validity(&inputs.query_input)?;
233
234    // 2. Credential validity checks
235    // Check timestamps are within bounds.
236    let current_timestamp_u64 = u64::try_from(FieldElement::from(inputs.current_timestamp))
237        .map_err(|_| ProofInputError::ValueOutOfBounds {
238            name: "current timestamp",
239            is: inputs.current_timestamp,
240            limit: BaseField::new(u64::MAX.into()),
241        })?;
242    let credential_expires_at_u64 = u64::try_from(FieldElement::from(inputs.cred_expires_at))
243        .map_err(|_| ProofInputError::ValueOutOfBounds {
244            name: "credential expiry timestamp",
245            is: inputs.cred_expires_at,
246            limit: BaseField::new(u64::MAX.into()),
247        })?;
248    // Check that the credential has not expired.
249    if credential_expires_at_u64 <= current_timestamp_u64 {
250        return Err(ProofInputError::CredentialExpired {
251            current_timestamp: current_timestamp_u64,
252            expires_at: credential_expires_at_u64,
253        });
254    }
255    // Genesis checks
256    let genesis_issued_at_u64 = u64::try_from(FieldElement::from(inputs.cred_genesis_issued_at))
257        .map_err(|_| ProofInputError::ValueOutOfBounds {
258            name: "credential genesis issued at",
259            is: inputs.cred_genesis_issued_at,
260            limit: BaseField::new(u64::MAX.into()),
261        })?;
262    let genesis_issued_at_min_u64 =
263        u64::try_from(FieldElement::from(inputs.cred_genesis_issued_at_min)).map_err(|_| {
264            ProofInputError::ValueOutOfBounds {
265                name: "credential genesis issued at minimum bound",
266                is: inputs.cred_genesis_issued_at_min,
267                limit: BaseField::new(u64::MAX.into()),
268            }
269        })?;
270    if genesis_issued_at_min_u64 > genesis_issued_at_u64 {
271        return Err(ProofInputError::CredentialGenesisExpired {
272            genesis_issued_at_min: genesis_issued_at_min_u64,
273            genesis_issued_at: genesis_issued_at_u64,
274        });
275    }
276
277    let blinded_subject = sub(
278        FieldElement::from(inputs.query_input.mt_index),
279        FieldElement::from(inputs.cred_sub_blinding_factor),
280    );
281
282    let cred_hash = hash_credential(
283        FieldElement::from(inputs.issuer_schema_id),
284        blinded_subject,
285        FieldElement::from(inputs.cred_genesis_issued_at),
286        FieldElement::from(inputs.cred_expires_at),
287        FieldElement::from(inputs.cred_hashes[0]),
288        FieldElement::from(inputs.cred_hashes[1]),
289        FieldElement::from(inputs.cred_id),
290    );
291    let pk = EdDSAPublicKey { pk: inputs.cred_pk };
292
293    let signature = eddsa_babyjubjub::EdDSASignature {
294        r: inputs.cred_r,
295        s: inputs.cred_s,
296    };
297
298    if !inputs.cred_r.is_on_curve() || !inputs.cred_r.is_in_correct_subgroup_assuming_on_curve() {
299        return Err(ProofInputError::InvalidBabyJubJubPoint {
300            name: "Credential Signature R",
301        });
302    }
303    if !pk.pk.is_on_curve() || !pk.pk.is_in_correct_subgroup_assuming_on_curve() {
304        return Err(ProofInputError::InvalidBabyJubJubPoint {
305            name: "Credential Public Key",
306        });
307    }
308
309    if !pk.verify(*cred_hash, &signature) {
310        return Err(ProofInputError::InvalidCredentialSignature);
311    }
312
313    // 3. Dlog Equality proof checks
314    if !inputs.oprf_pk.is_on_curve() || !inputs.oprf_pk.is_in_correct_subgroup_assuming_on_curve() {
315        return Err(ProofInputError::InvalidBabyJubJubPoint {
316            name: "OPRF Public Key",
317        });
318    }
319    if !inputs.oprf_response_blinded.is_on_curve()
320        || !inputs
321            .oprf_response_blinded
322            .is_in_correct_subgroup_assuming_on_curve()
323    {
324        return Err(ProofInputError::InvalidBabyJubJubPoint {
325            name: "OPRF Blinded Response",
326        });
327    }
328
329    // check dlog eq proof is valid
330    let dlog_proof = DLogEqualityProof::new(inputs.dlog_e, inputs.dlog_s);
331    dlog_proof
332        .verify(
333            inputs.oprf_pk,
334            blinded_query,
335            inputs.oprf_response_blinded,
336            Affine::generator(),
337        )
338        .map_err(|_| ProofInputError::InvalidOprfProof)?;
339
340    // check that the unblinded response is correct
341    if !inputs.oprf_response.is_on_curve()
342        || !inputs
343            .oprf_response
344            .is_in_correct_subgroup_assuming_on_curve()
345    {
346        return Err(ProofInputError::InvalidBabyJubJubPoint {
347            name: "OPRF Unblinded Response",
348        });
349    }
350    let expected_blinded_response = (inputs.oprf_response * inputs.query_input.beta).into_affine();
351    if expected_blinded_response != inputs.oprf_response_blinded {
352        return Err(ProofInputError::InvalidOprfResponse);
353    }
354
355    // check that session_id commitment is correct
356    if !inputs.id_commitment.is_zero() {
357        let expected_commitment = session_id_commitment(
358            FieldElement::from(inputs.query_input.mt_index),
359            FieldElement::from(inputs.id_commitment_r),
360        );
361        if expected_commitment != FieldElement::from(inputs.id_commitment) {
362            return Err(ProofInputError::InvalidSessionId);
363        }
364    }
365
366    // 4. Compute the nullifier
367    let nullifier = oprf_finalize_hash(
368        *world_id_primitives::authenticator::oprf_query_digest(
369            #[expect(
370                clippy::missing_panics_doc,
371                reason = "checked in check_query_input_validity"
372            )]
373            u64::try_from(FieldElement::from(inputs.query_input.mt_index)).unwrap(),
374            FieldElement::from(inputs.query_input.action),
375            FieldElement::from(inputs.query_input.rp_id),
376        ),
377        inputs.oprf_response,
378    );
379
380    Ok(nullifier)
381}
382
383// Helper functions to recompute various hashes used in the circuit
384
385// Recompute the blinded subject, copied from credential
386fn sub(leaf_index: FieldElement, blinding_factor: FieldElement) -> FieldElement {
387    let sub_ds = Fr::from_be_bytes_mod_order(b"H_CS(id, r)");
388    let mut input = [sub_ds, *leaf_index, *blinding_factor];
389    poseidon2::bn254::t3::permutation_in_place(&mut input);
390    input[1].into()
391}
392// Recompute the OPRF finalization hash
393fn oprf_finalize_hash(query: BaseField, oprf_response: Affine) -> FieldElement {
394    let finalize_ds = Fr::from_be_bytes_mod_order(super::OPRF_PROOF_DS);
395    let mut input = [finalize_ds, query, oprf_response.x, oprf_response.y];
396    poseidon2::bn254::t4::permutation_in_place(&mut input);
397    input[1].into()
398}
399
400// Recompute the session_id_commitment
401fn session_id_commitment(user_id: FieldElement, commitment_rand: FieldElement) -> FieldElement {
402    let sub_ds = Fr::from_be_bytes_mod_order(b"H(id, r)");
403    let mut input = [sub_ds, *user_id, *commitment_rand];
404    poseidon2::bn254::t3::permutation_in_place(&mut input);
405    input[1].into()
406}
407
408// Recompute the credential hash, copied from credential
409fn hash_credential(
410    issuer_schema_id: FieldElement,
411    sub: FieldElement,
412    genesis_issued_at: FieldElement,
413    expires_at: FieldElement,
414    claims_hash: FieldElement,
415    associated_data_commitment: FieldElement,
416    id: FieldElement,
417) -> FieldElement {
418    let cred_ds = Fr::from_be_bytes_mod_order(b"POSEIDON2+EDDSA-BJJ");
419    let mut input = [
420        cred_ds,
421        *issuer_schema_id,
422        *sub,
423        *genesis_issued_at,
424        *expires_at,
425        *claims_hash,
426        *associated_data_commitment,
427        *id,
428    ];
429    poseidon2::bn254::t8::permutation_in_place(&mut input);
430    input[1].into()
431}
432
433#[cfg(test)]
434mod tests {
435    use crate::circuit_inputs::{NullifierProofCircuitInput, QueryProofCircuitInput};
436    use ark_ec::twisted_edwards::Affine;
437    use std::str::FromStr;
438
439    use crate::proof::errors::{check_nullifier_input_validity, check_query_input_validity};
440
441    // gotten these values by `dbg`-ing the struct in the e2e_authenticator_generate test
442    fn get_valid_query_proof_input() -> QueryProofCircuitInput<30> {
443        QueryProofCircuitInput {
444            pk: [Affine {
445                x: ark_babyjubjub::Fq::from_str(
446                    "19037598474602150174935475944965340829216795940473064039209388058233204431288",
447                ).unwrap(),
448                y: ark_babyjubjub::Fq::from_str(
449                    "3549932221586364715003722955756497910920276078443163728621283280434115857197",
450                ).unwrap(),
451            },
452                Affine::zero(),
453                Affine::zero(),
454                Affine::zero(),
455                Affine::zero(),
456                Affine::zero(),
457                Affine::zero(),
458            ],
459            pk_index: ark_bn254::Fr::from(0u64),
460            s: ark_babyjubjub::Fr::from_str(
461                "2692248185200295468055279425612708965310378163906753799023551825366269352327",
462            ).unwrap(),
463            r: Affine {
464               x: ark_babyjubjub::Fq::from_str(
465                    "14689596469778385278298478829656243946283084496217945909620117398922933730711",
466                ).unwrap(),
467                y: ark_babyjubjub::Fq::from_str(
468                    "4424830738973486800075394160997493242162871494907432163152597205147606706197",
469                ).unwrap(),
470            },
471            merkle_root: ark_bn254::Fr::from_str("4959814736111706042728533661656003495359474679272202023690954858781105690707").unwrap(),
472            depth: ark_babyjubjub::Fq::from(30u64),
473            mt_index: ark_bn254::Fr::from(1u64),
474            siblings: [
475                    ark_bn254::Fr::from_str("0").unwrap(),
476                    ark_bn254::Fr::from_str("15621590199821056450610068202457788725601603091791048810523422053872049975191").unwrap(),
477                    ark_bn254::Fr::from_str("15180302612178352054084191513289999058431498575847349863917170755410077436260").unwrap(),
478                    ark_bn254::Fr::from_str("20846426933296943402289409165716903143674406371782261099735847433924593192150").unwrap(),
479                    ark_bn254::Fr::from_str("19570709311100149041770094415303300085749902031216638721752284824736726831172").unwrap(),
480                    ark_bn254::Fr::from_str("11737142173000203701607979434185548337265641794352013537668027209469132654026").unwrap(),
481                    ark_bn254::Fr::from_str("11865865012735342650993929214218361747705569437250152833912362711743119784159").unwrap(),
482                    ark_bn254::Fr::from_str("1493463551715988755902230605042557878234810673525086316376178495918903796315").unwrap(),
483                    ark_bn254::Fr::from_str("18746103596419850001763894956142528089435746267438407061601783590659355049966").unwrap(),
484                    ark_bn254::Fr::from_str("21234194473503024590374857258930930634542887619436018385581872843343250130100").unwrap(),
485                    ark_bn254::Fr::from_str("14681119568252857310414189897145410009875739166689283501408363922419813627484").unwrap(),
486                    ark_bn254::Fr::from_str("13243470632183094581890559006623686685113540193867211988709619438324105679244").unwrap(),
487                    ark_bn254::Fr::from_str("19463898140191333844443019106944343282402694318119383727674782613189581590092").unwrap(),
488                    ark_bn254::Fr::from_str("10565902370220049529800497209344287504121041033501189980624875736992201671117").unwrap(),
489                    ark_bn254::Fr::from_str("5560307625408070902174028041423028597194394554482880015024167821933869023078").unwrap(),
490                    ark_bn254::Fr::from_str("20576730574720116265513866548855226316241518026808984067485384181494744706390").unwrap(),
491                    ark_bn254::Fr::from_str("11166760821615661136366651998133963805984915741187325490784169611245269155689").unwrap(),
492                    ark_bn254::Fr::from_str("13692603500396323648417392244466291089928913430742736835590182936663435788822").unwrap(),
493                    ark_bn254::Fr::from_str("11129674755567463025028188404867541558752927519269975708924528737249823830641").unwrap(),
494                    ark_bn254::Fr::from_str("6673535049007525806710184801639542254440636510496168661971704157154828514023").unwrap(),
495                    ark_bn254::Fr::from_str("7958154589163466663626421142270206662020519181323839780192984613274682930816").unwrap(),
496                    ark_bn254::Fr::from_str("3739156991379607404516753076057250171966250101655747790592556040569841550790").unwrap(),
497                    ark_bn254::Fr::from_str("1334107297020502384420211493664486465203492095766400031330900935069700302301").unwrap(),
498                    ark_bn254::Fr::from_str("20357028769054354174264046872903423695314313082869184437966002491602414517674").unwrap(),
499                    ark_bn254::Fr::from_str("19392290367394672558538719012722289280213395590510602524366987685302929990731").unwrap(),
500                    ark_bn254::Fr::from_str("7360502715619830055199267117332475946442427205382059394111067387016428818088").unwrap(),
501                    ark_bn254::Fr::from_str("9629177338475347225553791169746168712988898028547587350296027054067573957412").unwrap(),
502                    ark_bn254::Fr::from_str("21877160135037839571797468541807904053886800340144060811298025652177410263004").unwrap(),
503                    ark_bn254::Fr::from_str("7105691694342706282901391345307729036900705570482804586768449537652208350743").unwrap(),
504                    ark_bn254::Fr::from_str("15888057581779748293164452094398990053773731478520540058125130669204703869637").unwrap(),
505            ],
506            beta: ark_babyjubjub::Fr::from_str("1277277022932719396321614946989807194659268059729440522321681213750340643042").unwrap(),
507            rp_id: ark_bn254::Fr::from_str("14631649082411674499").unwrap(),
508            action: ark_bn254::Fr::from_str("8982441576518976929447725179565370305223105654688049122733783421407497941726").unwrap(),
509            nonce: ark_bn254::Fr::from_str("8530676162050357218814694371816107906694725175836943927290214963954696613748").unwrap(),
510        }
511    }
512
513    #[test]
514    fn test_valid_query_proof_input() {
515        let inputs = get_valid_query_proof_input();
516        let _ = check_query_input_validity(&inputs).unwrap();
517    }
518
519    #[test]
520    fn test_invalid_query_proof_input() {
521        let inputs = get_valid_query_proof_input();
522        {
523            let mut inputs = inputs.clone();
524            inputs.depth = ark_babyjubjub::Fq::from(29u64); // invalid depth
525            assert!(matches!(
526                check_query_input_validity(&inputs).unwrap_err(),
527                super::ProofInputError::InvalidMerkleTreeDepth { .. }
528            ));
529        }
530        {
531            let mut inputs = inputs.clone();
532            // 1 << 30
533            inputs.mt_index = ark_bn254::Fr::from(1073741824u64);
534            assert!(matches!(
535                check_query_input_validity(&inputs).unwrap_err(),
536                super::ProofInputError::ValueOutOfBounds {
537                    name: "Merkle tree index",
538                    ..
539                }
540            ));
541        }
542        {
543            let mut inputs = inputs.clone();
544            inputs.merkle_root = ark_bn254::Fr::from(12345u64);
545            assert!(matches!(
546                check_query_input_validity(&inputs).unwrap_err(),
547                super::ProofInputError::InvalidMerkleTreeInclusionProof
548            ));
549        }
550        {
551            let mut inputs = inputs.clone();
552            inputs.pk_index = ark_bn254::Fr::from(7u64); // MAX_AUTHENTICATOR_KEYS is 7, so index 7 is out of bounds (0-6)
553            assert!(matches!(
554                check_query_input_validity(&inputs).unwrap_err(),
555                super::ProofInputError::ValueOutOfBounds {
556                    name: "Authenticator PubKey index",
557                    ..
558                }
559            ));
560        }
561        {
562            let mut inputs = inputs.clone();
563            inputs.r = Affine {
564                x: ark_babyjubjub::Fq::from(1u64),
565                y: ark_babyjubjub::Fq::from(2u64),
566            };
567            assert!(matches!(
568                check_query_input_validity(&inputs).unwrap_err(),
569                super::ProofInputError::InvalidBabyJubJubPoint {
570                    name: "Query Signature R"
571                }
572            ));
573        }
574        {
575            let mut inputs = inputs.clone();
576            inputs.pk[0] = Affine {
577                x: ark_babyjubjub::Fq::from(1u64),
578                y: ark_babyjubjub::Fq::from(2u64),
579            };
580
581            // Recompute the merkle root so the proof is valid
582            let pk_set = world_id_primitives::AuthenticatorPublicKeySet::new(
583                inputs
584                    .pk
585                    .iter()
586                    .map(|&x| eddsa_babyjubjub::EdDSAPublicKey { pk: x })
587                    .collect(),
588            )
589            .unwrap();
590            let mut current = pk_set.leaf_hash();
591            let idx =
592                u64::try_from(world_id_primitives::FieldElement::from(inputs.mt_index)).unwrap();
593            for (i, sibling) in inputs.siblings.iter().enumerate() {
594                let sibling_fr = *world_id_primitives::FieldElement::from(*sibling);
595                if (idx >> i) & 1 == 0 {
596                    let mut state = poseidon2::bn254::t2::permutation(&[current, sibling_fr]);
597                    state[0] += current;
598                    current = state[0];
599                } else {
600                    let mut state = poseidon2::bn254::t2::permutation(&[sibling_fr, current]);
601                    state[0] += sibling_fr;
602                    current = state[0];
603                }
604            }
605            inputs.merkle_root = current;
606
607            assert!(matches!(
608                check_query_input_validity(&inputs).unwrap_err(),
609                super::ProofInputError::InvalidBabyJubJubPoint {
610                    name: "Authenticator Public Key"
611                }
612            ));
613        }
614        {
615            let mut inputs = inputs.clone();
616            inputs.action = ark_bn254::Fr::from(12345u64);
617            assert!(matches!(
618                check_query_input_validity(&inputs).unwrap_err(),
619                super::ProofInputError::InvalidQuerySignature
620            ));
621        }
622    }
623
624    fn get_valid_nullifier_proof_input() -> NullifierProofCircuitInput<30> {
625        NullifierProofCircuitInput {
626            query_input: get_valid_query_proof_input(),
627            issuer_schema_id: ark_bn254::Fr::from(1u64),
628            cred_pk: Affine {
629                x: ark_babyjubjub::Fq::from_str(
630                    "15406775215557320288232407896017344573719706795510112309920214099347968981892",
631                )
632                .unwrap(),
633                y: ark_babyjubjub::Fq::from_str(
634                    "486388649729314270871358770861421181497883381447163109744630700259216042819",
635                )
636                .unwrap(),
637            },
638            cred_hashes: [
639                ark_bn254::Fr::from_str(
640                    "14272087287699568472569351444185311392108883722570788958733484799744115401870",
641                )
642                .unwrap(),
643                ark_bn254::Fr::from_str("0").unwrap(),
644            ],
645            cred_genesis_issued_at: ark_bn254::Fr::from(1770125923u64),
646            cred_expires_at: ark_bn254::Fr::from(1770125983u64),
647            cred_s: ark_babyjubjub::Fr::from_str(
648                "1213918488111680600555111454085490191981091366153388773926786471247948539005",
649            )
650            .unwrap(),
651            cred_r: Affine {
652                x: ark_babyjubjub::Fq::from_str(
653                    "15844586803954862856390946258558419582000810449135704981677693963391564067969",
654                )
655                .unwrap(),
656                y: ark_babyjubjub::Fq::from_str(
657                    "592710378120172403096018676235519447487818389124797234601458948988041235710",
658                )
659                .unwrap(),
660            },
661            current_timestamp: ark_bn254::Fr::from(1770125908u64),
662            cred_genesis_issued_at_min: ark_bn254::Fr::from(0u64),
663            cred_sub_blinding_factor: ark_bn254::Fr::from_str(
664                "12170146734368267085913078854954627576787934009906407554611507307540342380837",
665            )
666            .unwrap(),
667            cred_id: ark_bn254::Fr::from(3198767490419873482u64),
668            id_commitment_r: ark_bn254::Fr::from_str(
669                "11722352184830287916674945948108962396487445899741105828127518108056503126019",
670            )
671            .unwrap(),
672            id_commitment: ark_bn254::Fr::from(0u64),
673            dlog_e: ark_bn254::Fr::from_str(
674                "20738873297635092620048980552264360096607713029337408079647701591795211132447",
675            )
676            .unwrap(),
677            dlog_s: ark_babyjubjub::Fr::from_str(
678                "409914485496464180245985942628922659137136006706846380135829705769429965654",
679            )
680            .unwrap(),
681            oprf_pk: Affine {
682                x: ark_babyjubjub::Fq::from_str(
683                    "2124016492737602714904869498047199181102594928943726277329982080254326092458",
684                )
685                .unwrap(),
686                y: ark_babyjubjub::Fq::from_str(
687                    "13296886400185574560491768605341786437896334271868835545571935419923854148448",
688                )
689                .unwrap(),
690            },
691            oprf_response_blinded: Affine {
692                x: ark_babyjubjub::Fq::from_str(
693                    "186021305824089989598292966483056363224488147240980559441958002546059602483",
694                )
695                .unwrap(),
696                y: ark_babyjubjub::Fq::from_str(
697                    "16813058203546508924422863380215026034284821141284206571184467783067057954778",
698                )
699                .unwrap(),
700            },
701            oprf_response: Affine {
702                x: ark_babyjubjub::Fq::from_str(
703                    "10209445202057032226639052993170591937356545068582397532992536070677055126187",
704                )
705                .unwrap(),
706                y: ark_babyjubjub::Fq::from_str(
707                    "21877375411477040679486668720099554257785799784699842830375906922948306109699",
708                )
709                .unwrap(),
710            },
711            signal_hash: ark_bn254::Fr::from_str(
712                "37938388892362834151584770384290207919364301626797345218722464515205243407",
713            )
714            .unwrap(),
715        }
716    }
717
718    #[test]
719    fn test_valid_nullifier_proof_input() {
720        let inputs = get_valid_nullifier_proof_input();
721        let _ = check_nullifier_input_validity(&inputs).unwrap();
722    }
723
724    #[test]
725    fn test_invalid_nullifier_proof_input() {
726        let inputs = get_valid_nullifier_proof_input();
727        {
728            let mut inputs = inputs.clone();
729            inputs.current_timestamp =
730                ark_babyjubjub::Fq::from_str("123465723894591324701234982134000070").unwrap(); // invalid timestamp
731            assert!(matches!(
732                check_nullifier_input_validity(&inputs).unwrap_err(),
733                super::ProofInputError::ValueOutOfBounds {
734                    name: "current timestamp",
735                    ..
736                }
737            ));
738        }
739        {
740            let mut inputs = inputs.clone();
741            inputs.current_timestamp = inputs.cred_expires_at;
742            assert!(matches!(
743                check_nullifier_input_validity(&inputs).unwrap_err(),
744                super::ProofInputError::CredentialExpired { .. }
745            ));
746        }
747        {
748            let mut inputs = inputs.clone();
749            // genesis issued at 1770125923
750            inputs.cred_genesis_issued_at_min = ark_bn254::Fr::from(1770125924u64);
751            assert!(matches!(
752                check_nullifier_input_validity(&inputs).unwrap_err(),
753                super::ProofInputError::CredentialGenesisExpired { .. }
754            ));
755        }
756        {
757            let mut inputs = inputs.clone();
758            inputs.cred_r = Affine {
759                x: ark_babyjubjub::Fq::from(1u64),
760                y: ark_babyjubjub::Fq::from(2u64),
761            };
762            assert!(matches!(
763                check_nullifier_input_validity(&inputs).unwrap_err(),
764                super::ProofInputError::InvalidBabyJubJubPoint {
765                    name: "Credential Signature R"
766                }
767            ));
768        }
769        {
770            let mut inputs = inputs.clone();
771            inputs.cred_pk = Affine {
772                x: ark_babyjubjub::Fq::from(1u64),
773                y: ark_babyjubjub::Fq::from(2u64),
774            };
775            assert!(matches!(
776                check_nullifier_input_validity(&inputs).unwrap_err(),
777                super::ProofInputError::InvalidBabyJubJubPoint {
778                    name: "Credential Public Key"
779                }
780            ));
781        }
782        {
783            let mut inputs = inputs.clone();
784            inputs.cred_s = ark_babyjubjub::Fr::from(12345u64);
785            assert!(matches!(
786                check_nullifier_input_validity(&inputs).unwrap_err(),
787                super::ProofInputError::InvalidCredentialSignature
788            ));
789        }
790        {
791            let mut inputs = inputs.clone();
792            inputs.oprf_pk = Affine {
793                x: ark_babyjubjub::Fq::from(1u64),
794                y: ark_babyjubjub::Fq::from(2u64),
795            };
796            assert!(matches!(
797                check_nullifier_input_validity(&inputs).unwrap_err(),
798                super::ProofInputError::InvalidBabyJubJubPoint {
799                    name: "OPRF Public Key"
800                }
801            ));
802        }
803        {
804            let mut inputs = inputs.clone();
805            inputs.oprf_response_blinded = Affine {
806                x: ark_babyjubjub::Fq::from(1u64),
807                y: ark_babyjubjub::Fq::from(2u64),
808            };
809            assert!(matches!(
810                check_nullifier_input_validity(&inputs).unwrap_err(),
811                super::ProofInputError::InvalidBabyJubJubPoint {
812                    name: "OPRF Blinded Response"
813                }
814            ));
815        }
816        {
817            let mut inputs = inputs.clone();
818            inputs.dlog_s = ark_babyjubjub::Fr::from(12345u64);
819            assert!(matches!(
820                check_nullifier_input_validity(&inputs).unwrap_err(),
821                super::ProofInputError::InvalidOprfProof
822            ));
823        }
824        {
825            let mut inputs = inputs.clone();
826            inputs.oprf_response = Affine {
827                x: ark_babyjubjub::Fq::from(1u64),
828                y: ark_babyjubjub::Fq::from(2u64),
829            };
830            assert!(matches!(
831                check_nullifier_input_validity(&inputs).unwrap_err(),
832                super::ProofInputError::InvalidBabyJubJubPoint {
833                    name: "OPRF Unblinded Response"
834                }
835            ));
836        }
837        {
838            let mut inputs = inputs.clone();
839            // Valid point but incorrect for the blinded response
840            use ark_ec::AffineRepr;
841            inputs.oprf_response = ark_babyjubjub::EdwardsAffine::generator();
842            assert!(matches!(
843                check_nullifier_input_validity(&inputs).unwrap_err(),
844                super::ProofInputError::InvalidOprfResponse
845            ));
846        }
847        {
848            let mut inputs = inputs.clone();
849            inputs.id_commitment = ark_bn254::Fr::from(12345u64);
850            assert!(matches!(
851                check_nullifier_input_validity(&inputs).unwrap_err(),
852                super::ProofInputError::InvalidSessionId
853            ));
854        }
855    }
856
857    /// Verifies that the `cred_id` slot of the credential hash, which packs the
858    /// `Credential::id` and `Credential::issuer_version`, is computed
859    /// consistently between `Credential::hash` and the validation path used by
860    /// `check_nullifier_input_validity`.
861    ///
862    /// This protects against the proof-input validator silently diverging from
863    /// the issuer's actual signed hash when `issuer_version` is non-zero.
864    #[test]
865    fn test_packed_cred_id_matches_credential_hash() {
866        use super::hash_credential;
867        use ark_ff::BigInt;
868        use world_id_primitives::{Credential, FieldElement};
869
870        let mut credential = Credential::new();
871        credential.id = 0x1234_5678_9ABC_DEF0;
872        credential.issuer_version = 7;
873        credential.issuer_schema_id = 42;
874        credential.sub = FieldElement::from(100u64);
875        credential.genesis_issued_at = 1_000_000;
876        credential.expires_at = 2_000_000;
877
878        let direct = credential.hash().unwrap();
879
880        let packed_cred_id: ark_babyjubjub::Fq =
881            BigInt([credential.id, u64::from(credential.issuer_version), 0, 0]).into();
882
883        let via_validation = hash_credential(
884            FieldElement::from(credential.issuer_schema_id),
885            credential.sub,
886            FieldElement::from(credential.genesis_issued_at),
887            FieldElement::from(credential.expires_at),
888            credential.claims_hash().unwrap(),
889            credential.associated_data_commitment,
890            FieldElement::from(packed_cred_id),
891        );
892
893        assert_eq!(
894            direct, via_validation,
895            "packed cred_id must reproduce Credential::hash"
896        );
897
898        let unpacked_cred_id: ark_babyjubjub::Fq = credential.id.into();
899        let via_validation_unpacked = hash_credential(
900            FieldElement::from(credential.issuer_schema_id),
901            credential.sub,
902            FieldElement::from(credential.genesis_issued_at),
903            FieldElement::from(credential.expires_at),
904            credential.claims_hash().unwrap(),
905            credential.associated_data_commitment,
906            FieldElement::from(unpacked_cred_id),
907        );
908
909        assert_ne!(
910            direct, via_validation_unpacked,
911            "passing only id (without issuer_version) must NOT reproduce Credential::hash"
912        );
913    }
914}