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