world_id_core/requests/
mod.rs

1//! Module containing all the functionality to handle requests from Relying Parties (RPs) to Authenticators.
2//!
3//! Enables an RP to create a Proof request or a Session Proof request, and provides base functionality
4//! for Authenticators to handle such requests.
5
6mod constraints;
7pub use constraints::{ConstraintExpr, ConstraintKind, ConstraintNode, MAX_CONSTRAINT_NODES};
8
9use serde::de::Error as _;
10use serde::{Deserialize, Serialize};
11use std::collections::HashSet;
12use taceo_oprf_types::crypto::OprfPublicKey;
13use world_id_primitives::rp::RpId;
14use world_id_primitives::{FieldElement, PrimitiveError, WorldIdProof};
15
16/// Protocol schema version for proof requests and responses.
17#[repr(u8)]
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum RequestVersion {
20    /// Version 1
21    V1 = 1,
22}
23
24impl serde::Serialize for RequestVersion {
25    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
26    where
27        S: serde::Serializer,
28    {
29        let v = *self as u8;
30        serializer.serialize_u8(v)
31    }
32}
33
34impl<'de> serde::Deserialize<'de> for RequestVersion {
35    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
36    where
37        D: serde::Deserializer<'de>,
38    {
39        let v = u8::deserialize(deserializer)?;
40        match v {
41            1 => Ok(Self::V1),
42            _ => Err(serde::de::Error::custom("unsupported version")),
43        }
44    }
45}
46
47/// A proof request from a relying party for an authenticator
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(deny_unknown_fields)]
50pub struct ProofRequest {
51    /// Unique identifier for this request
52    pub id: String,
53    /// Version of the request
54    pub version: RequestVersion,
55    /// Unix timestamp (seconds since epoch) when the request was created
56    pub created_at: u64,
57    /// Unix timestamp (seconds since epoch) when request expires
58    pub expires_at: u64,
59    /// Registered RP id
60    pub rp_id: RpId,
61    /// The raw representation of the action. This must be already a field element.
62    ///
63    /// When dealing with strings or bytes, such value can be hashed e.g. with a byte-friendly
64    /// hash function like keccak256 or SHA256 and then reduced to a field element.
65    pub action: FieldElement,
66    /// The nullifier key of the RP (FIXME: documentation & serialization after #129)
67    pub oprf_public_key: OprfPublicKey,
68    /// The RP's ECDSA signature over the request
69    pub signature: k256::ecdsa::Signature,
70    /// Unique nonce for this request (serialized as hex string)
71    pub nonce: FieldElement,
72    /// Specific credential requests. This defines which credentials to ask for.
73    #[serde(rename = "proof_requests")]
74    pub requests: Vec<RequestItem>,
75    /// Constraint expression (all/any) optional
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub constraints: Option<ConstraintExpr<'static>>,
78}
79
80/// Per-credential request payload
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct RequestItem {
84    /// An RP-defined identifier for this request item which can be used to match against constraints and responses.
85    ///
86    /// Example: `orb`, `document`.
87    pub identifier: String,
88
89    /// The specific credential being requested as registered in the `CredentialIssuerSchemaRegistry`.
90    /// Serialized as hex string in JSON.
91    pub issuer_schema_id: FieldElement,
92    /// Optional RP-defined signal that will be bound into the proof.
93    ///
94    /// When present, the authenticator hashes this via `signal_hash`
95    /// and commits it into the proof circuit so the RP can tie the proof to a
96    /// particular action.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub signal: Option<String>,
99
100    /// An optional constraint on the minimum genesis issued at timestamp on the used credential.
101    ///
102    /// If present, the proof will include a constraint that the credential's genesis issued at timestamp
103    /// is greater than or equal to this value. This is useful for migration from previous protocol versions.
104    pub genesis_issued_at_min: Option<u64>,
105
106    /// If provided, a Session Proof will be generated instead of a Uniqueness Proof.
107    ///
108    /// The proof will only be valid if the session ID is meant for this context and this
109    /// particular World ID holder.
110    pub session_id: Option<FieldElement>,
111}
112
113impl RequestItem {
114    /// Create a new request item with the given identifier, issuer schema ID and optional signal.
115    #[must_use]
116    pub const fn new(
117        identifier: String,
118        issuer_schema_id: FieldElement,
119        signal: Option<String>,
120        genesis_issued_at_min: Option<u64>,
121        session_id: Option<FieldElement>,
122    ) -> Self {
123        Self {
124            identifier,
125            issuer_schema_id,
126            signal,
127            genesis_issued_at_min,
128            session_id,
129        }
130    }
131
132    /// Get the signal hash for the request item.
133    #[must_use]
134    pub fn signal_hash(&self) -> FieldElement {
135        if let Some(signal) = &self.signal {
136            FieldElement::from_arbitrary_raw_bytes(signal.as_bytes())
137        } else {
138            FieldElement::ZERO
139        }
140    }
141}
142
143/// Overall response from the Authenticator to the RP
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145#[serde(deny_unknown_fields)]
146pub struct ProofResponse {
147    /// The response id references request id
148    pub id: String,
149    /// Version corresponding to request version
150    pub version: RequestVersion,
151    /// Per-credential results
152    pub responses: Vec<ResponseItem>,
153}
154
155/// Per-credential response item returned by the authenticator.
156///
157/// Each entry corresponds to one requested credential. It carries the proof
158/// material when the authenticator could satisfy the request, or an `error`
159/// explaining why the credential could not be provided.
160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
161#[serde(deny_unknown_fields)]
162pub struct ResponseItem {
163    /// An RP-defined identifier for this request item which can be used to match against constraints and responses.
164    ///
165    /// Example: `orb`, `document`.
166    pub identifier: String,
167
168    /// Issuer schema id this item refers to (serialized as hex string)
169    pub issuer_schema_id: FieldElement,
170    /// Proof payload
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub proof: Option<WorldIdProof>,
173    /// RP-scoped nullifier derived from the credential, action, and RP id.
174    ///
175    /// Encoded as a hex string representation of the field element output by
176    /// the nullifier circuit. Present only when a proof was produced.
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub nullifier: Option<FieldElement>,
179    /// Optional RP session identifier that links multiple proofs for the same
180    /// user/RP pair across requests.
181    ///
182    /// When session proofs are enabled, this is the hex-encoded field element
183    /// emitted by the session circuit; otherwise it is omitted.
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub session_id: Option<FieldElement>,
186    /// Present if credential not provided
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub error: Option<String>,
189}
190
191impl ProofResponse {
192    /// Determine if constraints are satisfied given a constraint expression.
193    #[must_use]
194    pub fn constraints_satisfied(&self, constraints: &ConstraintExpr<'_>) -> bool {
195        let provided: HashSet<&str> = self
196            .responses
197            .iter()
198            .filter(|item| item.error.is_none())
199            .map(|item| item.identifier.as_str())
200            .collect();
201
202        constraints.evaluate(&|t| provided.contains(t))
203    }
204}
205
206impl ProofRequest {
207    /// Determine which requested credentials to prove given available credentials.
208    ///
209    /// Returns `None` if constraints (or lack thereof) cannot be satisfied with the available set.
210    ///
211    /// # Panics
212    /// Panics if constraints are present but invalid according to the type invariants
213    /// (this should not occur as constraints are provided by trusted request issuer).
214    #[must_use]
215    pub fn credentials_to_prove(&self, available: &HashSet<String>) -> Option<Vec<&RequestItem>> {
216        // Build set of requested identifiers
217        let requested: HashSet<&str> = self
218            .requests
219            .iter()
220            .map(|r| r.identifier.as_str())
221            .collect();
222
223        // Predicate: only select if both available and requested
224        let is_selectable =
225            |identifier: &str| available.contains(identifier) && requested.contains(identifier);
226
227        // If no explicit constraints: require all requested be available
228        if self.constraints.is_none() {
229            return if self
230                .requests
231                .iter()
232                .all(|r| available.contains(&r.identifier))
233            {
234                Some(self.requests.iter().collect())
235            } else {
236                None
237            };
238        }
239
240        // Compute selected identifiers using the constraint expression
241        let selected_identifiers = select_expr(self.constraints.as_ref().unwrap(), &is_selectable)?;
242        let selected_set: HashSet<&str> = selected_identifiers.into_iter().collect();
243
244        // Return proof_requests in original order filtered by selected identifiers
245        let result: Vec<&RequestItem> = self
246            .requests
247            .iter()
248            .filter(|r| selected_set.contains(r.identifier.as_str()))
249            .collect();
250        Some(result)
251    }
252
253    /// Find a request item by issuer schema ID if available
254    #[must_use]
255    pub fn find_request_by_issuer_schema_id(
256        &self,
257        issuer_schema_id: FieldElement,
258    ) -> Option<&RequestItem> {
259        self.requests
260            .iter()
261            .find(|r| r.issuer_schema_id == issuer_schema_id)
262    }
263
264    /// Returns true if the request is expired relative to now (unix timestamp in seconds)
265    #[must_use]
266    pub const fn is_expired(&self, now: u64) -> bool {
267        now > self.expires_at
268    }
269
270    /// Compute the digest hash of this request that should be signed by the RP, which right now
271    /// includes the `nonce` and the timestamp of the request.
272    ///
273    /// # Returns
274    /// A 32-byte hash that represents this request and should be signed by the RP.
275    ///
276    /// # Errors
277    /// Returns a `PrimitiveError` if `FieldElement` serialization fails (which should never occur in practice).
278    ///
279    /// Note: the timestamp is encoded as little-endian to mirror the RP-side signing
280    /// performed in test fixtures and the OPRF stub.
281    pub fn digest_hash(&self) -> Result<[u8; 32], PrimitiveError> {
282        use k256::sha2::{Digest, Sha256};
283
284        let mut writer = Vec::new();
285        let mut hasher = Sha256::new();
286        self.nonce.serialize_as_bytes(&mut writer)?;
287        hasher.update(&writer);
288        // Keep byte order aligned with RP signature generation (little-endian).
289        hasher.update(self.created_at.to_be_bytes());
290        Ok(hasher.finalize().into())
291    }
292
293    /// Validate that a response satisfies this request: id match and constraints semantics.
294    ///
295    /// # Errors
296    /// Returns a `ValidationError` if the response does not correspond to this request or
297    /// does not satisfy the declared constraints.
298    pub fn validate_response(&self, response: &ProofResponse) -> Result<(), ValidationError> {
299        // Validate id and version match
300        if self.id != response.id {
301            return Err(ValidationError::RequestIdMismatch);
302        }
303        if self.version != response.version {
304            return Err(ValidationError::VersionMismatch);
305        }
306
307        // Build set of successful credentials by identifier
308        let provided: HashSet<&str> = response
309            .responses
310            .iter()
311            .filter(|r| r.error.is_none())
312            .map(|r| r.identifier.as_str())
313            .collect();
314
315        match &self.constraints {
316            // None => all requested credentials (via identifier) are required
317            None => {
318                for req in &self.requests {
319                    if !provided.contains(req.identifier.as_str()) {
320                        return Err(ValidationError::MissingCredential(req.identifier.clone()));
321                    }
322                }
323                Ok(())
324            }
325            Some(expr) => {
326                if !expr.validate_max_depth(2) {
327                    return Err(ValidationError::ConstraintTooDeep);
328                }
329                if !expr.validate_max_nodes(MAX_CONSTRAINT_NODES) {
330                    return Err(ValidationError::ConstraintTooLarge);
331                }
332                if expr.evaluate(&|t| provided.contains(t)) {
333                    Ok(())
334                } else {
335                    Err(ValidationError::ConstraintNotSatisfied)
336                }
337            }
338        }
339    }
340
341    /// Parse from JSON
342    ///
343    /// # Errors
344    /// Returns an error if the JSON is invalid or contains duplicate issuer schema ids.
345    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
346        let v: Self = serde_json::from_str(json)?;
347        // Enforce unique issuer schema ids within a single request
348        let mut seen: HashSet<String> = HashSet::new();
349        for r in &v.requests {
350            let t = r.issuer_schema_id.to_string();
351            if !seen.insert(t.clone()) {
352                return Err(serde_json::Error::custom(format!(
353                    "duplicate issuer schema id: {t}"
354                )));
355            }
356        }
357        Ok(v)
358    }
359
360    /// Serialize to JSON
361    ///
362    /// # Errors
363    /// Returns an error if serialization unexpectedly fails.
364    pub fn to_json(&self) -> Result<String, serde_json::Error> {
365        serde_json::to_string(self)
366    }
367
368    /// Serialize to pretty JSON
369    ///
370    /// # Errors
371    /// Returns an error if serialization unexpectedly fails.
372    pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
373        serde_json::to_string_pretty(self)
374    }
375}
376
377impl ProofResponse {
378    /// Parse from JSON
379    ///
380    /// # Errors
381    /// Returns an error if the JSON does not match the expected response shape.
382    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
383        serde_json::from_str(json)
384    }
385
386    /// Serialize to pretty JSON
387    ///
388    /// # Errors
389    /// Returns an error if serialization fails.
390    pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
391        serde_json::to_string_pretty(self)
392    }
393
394    /// Return the list of successful issuer schema ids (no error)
395    #[must_use]
396    pub fn successful_credentials(&self) -> Vec<String> {
397        self.responses
398            .iter()
399            .filter(|r| r.error.is_none())
400            .map(|r| r.issuer_schema_id.to_string())
401            .collect()
402    }
403}
404
405/// Validation errors when checking a response against a request
406#[derive(Debug, thiserror::Error, PartialEq, Eq)]
407pub enum ValidationError {
408    /// The response `id` does not match the request `id`
409    #[error("Request ID mismatch")]
410    RequestIdMismatch,
411    /// The response `version` does not match the request `version`
412    #[error("Version mismatch")]
413    VersionMismatch,
414    /// A required credential was not provided
415    #[error("Missing required credential: {0}")]
416    MissingCredential(String),
417    /// The provided credentials do not satisfy the request constraints
418    #[error("Constraints not satisfied")]
419    ConstraintNotSatisfied,
420    /// The constraints expression exceeds the supported nesting depth
421    #[error("Constraints nesting exceeds maximum allowed depth")]
422    ConstraintTooDeep,
423    /// The constraints expression exceeds the maximum allowed size/complexity
424    #[error("Constraints exceed maximum allowed size")]
425    ConstraintTooLarge,
426}
427
428// Helper selection functions for constraint evaluation
429fn select_node<'a, F>(node: &'a ConstraintNode<'a>, pred: &F) -> Option<Vec<&'a str>>
430where
431    F: Fn(&str) -> bool,
432{
433    match node {
434        ConstraintNode::Type(t) => pred(t.as_ref()).then(|| vec![t.as_ref()]),
435        ConstraintNode::Expr(e) => select_expr(e, pred),
436    }
437}
438
439fn select_expr<'a, F>(expr: &'a ConstraintExpr<'a>, pred: &F) -> Option<Vec<&'a str>>
440where
441    F: Fn(&str) -> bool,
442{
443    match expr {
444        ConstraintExpr::All { all } => {
445            let mut seen: std::collections::HashSet<&'a str> = std::collections::HashSet::new();
446            let mut out: Vec<&'a str> = Vec::new();
447            for n in all {
448                let sub = select_node(n, pred)?;
449                for s in sub {
450                    if seen.insert(s) {
451                        out.push(s);
452                    }
453                }
454            }
455            Some(out)
456        }
457        ConstraintExpr::Any { any } => any.iter().find_map(|n| select_node(n, pred)),
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464    use alloy::uint;
465    use k256::ecdsa::{signature::Signer, SigningKey};
466
467    // Test helpers
468    fn test_signature() -> k256::ecdsa::Signature {
469        let signing_key = SigningKey::from_bytes(&[1u8; 32].into()).unwrap();
470        signing_key.sign(b"test")
471    }
472
473    fn test_oprf_public_key() -> OprfPublicKey {
474        // Create a dummy point for testing
475        use ark_ec::AffineRepr;
476        OprfPublicKey::new(ark_babyjubjub::EdwardsAffine::generator())
477    }
478
479    fn test_nonce() -> FieldElement {
480        FieldElement::from(1u64)
481    }
482
483    fn test_field_element(n: u64) -> FieldElement {
484        FieldElement::from(n)
485    }
486
487    #[test]
488    fn constraints_all_any_nested() {
489        // Build a response that has orb and passport successful, gov-id missing
490        let id1 = test_field_element(1);
491        let id2 = test_field_element(2);
492        let id3 = test_field_element(3);
493
494        let response = ProofResponse {
495            id: "req_123".into(),
496            version: RequestVersion::V1,
497            responses: vec![
498                ResponseItem {
499                    identifier: "test_req_1".into(),
500                    issuer_schema_id: id1,
501                    proof: Some(WorldIdProof::default()),
502                    nullifier: Some(test_field_element(1001)),
503                    session_id: None,
504                    error: None,
505                },
506                ResponseItem {
507                    identifier: "test_req_2".into(),
508                    issuer_schema_id: id2,
509                    proof: Some(WorldIdProof::default()),
510                    nullifier: Some(test_field_element(1002)),
511                    session_id: None,
512                    error: None,
513                },
514                ResponseItem {
515                    identifier: "test_req_3".into(),
516                    issuer_schema_id: id3,
517                    proof: None,
518                    nullifier: None,
519                    session_id: None,
520                    error: Some("credential_not_available".into()),
521                },
522            ],
523        };
524
525        // all: [test_req_1, any: [test_req_2, test_req_4]]
526        let expr = ConstraintExpr::All {
527            all: vec![
528                ConstraintNode::Type("test_req_1".into()),
529                ConstraintNode::Expr(ConstraintExpr::Any {
530                    any: vec![
531                        ConstraintNode::Type("test_req_2".into()),
532                        ConstraintNode::Type("test_req_4".into()),
533                    ],
534                }),
535            ],
536        };
537
538        assert!(response.constraints_satisfied(&expr));
539
540        // all: [test_req_1, test_req_3] should fail due to test_req_3 error
541        let fail_expr = ConstraintExpr::All {
542            all: vec![
543                ConstraintNode::Type("test_req_1".into()),
544                ConstraintNode::Type("test_req_3".into()),
545            ],
546        };
547        assert!(!response.constraints_satisfied(&fail_expr));
548    }
549
550    #[test]
551    fn test_digest_hash() {
552        let request = ProofRequest {
553            id: "test_request".into(),
554            version: RequestVersion::V1,
555            created_at: 1_700_000_000,
556            expires_at: 1_700_100_000,
557            rp_id: RpId::from(uint!(1_U160)),
558            action: FieldElement::ZERO,
559            oprf_public_key: test_oprf_public_key(),
560            signature: test_signature(),
561            nonce: test_nonce(),
562            requests: vec![RequestItem {
563                identifier: "orb".into(),
564                issuer_schema_id: test_field_element(1),
565                signal: Some("test_signal".into()),
566                genesis_issued_at_min: None,
567                session_id: None,
568            }],
569            constraints: None,
570        };
571
572        let digest1 = request.digest_hash().unwrap();
573        // Verify it returns a 32-byte hash
574        assert_eq!(digest1.len(), 32);
575
576        // Verify deterministic: same request produces same hash
577        let digest2 = request.digest_hash().unwrap();
578        assert_eq!(digest1, digest2);
579
580        // Verify different request nonces produce different hashes
581        let request2 = ProofRequest {
582            nonce: test_field_element(3),
583            ..request
584        };
585        let digest3 = request2.digest_hash().unwrap();
586        assert_ne!(digest1, digest3);
587    }
588
589    #[test]
590    fn request_validate_response_none_constraints_means_all() {
591        let request = ProofRequest {
592            id: "req_1".into(),
593            version: RequestVersion::V1,
594            created_at: 1_735_689_600,
595            expires_at: 1_735_689_600, // 2025-01-01
596            rp_id: RpId::from(uint!(1_U160)),
597            action: FieldElement::ZERO,
598            oprf_public_key: test_oprf_public_key(),
599            signature: test_signature(),
600            nonce: test_nonce(),
601            requests: vec![
602                RequestItem {
603                    identifier: "orb".into(),
604                    issuer_schema_id: test_field_element(1),
605                    signal: None,
606                    genesis_issued_at_min: None,
607                    session_id: None,
608                },
609                RequestItem {
610                    identifier: "document".into(),
611                    issuer_schema_id: test_field_element(2),
612                    signal: None,
613                    genesis_issued_at_min: None,
614                    session_id: None,
615                },
616            ],
617            constraints: None,
618        };
619
620        let ok = ProofResponse {
621            id: "req_1".into(),
622            version: RequestVersion::V1,
623            responses: vec![
624                ResponseItem {
625                    identifier: "orb".into(),
626                    issuer_schema_id: test_field_element(1),
627                    proof: Some(WorldIdProof::default()),
628                    nullifier: None,
629                    session_id: None,
630                    error: None,
631                },
632                ResponseItem {
633                    identifier: "document".into(),
634                    issuer_schema_id: test_field_element(2),
635                    proof: Some(WorldIdProof::default()),
636                    nullifier: None,
637                    session_id: None,
638                    error: None,
639                },
640            ],
641        };
642        assert!(request.validate_response(&ok).is_ok());
643
644        let missing = ProofResponse {
645            id: "req_1".into(),
646            version: RequestVersion::V1,
647            responses: vec![ResponseItem {
648                identifier: "orb".into(),
649                issuer_schema_id: test_field_element(1),
650                proof: Some(WorldIdProof::default()),
651                nullifier: None,
652                session_id: None,
653                error: None,
654            }],
655        };
656        let err = request.validate_response(&missing).unwrap_err();
657        assert!(matches!(err, ValidationError::MissingCredential(_)));
658    }
659
660    #[test]
661    fn constraint_depth_enforced() {
662        // Root all -> nested any -> nested all (depth 3) should be rejected
663        let deep = ConstraintExpr::All {
664            all: vec![ConstraintNode::Expr(ConstraintExpr::Any {
665                any: vec![ConstraintNode::Expr(ConstraintExpr::All {
666                    all: vec![ConstraintNode::Type("orb".into())],
667                })],
668            })],
669        };
670
671        let request = ProofRequest {
672            id: "req_2".into(),
673            version: RequestVersion::V1,
674            created_at: 1_735_689_600,
675            expires_at: 1_735_689_600,
676            rp_id: RpId::from(uint!(1_U160)),
677            action: test_field_element(1),
678            oprf_public_key: test_oprf_public_key(),
679            signature: test_signature(),
680            nonce: test_nonce(),
681            requests: vec![RequestItem {
682                identifier: "orb".into(),
683                issuer_schema_id: test_field_element(1),
684                signal: None,
685                genesis_issued_at_min: None,
686                session_id: None,
687            }],
688            constraints: Some(deep),
689        };
690
691        let response = ProofResponse {
692            id: "req_2".into(),
693            version: RequestVersion::V1,
694            responses: vec![ResponseItem {
695                identifier: "orb".into(),
696                issuer_schema_id: test_field_element(1),
697                proof: Some(WorldIdProof::default()),
698                nullifier: None,
699                session_id: None,
700                error: None,
701            }],
702        };
703
704        let err = request.validate_response(&response).unwrap_err();
705        assert!(matches!(err, ValidationError::ConstraintTooDeep));
706    }
707
708    #[test]
709    #[allow(clippy::too_many_lines)]
710    fn constraint_node_limit_boundary_passes() {
711        // Root All with: 1 Type + Any(4) + Any(4)
712        // Node count = root(1) + type(1) + any(1+4) + any(1+4) = 12
713        let id10 = test_field_element(10);
714        let id11 = test_field_element(11);
715        let id12 = test_field_element(12);
716        let id13 = test_field_element(13);
717        let id14 = test_field_element(14);
718        let id15 = test_field_element(15);
719        let id16 = test_field_element(16);
720        let id17 = test_field_element(17);
721        let id18 = test_field_element(18);
722
723        let expr = ConstraintExpr::All {
724            all: vec![
725                ConstraintNode::Type("test_req_10".into()),
726                ConstraintNode::Expr(ConstraintExpr::Any {
727                    any: vec![
728                        ConstraintNode::Type("test_req_11".into()),
729                        ConstraintNode::Type("test_req_12".into()),
730                        ConstraintNode::Type("test_req_13".into()),
731                        ConstraintNode::Type("test_req_14".into()),
732                    ],
733                }),
734                ConstraintNode::Expr(ConstraintExpr::Any {
735                    any: vec![
736                        ConstraintNode::Type("test_req_15".into()),
737                        ConstraintNode::Type("test_req_16".into()),
738                        ConstraintNode::Type("test_req_17".into()),
739                        ConstraintNode::Type("test_req_18".into()),
740                    ],
741                }),
742            ],
743        };
744
745        let request = ProofRequest {
746            id: "req_nodes_ok".into(),
747            version: RequestVersion::V1,
748            created_at: 1_735_689_600,
749            expires_at: 1_735_689_600,
750            rp_id: RpId::from(uint!(1_U160)),
751            action: test_field_element(5),
752            oprf_public_key: test_oprf_public_key(),
753            signature: test_signature(),
754            nonce: test_nonce(),
755            requests: vec![
756                RequestItem {
757                    identifier: "test_req_10".into(),
758                    issuer_schema_id: id10,
759                    signal: None,
760                    genesis_issued_at_min: None,
761                    session_id: None,
762                },
763                RequestItem {
764                    identifier: "test_req_11".into(),
765                    issuer_schema_id: id11,
766                    signal: None,
767                    genesis_issued_at_min: None,
768                    session_id: None,
769                },
770                RequestItem {
771                    identifier: "test_req_12".into(),
772                    issuer_schema_id: id12,
773                    signal: None,
774                    genesis_issued_at_min: None,
775                    session_id: None,
776                },
777                RequestItem {
778                    identifier: "test_req_13".into(),
779                    issuer_schema_id: id13,
780                    signal: None,
781                    genesis_issued_at_min: None,
782                    session_id: None,
783                },
784                RequestItem {
785                    identifier: "test_req_14".into(),
786                    issuer_schema_id: id14,
787                    signal: None,
788                    genesis_issued_at_min: None,
789                    session_id: None,
790                },
791                RequestItem {
792                    identifier: "test_req_15".into(),
793                    issuer_schema_id: id15,
794                    signal: None,
795                    genesis_issued_at_min: None,
796                    session_id: None,
797                },
798                RequestItem {
799                    identifier: "test_req_16".into(),
800                    issuer_schema_id: id16,
801                    signal: None,
802                    genesis_issued_at_min: None,
803                    session_id: None,
804                },
805                RequestItem {
806                    identifier: "test_req_17".into(),
807                    issuer_schema_id: id17,
808                    signal: None,
809                    genesis_issued_at_min: None,
810                    session_id: None,
811                },
812                RequestItem {
813                    identifier: "test_req_18".into(),
814                    issuer_schema_id: id18,
815                    signal: None,
816                    genesis_issued_at_min: None,
817                    session_id: None,
818                },
819            ],
820            constraints: Some(expr),
821        };
822
823        // Provide just enough to satisfy both any-groups and the single type
824        let response = ProofResponse {
825            id: "req_nodes_ok".into(),
826            version: RequestVersion::V1,
827            responses: vec![
828                ResponseItem {
829                    identifier: "test_req_10".into(),
830                    issuer_schema_id: id10,
831                    proof: Some(WorldIdProof::default()),
832                    nullifier: None,
833                    session_id: None,
834                    error: None,
835                },
836                ResponseItem {
837                    identifier: "test_req_11".into(),
838                    issuer_schema_id: id11,
839                    proof: Some(WorldIdProof::default()),
840                    nullifier: None,
841                    session_id: None,
842                    error: None,
843                },
844                ResponseItem {
845                    identifier: "test_req_15".into(),
846                    issuer_schema_id: id15,
847                    proof: Some(WorldIdProof::default()),
848                    nullifier: None,
849                    session_id: None,
850                    error: None,
851                },
852            ],
853        };
854
855        // Should not exceed size and should validate OK
856        assert!(request.validate_response(&response).is_ok());
857    }
858
859    #[test]
860    #[allow(clippy::too_many_lines)]
861    fn constraint_node_limit_exceeded_fails() {
862        // Root All with: 1 Type + Any(4) + Any(5)
863        // Node count = root(1) + type(1) + any(1+4) + any(1+5) = 13 (> 12)
864        let expr = ConstraintExpr::All {
865            all: vec![
866                ConstraintNode::Type("t0".into()),
867                ConstraintNode::Expr(ConstraintExpr::Any {
868                    any: vec![
869                        ConstraintNode::Type("t1".into()),
870                        ConstraintNode::Type("t2".into()),
871                        ConstraintNode::Type("t3".into()),
872                        ConstraintNode::Type("t4".into()),
873                    ],
874                }),
875                ConstraintNode::Expr(ConstraintExpr::Any {
876                    any: vec![
877                        ConstraintNode::Type("t5".into()),
878                        ConstraintNode::Type("t6".into()),
879                        ConstraintNode::Type("t7".into()),
880                        ConstraintNode::Type("t8".into()),
881                        ConstraintNode::Type("t9".into()),
882                    ],
883                }),
884            ],
885        };
886
887        let request = ProofRequest {
888            id: "req_nodes_too_many".into(),
889            version: RequestVersion::V1,
890            created_at: 1_735_689_600,
891            expires_at: 1_735_689_600,
892            rp_id: RpId::from(uint!(1_U160)),
893            action: test_field_element(1),
894            oprf_public_key: test_oprf_public_key(),
895            signature: test_signature(),
896            nonce: test_nonce(),
897            requests: vec![
898                RequestItem {
899                    identifier: "test_req_20".into(),
900                    issuer_schema_id: test_field_element(20),
901                    signal: None,
902                    genesis_issued_at_min: None,
903                    session_id: None,
904                },
905                RequestItem {
906                    identifier: "test_req_21".into(),
907                    issuer_schema_id: test_field_element(21),
908                    signal: None,
909                    genesis_issued_at_min: None,
910                    session_id: None,
911                },
912                RequestItem {
913                    identifier: "test_req_22".into(),
914                    issuer_schema_id: test_field_element(22),
915                    signal: None,
916                    genesis_issued_at_min: None,
917                    session_id: None,
918                },
919                RequestItem {
920                    identifier: "test_req_23".into(),
921                    issuer_schema_id: test_field_element(23),
922                    signal: None,
923                    genesis_issued_at_min: None,
924                    session_id: None,
925                },
926                RequestItem {
927                    identifier: "test_req_24".into(),
928                    issuer_schema_id: test_field_element(24),
929                    signal: None,
930                    genesis_issued_at_min: None,
931                    session_id: None,
932                },
933                RequestItem {
934                    identifier: "test_req_25".into(),
935                    issuer_schema_id: test_field_element(25),
936                    signal: None,
937                    genesis_issued_at_min: None,
938                    session_id: None,
939                },
940                RequestItem {
941                    identifier: "test_req_26".into(),
942                    issuer_schema_id: test_field_element(26),
943                    signal: None,
944                    genesis_issued_at_min: None,
945                    session_id: None,
946                },
947                RequestItem {
948                    identifier: "test_req_27".into(),
949                    issuer_schema_id: test_field_element(27),
950                    signal: None,
951                    genesis_issued_at_min: None,
952                    session_id: None,
953                },
954                RequestItem {
955                    identifier: "test_req_28".into(),
956                    issuer_schema_id: test_field_element(28),
957                    signal: None,
958                    genesis_issued_at_min: None,
959                    session_id: None,
960                },
961                RequestItem {
962                    identifier: "test_req_29".into(),
963                    issuer_schema_id: test_field_element(29),
964                    signal: None,
965                    genesis_issued_at_min: None,
966                    session_id: None,
967                },
968            ],
969            constraints: Some(expr),
970        };
971
972        // Response content is irrelevant; validation should fail before evaluation due to size
973        let response = ProofResponse {
974            id: "req_nodes_too_many".into(),
975            version: RequestVersion::V1,
976            responses: vec![ResponseItem {
977                identifier: "test_req_20".into(),
978                issuer_schema_id: test_field_element(20),
979                proof: Some(WorldIdProof::default()),
980                nullifier: None,
981                session_id: None,
982                error: None,
983            }],
984        };
985
986        let err = request.validate_response(&response).unwrap_err();
987        assert!(matches!(err, ValidationError::ConstraintTooLarge));
988    }
989
990    #[test]
991    fn request_single_credential_parse_and_validate() {
992        let req = ProofRequest {
993            id: "req_18c0f7f03e7d".into(),
994            version: RequestVersion::V1,
995            created_at: 1_725_381_192,
996            expires_at: 1_725_381_492,
997            rp_id: RpId::from(uint!(1_U160)),
998            action: test_field_element(1),
999            oprf_public_key: test_oprf_public_key(),
1000            signature: test_signature(),
1001            nonce: test_nonce(),
1002            requests: vec![RequestItem {
1003                identifier: "test_req_1".into(),
1004                issuer_schema_id: test_field_element(1),
1005                signal: Some("abcd-efgh-ijkl".into()),
1006                genesis_issued_at_min: Some(1_725_381_192),
1007                session_id: Some(test_field_element(55)),
1008            }],
1009            constraints: None,
1010        };
1011
1012        assert_eq!(req.id, "req_18c0f7f03e7d");
1013        assert_eq!(req.requests.len(), 1);
1014
1015        // Build matching successful response
1016        let resp = ProofResponse {
1017            id: req.id.clone(),
1018            version: RequestVersion::V1,
1019            responses: vec![ResponseItem {
1020                identifier: "test_req_1".into(),
1021                issuer_schema_id: test_field_element(1),
1022                proof: Some(WorldIdProof::default()),
1023                nullifier: Some(test_field_element(1001)),
1024                session_id: None,
1025                error: None,
1026            }],
1027        };
1028        assert!(req.validate_response(&resp).is_ok());
1029    }
1030
1031    #[test]
1032    fn request_multiple_credentials_all_constraint_and_failure() {
1033        let req = ProofRequest {
1034            id: "req_18c0f7f03e7d".into(),
1035            version: RequestVersion::V1,
1036            created_at: 1_725_381_192,
1037            expires_at: 1_725_381_492,
1038            rp_id: RpId::from(uint!(1_U160)),
1039            action: test_field_element(1),
1040            oprf_public_key: test_oprf_public_key(),
1041            signature: test_signature(),
1042            nonce: test_nonce(),
1043            requests: vec![
1044                RequestItem {
1045                    identifier: "test_req_1".into(),
1046                    issuer_schema_id: test_field_element(1),
1047                    signal: Some("abcd-efgh-ijkl".into()),
1048                    genesis_issued_at_min: Some(1_725_381_192),
1049                    session_id: Some(test_field_element(100)),
1050                },
1051                RequestItem {
1052                    identifier: "test_req_2".into(),
1053                    issuer_schema_id: test_field_element(2),
1054                    signal: Some("abcd-efgh-ijkl".into()),
1055                    genesis_issued_at_min: Some(1_725_381_192),
1056                    session_id: Some(test_field_element(12)),
1057                },
1058            ],
1059            constraints: Some(ConstraintExpr::All {
1060                all: vec![
1061                    ConstraintNode::Type("test_req_1".into()),
1062                    ConstraintNode::Type("test_req_2".into()),
1063                ],
1064            }),
1065        };
1066
1067        // Build response that fails constraints (0x1 error)
1068        let resp = ProofResponse {
1069            id: req.id.clone(),
1070            version: RequestVersion::V1,
1071            responses: vec![
1072                ResponseItem {
1073                    identifier: "test_req_2".into(),
1074                    issuer_schema_id: test_field_element(2),
1075                    proof: Some(WorldIdProof::default()),
1076                    nullifier: Some(test_field_element(1001)),
1077                    session_id: None,
1078                    error: None,
1079                },
1080                ResponseItem {
1081                    identifier: "test_req_1".into(),
1082                    issuer_schema_id: test_field_element(1),
1083                    proof: None,
1084                    nullifier: None,
1085                    session_id: None,
1086                    error: Some("credential_not_available".into()),
1087                },
1088            ],
1089        };
1090
1091        let err = req.validate_response(&resp).unwrap_err();
1092        assert!(matches!(err, ValidationError::ConstraintNotSatisfied));
1093    }
1094
1095    #[test]
1096    fn request_more_complex_constraints_nested_success() {
1097        let req = ProofRequest {
1098            id: "req_18c0f7f03e7d".into(),
1099            version: RequestVersion::V1,
1100            created_at: 1_725_381_192,
1101            expires_at: 1_725_381_492,
1102            rp_id: RpId::from(uint!(1_U160)),
1103            action: test_field_element(1),
1104            oprf_public_key: test_oprf_public_key(),
1105            signature: test_signature(),
1106            nonce: test_nonce(),
1107            requests: vec![
1108                RequestItem {
1109                    identifier: "test_req_1".into(),
1110                    issuer_schema_id: test_field_element(1),
1111                    signal: Some("abcd-efgh-ijkl".into()),
1112                    genesis_issued_at_min: None,
1113                    session_id: None,
1114                },
1115                RequestItem {
1116                    identifier: "test_req_2".into(),
1117                    issuer_schema_id: test_field_element(2),
1118                    signal: Some("mnop-qrst-uvwx".into()),
1119                    genesis_issued_at_min: None,
1120                    session_id: None,
1121                },
1122                RequestItem {
1123                    identifier: "test_req_3".into(),
1124                    issuer_schema_id: test_field_element(3),
1125                    signal: Some("abcd-efgh-ijkl".into()),
1126                    genesis_issued_at_min: None,
1127                    session_id: None,
1128                },
1129            ],
1130            constraints: Some(ConstraintExpr::All {
1131                all: vec![
1132                    ConstraintNode::Type("test_req_3".into()),
1133                    ConstraintNode::Expr(ConstraintExpr::Any {
1134                        any: vec![
1135                            ConstraintNode::Type("test_req_1".into()),
1136                            ConstraintNode::Type("test_req_2".into()),
1137                        ],
1138                    }),
1139                ],
1140            }),
1141        };
1142
1143        // Satisfy nested any with 0x1 + 0x3
1144        let resp = ProofResponse {
1145            id: req.id.clone(),
1146            version: RequestVersion::V1,
1147            responses: vec![
1148                ResponseItem {
1149                    identifier: "test_req_3".into(),
1150                    issuer_schema_id: test_field_element(3),
1151                    proof: Some(WorldIdProof::default()),
1152                    nullifier: Some(test_field_element(1001)),
1153                    session_id: None,
1154                    error: None,
1155                },
1156                ResponseItem {
1157                    identifier: "test_req_1".into(),
1158                    issuer_schema_id: test_field_element(1),
1159                    proof: Some(WorldIdProof::default()),
1160                    nullifier: Some(test_field_element(1002)),
1161                    session_id: None,
1162                    error: None,
1163                },
1164            ],
1165        };
1166
1167        assert!(req.validate_response(&resp).is_ok());
1168    }
1169
1170    #[test]
1171    fn response_success_and_with_session_and_failure_parse() {
1172        // Success OK - using default proof (all zeros) in hex
1173        let orb_id_str = test_field_element(100).to_string();
1174        let gov_id_str = test_field_element(101).to_string();
1175
1176        let ok_json = format!(
1177            r#"{{
1178  "id": "req_18c0f7f03e7d",
1179  "version": 1,
1180  "responses": [
1181    {{
1182      "identifier": "orb",
1183      "issuer_schema_id": "{orb_id_str}",
1184      "proof": "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000",
1185      "nullifier": "0x00000000000000000000000000000000000000000000000000000000000003e9"
1186    }}
1187  ]
1188}}"#
1189        );
1190        let ok = ProofResponse::from_json(&ok_json).unwrap();
1191        assert_eq!(ok.successful_credentials(), vec![orb_id_str.clone()]);
1192
1193        // Failure (constraints not satisfied) shape parsing
1194        let fail_json = format!(
1195            r#"{{
1196  "id": "req_18c0f7f03e7d",
1197  "version": 1,
1198  "responses": [
1199    {{ "identifier": "orb", "issuer_schema_id": "{orb_id_str}", "proof": "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000", "nullifier": "0x00000000000000000000000000000000000000000000000000000000000003e9" }},
1200    {{ "identifier": "gov_id", "issuer_schema_id": "{gov_id_str}", "error": "credential_not_available" }}
1201  ]
1202}}"#
1203        );
1204        let fail = ProofResponse::from_json(&fail_json).unwrap();
1205        assert_eq!(fail.successful_credentials(), vec![orb_id_str.clone()]);
1206
1207        // Success with Session
1208        let sess_json = format!(
1209            r#"{{
1210  "id": "req_18c0f7f03e7d",
1211  "version": 1,
1212  "responses": [
1213    {{
1214      "identifier": "orb",
1215      "issuer_schema_id": "{orb_id_str}",
1216      "proof": "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000",
1217      "nullifier": "0x00000000000000000000000000000000000000000000000000000000000003e9",
1218      "session_id": "0x00000000000000000000000000000000000000000000000000000000000003ea"
1219    }}
1220  ]
1221}}"#
1222        );
1223        let sess = ProofResponse::from_json(&sess_json).unwrap();
1224        assert_eq!(sess.successful_credentials(), vec![orb_id_str]);
1225        assert!(sess.responses[0].session_id.is_some());
1226    }
1227
1228    #[test]
1229    fn request_rejects_duplicate_issuer_schema_ids_on_parse() {
1230        // Test duplicate detection by creating a serialized ProofRequest with duplicates
1231        // and then trying to parse it with from_json which should detect the duplicates
1232        let id1 = test_field_element(1);
1233        let req = ProofRequest {
1234            id: "req_dup".into(),
1235            version: RequestVersion::V1,
1236            created_at: 1_725_381_192,
1237            expires_at: 1_725_381_492,
1238            rp_id: RpId::from(uint!(1_U160)),
1239            action: test_field_element(5),
1240            oprf_public_key: test_oprf_public_key(),
1241            signature: test_signature(),
1242            nonce: test_nonce(),
1243            requests: vec![
1244                RequestItem {
1245                    identifier: "test_req_1".into(),
1246                    issuer_schema_id: id1,
1247                    signal: None,
1248                    genesis_issued_at_min: None,
1249                    session_id: None,
1250                },
1251                RequestItem {
1252                    identifier: "test_req_2".into(),
1253                    issuer_schema_id: id1, // Duplicate!
1254                    signal: None,
1255                    genesis_issued_at_min: None,
1256                    session_id: None,
1257                },
1258            ],
1259            constraints: None,
1260        };
1261
1262        // Serialize then deserialize to trigger the duplicate check in from_json
1263        let json = req.to_json().unwrap();
1264        let err = ProofRequest::from_json(&json).unwrap_err();
1265        let msg = err.to_string();
1266        assert!(
1267            msg.contains("duplicate issuer schema id"),
1268            "Expected error message to contain 'duplicate issuer schema id', got: {msg}"
1269        );
1270    }
1271
1272    #[test]
1273    fn credentials_to_prove_none_constraints_requires_all_and_drops_if_missing() {
1274        let orb_id = test_field_element(100);
1275        let passport_id = test_field_element(101);
1276
1277        let req = ProofRequest {
1278            id: "req".into(),
1279            version: RequestVersion::V1,
1280            created_at: 1_735_689_600,
1281            expires_at: 1_735_689_600, // 2025-01-01 00:00:00 UTC
1282            rp_id: RpId::from(uint!(1_U160)),
1283            action: test_field_element(5),
1284            oprf_public_key: test_oprf_public_key(),
1285            signature: test_signature(),
1286            nonce: test_nonce(),
1287            requests: vec![
1288                RequestItem {
1289                    identifier: "orb".into(),
1290                    issuer_schema_id: orb_id,
1291                    signal: None,
1292                    genesis_issued_at_min: None,
1293                    session_id: None,
1294                },
1295                RequestItem {
1296                    identifier: "passport".into(),
1297                    issuer_schema_id: passport_id,
1298                    signal: None,
1299                    genesis_issued_at_min: None,
1300                    session_id: None,
1301                },
1302            ],
1303            constraints: None,
1304        };
1305
1306        let available_ok: HashSet<String> = ["orb".to_string(), "passport".to_string()]
1307            .into_iter()
1308            .collect();
1309        let sel_ok = req.credentials_to_prove(&available_ok).unwrap();
1310        assert_eq!(sel_ok.len(), 2);
1311        assert_eq!(sel_ok[0].issuer_schema_id, orb_id);
1312        assert_eq!(sel_ok[1].issuer_schema_id, passport_id);
1313
1314        let available_missing: HashSet<String> = std::iter::once("orb".to_string()).collect();
1315        assert!(req.credentials_to_prove(&available_missing).is_none());
1316    }
1317
1318    #[test]
1319    fn credentials_to_prove_with_constraints_all_and_any() {
1320        // proof_requests: orb, passport, national-id
1321        let orb_id = test_field_element(100);
1322        let passport_id = test_field_element(101);
1323        let national_id_id = test_field_element(102);
1324
1325        let req = ProofRequest {
1326            id: "req".into(),
1327            version: RequestVersion::V1,
1328            created_at: 1_735_689_600,
1329            expires_at: 1_735_689_600, // 2025-01-01 00:00:00 UTC
1330            rp_id: RpId::from(uint!(1_U160)),
1331            action: test_field_element(1),
1332            oprf_public_key: test_oprf_public_key(),
1333            signature: test_signature(),
1334            nonce: test_nonce(),
1335            requests: vec![
1336                RequestItem {
1337                    identifier: "orb".into(),
1338                    issuer_schema_id: orb_id,
1339                    signal: None,
1340                    genesis_issued_at_min: None,
1341                    session_id: None,
1342                },
1343                RequestItem {
1344                    identifier: "passport".into(),
1345                    issuer_schema_id: passport_id,
1346                    signal: None,
1347                    genesis_issued_at_min: None,
1348                    session_id: None,
1349                },
1350                RequestItem {
1351                    identifier: "national_id".into(),
1352                    issuer_schema_id: national_id_id,
1353                    signal: None,
1354                    genesis_issued_at_min: None,
1355                    session_id: None,
1356                },
1357            ],
1358            constraints: Some(ConstraintExpr::All {
1359                all: vec![
1360                    ConstraintNode::Type("orb".into()),
1361                    ConstraintNode::Expr(ConstraintExpr::Any {
1362                        any: vec![
1363                            ConstraintNode::Type("passport".into()),
1364                            ConstraintNode::Type("national_id".into()),
1365                        ],
1366                    }),
1367                ],
1368            }),
1369        };
1370
1371        // Available has orb + passport → should pick [orb, passport]
1372        let available1: HashSet<String> = ["orb".to_string(), "passport".to_string()]
1373            .into_iter()
1374            .collect();
1375        let sel1 = req.credentials_to_prove(&available1).unwrap();
1376        assert_eq!(sel1.len(), 2);
1377        assert_eq!(sel1[0].issuer_schema_id, orb_id);
1378        assert_eq!(sel1[1].issuer_schema_id, passport_id);
1379
1380        // Available has orb + national-id → should pick [orb, national-id]
1381        let available2: HashSet<String> = ["orb".to_string(), "national_id".to_string()]
1382            .into_iter()
1383            .collect();
1384        let sel2 = req.credentials_to_prove(&available2).unwrap();
1385        assert_eq!(sel2.len(), 2);
1386        assert_eq!(sel2[0].issuer_schema_id, orb_id);
1387        assert_eq!(sel2[1].issuer_schema_id, national_id_id);
1388
1389        // Missing orb → cannot satisfy "all" → None
1390        let available3: HashSet<String> = std::iter::once("passport".to_string()).collect();
1391        assert!(req.credentials_to_prove(&available3).is_none());
1392    }
1393}