mod constraints;
pub use constraints::{ConstraintExpr, ConstraintKind, ConstraintNode, MAX_CONSTRAINT_NODES};
use crate::{
FieldElement, Nullifier, OprfPrefix, OprfPrefixedFieldElement as _, PrimitiveError, SessionId,
SessionNullifier, SessionRef, ZeroKnowledgeProof, rp::RpId,
};
use serde::{Deserialize, Serialize, de::Error as _};
use std::collections::HashSet;
use taceo_oprf::types::OprfKeyId;
use uuid as _;
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestVersion {
V1 = 1,
}
impl serde::Serialize for RequestVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let v = *self as u8;
serializer.serialize_u8(v)
}
}
impl<'de> serde::Deserialize<'de> for RequestVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let v = u8::deserialize(deserializer)?;
match v {
1 => Ok(Self::V1),
_ => Err(serde::de::Error::custom("unsupported version")),
}
}
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProofType {
#[default]
Uniqueness = OprfPrefix::Uniqueness as u8,
Session = OprfPrefix::SessionAction as u8,
}
impl ProofType {
#[must_use]
pub const fn is_uniqueness(&self) -> bool {
matches!(self, Self::Uniqueness)
}
#[must_use]
pub const fn is_session(&self) -> bool {
matches!(self, Self::Session)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofRequest {
pub id: String,
pub version: RequestVersion,
#[serde(default)]
pub proof_type: ProofType,
pub created_at: u64,
pub expires_at: u64,
pub rp_id: RpId,
pub oprf_key_id: OprfKeyId,
#[serde(default)]
pub session_id: SessionRef,
pub action: Option<FieldElement>,
#[serde(with = "crate::serde_utils::hex_signature")]
pub signature: alloy::signers::Signature,
pub nonce: FieldElement,
#[serde(rename = "proof_requests")]
pub requests: Vec<RequestItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub constraints: Option<ConstraintExpr<'static>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RequestItem {
pub identifier: String,
pub issuer_schema_id: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(with = "crate::serde_utils::hex_bytes_opt")]
pub signal: Option<Vec<u8>>,
pub genesis_issued_at_min: Option<u64>,
pub expires_at_min: Option<u64>,
}
impl RequestItem {
#[must_use]
pub const fn new(
identifier: String,
issuer_schema_id: u64,
signal: Option<Vec<u8>>,
genesis_issued_at_min: Option<u64>,
expires_at_min: Option<u64>,
) -> Self {
Self {
identifier,
issuer_schema_id,
signal,
genesis_issued_at_min,
expires_at_min,
}
}
#[must_use]
pub fn signal_hash(&self) -> FieldElement {
if let Some(signal) = &self.signal {
FieldElement::from_arbitrary_raw_bytes(signal)
} else {
FieldElement::ZERO
}
}
#[must_use]
pub const fn effective_expires_at_min(&self, request_created_at: u64) -> u64 {
match self.expires_at_min {
Some(value) => value,
None => request_created_at,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProofResponse {
pub id: String,
pub version: RequestVersion,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<SessionId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
pub responses: Vec<ResponseItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseItem {
pub identifier: String,
pub issuer_schema_id: u64,
pub proof: ZeroKnowledgeProof,
#[serde(skip_serializing_if = "Option::is_none")]
pub nullifier: Option<Nullifier>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_nullifier: Option<SessionNullifier>,
pub expires_at_min: u64,
}
impl ProofResponse {
#[must_use]
pub fn constraints_satisfied(&self, constraints: &ConstraintExpr<'_>) -> bool {
if self.error.is_some() {
return false;
}
let provided: HashSet<&str> = self
.responses
.iter()
.map(|item| item.identifier.as_str())
.collect();
constraints.evaluate(&|t| provided.contains(t))
}
}
impl ResponseItem {
#[must_use]
pub const fn new_uniqueness(
identifier: String,
issuer_schema_id: u64,
proof: ZeroKnowledgeProof,
nullifier: Nullifier,
expires_at_min: u64,
) -> Self {
Self {
identifier,
issuer_schema_id,
proof,
nullifier: Some(nullifier),
session_nullifier: None,
expires_at_min,
}
}
#[must_use]
pub const fn new_session(
identifier: String,
issuer_schema_id: u64,
proof: ZeroKnowledgeProof,
session_nullifier: SessionNullifier,
expires_at_min: u64,
) -> Self {
Self {
identifier,
issuer_schema_id,
proof,
nullifier: None,
session_nullifier: Some(session_nullifier),
expires_at_min,
}
}
#[must_use]
pub const fn is_session(&self) -> bool {
self.session_nullifier.is_some()
}
#[must_use]
pub const fn is_uniqueness(&self) -> bool {
self.nullifier.is_some()
}
}
impl ProofRequest {
#[must_use]
pub fn credentials_to_prove(&self, available: &HashSet<u64>) -> Option<Vec<&RequestItem>> {
let available_identifiers: HashSet<&str> = self
.requests
.iter()
.filter(|r| available.contains(&r.issuer_schema_id))
.map(|r| r.identifier.as_str())
.collect();
let is_selectable = |identifier: &str| available_identifiers.contains(identifier);
if self.constraints.is_none() {
return if self
.requests
.iter()
.all(|r| available.contains(&r.issuer_schema_id))
{
Some(self.requests.iter().collect())
} else {
None
};
}
let selected_identifiers = select_expr(self.constraints.as_ref().unwrap(), &is_selectable)?;
let selected_set: HashSet<&str> = selected_identifiers.into_iter().collect();
let result: Vec<&RequestItem> = self
.requests
.iter()
.filter(|r| selected_set.contains(r.identifier.as_str()))
.collect();
Some(result)
}
#[must_use]
pub fn find_request_by_issuer_schema_id(&self, issuer_schema_id: u64) -> Option<&RequestItem> {
self.requests
.iter()
.find(|r| r.issuer_schema_id == issuer_schema_id)
}
#[must_use]
pub const fn is_expired(&self, now: u64) -> bool {
now > self.expires_at
}
pub fn digest_hash(&self) -> Result<[u8; 32], PrimitiveError> {
use crate::rp::compute_rp_signature_msg;
use sha2::{Digest, Sha256};
let msg = compute_rp_signature_msg(
*self.nonce,
self.created_at,
self.expires_at,
self.action.map(|v| *v),
);
let mut hasher = Sha256::new();
hasher.update(&msg);
Ok(hasher.finalize().into())
}
pub fn validate_proof_type(&self) -> Result<(), PrimitiveError> {
match (self.proof_type, self.session_id, self.action) {
(ProofType::Uniqueness, _, None) => Err(PrimitiveError::InvalidInput {
attribute: "action".to_string(),
reason: "must be present for uniqueness proofs".to_string(),
}),
(ProofType::Uniqueness, SessionRef::Existing(_), _) => {
Err(PrimitiveError::InvalidInput {
attribute: "session_id".to_string(),
reason: "must be omitted or \"create\" for uniqueness proofs".to_string(),
})
}
(ProofType::Session, SessionRef::None, _) => Err(PrimitiveError::InvalidInput {
attribute: "session_id".to_string(),
reason: "must be \"create\" or an existing session id for session proofs"
.to_string(),
}),
(ProofType::Session, SessionRef::Create | SessionRef::Existing(_), Some(_)) => {
Err(PrimitiveError::InvalidInput {
attribute: "action".to_string(),
reason: "must be omitted for session proofs".to_string(),
})
}
_ => Ok(()),
}?;
if let Some(action) = self.action
&& !action.has_prefix(OprfPrefix::Uniqueness)
{
return Err(PrimitiveError::InvalidInput {
attribute: "action".to_string(),
reason: format!("MSB must be 0x{:02x}", OprfPrefix::Uniqueness as u8),
});
}
Ok(())
}
#[must_use]
pub const fn is_session_proof(&self) -> bool {
self.proof_type.is_session()
}
#[must_use]
pub const fn binds_session(&self) -> bool {
self.proof_type.is_uniqueness() && self.session_id.is_create()
}
pub fn validate_constraints(&self) -> Result<(), ValidationError> {
if let Some(expr) = &self.constraints {
if !expr.validate_max_depth(2) {
return Err(ValidationError::ConstraintTooDeep);
}
if !expr.validate_max_nodes(MAX_CONSTRAINT_NODES) {
return Err(ValidationError::ConstraintTooLarge);
}
}
Ok(())
}
pub fn validate_response(&self, response: &ProofResponse) -> Result<(), ValidationError> {
self.validate_proof_type()
.map_err(|err| ValidationError::InvalidProofRequest(err.to_string()))?;
if self.id != response.id {
return Err(ValidationError::RequestIdMismatch);
}
if self.version != response.version {
return Err(ValidationError::VersionMismatch);
}
if let Some(error) = &response.error {
return Err(ValidationError::ProofGenerationFailed(error.clone()));
}
match (self.proof_type, self.session_id) {
(ProofType::Uniqueness, SessionRef::None) => {
if response.session_id.is_some() {
return Err(ValidationError::UnexpectedSessionId);
}
}
(ProofType::Uniqueness, SessionRef::Create) => {
if response.session_id.is_none() {
return Err(ValidationError::MissingSessionId);
}
}
(ProofType::Uniqueness, SessionRef::Existing(_)) => {
return Err(ValidationError::InvalidProofRequest(
"uniqueness proof with an existing session_id".to_string(),
));
}
(ProofType::Session, SessionRef::Create) => {
if response.session_id.is_none() {
return Err(ValidationError::MissingSessionId);
}
}
(ProofType::Session, SessionRef::Existing(session_id)) => {
if response.session_id != Some(session_id) {
return Err(ValidationError::SessionIdMismatch);
}
}
(ProofType::Session, SessionRef::None) => {
return Err(ValidationError::InvalidProofRequest(
"session proof without session_id".to_string(),
));
}
}
let mut provided: HashSet<&str> = HashSet::new();
for response_item in &response.responses {
if !provided.insert(response_item.identifier.as_str()) {
return Err(ValidationError::DuplicateCredential(
response_item.identifier.clone(),
));
}
let request_item = self
.requests
.iter()
.find(|r| r.identifier == response_item.identifier)
.ok_or_else(|| {
ValidationError::UnexpectedCredential(response_item.identifier.clone())
})?;
if self.is_session_proof() {
if response_item.session_nullifier.is_none() {
return Err(ValidationError::MissingSessionNullifier(
response_item.identifier.clone(),
));
}
} else {
if response_item.nullifier.is_none() {
return Err(ValidationError::MissingNullifier(
response_item.identifier.clone(),
));
}
}
let expected_expires_at_min = request_item.effective_expires_at_min(self.created_at);
if response_item.expires_at_min != expected_expires_at_min {
return Err(ValidationError::ExpiresAtMinMismatch(
response_item.identifier.clone(),
expected_expires_at_min,
response_item.expires_at_min,
));
}
}
match &self.constraints {
None => {
for req in &self.requests {
if !provided.contains(req.identifier.as_str()) {
return Err(ValidationError::MissingCredential(req.identifier.clone()));
}
}
Ok(())
}
Some(expr) => {
self.validate_constraints()?;
if expr.evaluate(&|t| provided.contains(t)) {
Ok(())
} else {
Err(ValidationError::ConstraintNotSatisfied)
}
}
}
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
let v: Self = serde_json::from_str(json)?;
v.validate_proof_type().map_err(serde_json::Error::custom)?;
let mut seen: HashSet<String> = HashSet::new();
for r in &v.requests {
let t = r.issuer_schema_id.to_string();
if !seen.insert(t.clone()) {
return Err(serde_json::Error::custom(format!(
"duplicate issuer schema id: {t}"
)));
}
}
Ok(v)
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
}
impl ProofResponse {
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
#[must_use]
pub fn successful_credentials(&self) -> Vec<u64> {
if self.error.is_some() {
return vec![];
}
self.responses.iter().map(|r| r.issuer_schema_id).collect()
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ValidationError {
#[error("Invalid proof request: {0}")]
InvalidProofRequest(String),
#[error("Request ID mismatch")]
RequestIdMismatch,
#[error("Version mismatch")]
VersionMismatch,
#[error("Proof generation failed: {0}")]
ProofGenerationFailed(String),
#[error("Missing required credential: {0}")]
MissingCredential(String),
#[error("Unexpected credential in response: {0}")]
UnexpectedCredential(String),
#[error("Duplicate credential in response: {0}")]
DuplicateCredential(String),
#[error("Constraints not satisfied")]
ConstraintNotSatisfied,
#[error("Constraints nesting exceeds maximum allowed depth")]
ConstraintTooDeep,
#[error("Constraints exceed maximum allowed size")]
ConstraintTooLarge,
#[error("Invalid expires_at_min for credential '{0}': expected {1}, got {2}")]
ExpiresAtMinMismatch(String, u64, u64),
#[error("Session ID doesn't match between request and response")]
SessionIdMismatch,
#[error("Session ID missing from session response")]
MissingSessionId,
#[error("Session ID present in uniqueness response")]
UnexpectedSessionId,
#[error("Session nullifier missing for credential: {0}")]
MissingSessionNullifier(String),
#[error("Nullifier missing for credential: {0}")]
MissingNullifier(String),
}
fn select_node<'a, F>(node: &'a ConstraintNode<'a>, pred: &F) -> Option<Vec<&'a str>>
where
F: Fn(&str) -> bool,
{
match node {
ConstraintNode::Type(t) => pred(t.as_ref()).then(|| vec![t.as_ref()]),
ConstraintNode::Expr(e) => select_expr(e, pred),
}
}
fn select_expr<'a, F>(expr: &'a ConstraintExpr<'a>, pred: &F) -> Option<Vec<&'a str>>
where
F: Fn(&str) -> bool,
{
match expr {
ConstraintExpr::All { all } => {
let mut seen: std::collections::HashSet<&'a str> = std::collections::HashSet::new();
let mut out: Vec<&'a str> = Vec::new();
for n in all {
let sub = select_node(n, pred)?;
for s in sub {
if seen.insert(s) {
out.push(s);
}
}
}
Some(out)
}
ConstraintExpr::Any { any } => any.iter().find_map(|n| select_node(n, pred)),
ConstraintExpr::Enumerate { enumerate } => {
let mut seen: std::collections::HashSet<&'a str> = std::collections::HashSet::new();
let mut selected: Vec<&'a str> = Vec::new();
for child in enumerate {
let Some(child_selection) = select_node(child, pred) else {
continue;
};
for identifier in child_selection {
if seen.insert(identifier) {
selected.push(identifier);
}
}
}
if selected.is_empty() {
None
} else {
Some(selected)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SessionNullifier;
use alloy::{
signers::{SignerSync, local::PrivateKeySigner},
uint,
};
use k256::ecdsa::SigningKey;
fn test_signature() -> alloy::signers::Signature {
let signer =
PrivateKeySigner::from_signing_key(SigningKey::from_bytes(&[1u8; 32].into()).unwrap());
signer.sign_message_sync(b"test").expect("can sign")
}
fn test_nonce() -> FieldElement {
FieldElement::from(1u64)
}
fn test_field_element(n: u64) -> FieldElement {
FieldElement::from(n)
}
fn test_action_with_prefix(prefix: OprfPrefix, n: u64) -> FieldElement {
use ruint::aliases::U256;
let v = U256::from(n) | (U256::from(prefix as u8) << 248);
FieldElement::try_from(v).expect("test value fits in field")
}
fn test_action(n: u64) -> FieldElement {
test_action_with_prefix(OprfPrefix::SessionAction, n)
}
fn test_session_id(n: u64) -> SessionId {
use ruint::{aliases::U256, uint};
let seed = U256::from(n)
| uint!(0x0100000000000000000000000000000000000000000000000000000000000000_U256);
SessionId::new(
test_field_element(n),
FieldElement::try_from(seed).expect("test value fits in field"),
)
.expect("valid session id")
}
#[test]
fn constraints_all_any_nested() {
let response = ProofResponse {
id: "req_123".into(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![
ResponseItem::new_uniqueness(
"test_req_1".into(),
1,
ZeroKnowledgeProof::default(),
test_field_element(1001).into(),
1_735_689_600,
),
ResponseItem::new_uniqueness(
"test_req_2".into(),
2,
ZeroKnowledgeProof::default(),
test_field_element(1002).into(),
1_735_689_600,
),
],
};
let expr = ConstraintExpr::All {
all: vec![
ConstraintNode::Type("test_req_1".into()),
ConstraintNode::Expr(ConstraintExpr::Any {
any: vec![
ConstraintNode::Type("test_req_2".into()),
ConstraintNode::Type("test_req_4".into()),
],
}),
],
};
assert!(response.constraints_satisfied(&expr));
let fail_expr = ConstraintExpr::All {
all: vec![
ConstraintNode::Type("test_req_1".into()),
ConstraintNode::Type("test_req_3".into()),
],
};
assert!(!response.constraints_satisfied(&fail_expr));
}
#[test]
fn constraints_enumerate_partial_and_empty() {
let response = ProofResponse {
id: "req_123".into(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![
ResponseItem::new_uniqueness(
"orb".into(),
1,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1001)),
1_735_689_600,
),
ResponseItem::new_uniqueness(
"passport".into(),
2,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1002)),
1_735_689_600,
),
],
};
let expr = ConstraintExpr::Enumerate {
enumerate: vec![
ConstraintNode::Type("passport".into()),
ConstraintNode::Type("national_id".into()),
],
};
assert!(response.constraints_satisfied(&expr));
let fail_expr = ConstraintExpr::Enumerate {
enumerate: vec![
ConstraintNode::Type("national_id".into()),
ConstraintNode::Type("document".into()),
],
};
assert!(!response.constraints_satisfied(&fail_expr));
}
#[test]
fn test_digest_hash() {
let request = ProofRequest {
id: "test_request".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::None,
action: Some(FieldElement::ZERO),
created_at: 1_700_000_000,
expires_at: 1_700_100_000,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![RequestItem {
identifier: "orb".into(),
issuer_schema_id: 1,
signal: Some("test_signal".into()),
genesis_issued_at_min: None,
expires_at_min: None,
}],
constraints: None,
};
let digest1 = request.digest_hash().unwrap();
assert_eq!(digest1.len(), 32);
let digest2 = request.digest_hash().unwrap();
assert_eq!(digest1, digest2);
let request2 = ProofRequest {
nonce: test_field_element(3),
..request
};
let digest3 = request2.digest_hash().unwrap();
assert_ne!(digest1, digest3);
}
#[test]
fn proof_request_signature_serializes_as_hex_string() {
let request = ProofRequest {
id: "test".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::None,
action: Some(FieldElement::ZERO),
created_at: 1_700_000_000,
expires_at: 1_700_100_000,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![RequestItem {
identifier: "orb".into(),
issuer_schema_id: 1,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
}],
constraints: None,
};
let json = request.to_json().unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(value["proof_type"], "uniqueness");
assert_eq!(
value["action"],
serde_json::to_value(FieldElement::ZERO).unwrap()
);
assert!(value.get("kind").is_none());
let sig = value["signature"]
.as_str()
.expect("signature should be a string");
assert!(sig.starts_with("0x"));
assert_eq!(sig.len(), 132);
let roundtripped = ProofRequest::from_json(&json).unwrap();
assert_eq!(roundtripped.signature, request.signature);
}
#[test]
fn request_validate_response_none_constraints_means_all() {
let request = ProofRequest {
id: "req_1".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::None,
action: Some(FieldElement::ZERO),
created_at: 1_735_689_600,
expires_at: 1_735_689_600, rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![
RequestItem {
identifier: "orb".into(),
issuer_schema_id: 1,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "document".into(),
issuer_schema_id: 2,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
],
constraints: None,
};
let ok = ProofResponse {
id: "req_1".into(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![
ResponseItem::new_uniqueness(
"orb".into(),
1,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1001)),
1_735_689_600,
),
ResponseItem::new_uniqueness(
"document".into(),
2,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1002)),
1_735_689_600,
),
],
};
assert!(request.validate_response(&ok).is_ok());
let missing = ProofResponse {
id: "req_1".into(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![ResponseItem::new_uniqueness(
"orb".into(),
1,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1001)),
1_735_689_600,
)],
};
let err = request.validate_response(&missing).unwrap_err();
assert!(matches!(err, ValidationError::MissingCredential(_)));
let unexpected = ProofResponse {
id: "req_1".into(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![
ResponseItem::new_uniqueness(
"orb".into(),
1,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1001)),
1_735_689_600,
),
ResponseItem::new_uniqueness(
"document".into(),
2,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1002)),
1_735_689_600,
),
ResponseItem::new_uniqueness(
"passport".into(),
3,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1003)),
1_735_689_600,
),
],
};
let err = request.validate_response(&unexpected).unwrap_err();
assert!(matches!(
err,
ValidationError::UnexpectedCredential(ref id) if id == "passport"
));
let duplicate = ProofResponse {
id: "req_1".into(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![
ResponseItem::new_uniqueness(
"orb".into(),
1,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1001)),
1_735_689_600,
),
ResponseItem::new_uniqueness(
"orb".into(),
1,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1001)),
1_735_689_600,
),
],
};
let err = request.validate_response(&duplicate).unwrap_err();
assert!(matches!(
err,
ValidationError::DuplicateCredential(ref id) if id == "orb"
));
}
#[test]
fn constraint_depth_enforced() {
let deep = ConstraintExpr::All {
all: vec![ConstraintNode::Expr(ConstraintExpr::Any {
any: vec![ConstraintNode::Expr(ConstraintExpr::All {
all: vec![ConstraintNode::Type("orb".into())],
})],
})],
};
let request = ProofRequest {
id: "req_2".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::None,
action: Some(test_field_element(1)),
created_at: 1_735_689_600,
expires_at: 1_735_689_600,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![RequestItem {
identifier: "orb".into(),
issuer_schema_id: 1,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
}],
constraints: Some(deep),
};
let response = ProofResponse {
id: "req_2".into(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![ResponseItem::new_uniqueness(
"orb".into(),
1,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1001)),
1_735_689_600,
)],
};
let err = request.validate_response(&response).unwrap_err();
assert!(matches!(err, ValidationError::ConstraintTooDeep));
}
#[test]
#[allow(clippy::too_many_lines)]
fn constraint_node_limit_boundary_passes() {
let expr = ConstraintExpr::All {
all: vec![
ConstraintNode::Type("test_req_10".into()),
ConstraintNode::Expr(ConstraintExpr::Any {
any: vec![
ConstraintNode::Type("test_req_11".into()),
ConstraintNode::Type("test_req_12".into()),
ConstraintNode::Type("test_req_13".into()),
ConstraintNode::Type("test_req_14".into()),
],
}),
ConstraintNode::Expr(ConstraintExpr::Any {
any: vec![
ConstraintNode::Type("test_req_15".into()),
ConstraintNode::Type("test_req_16".into()),
ConstraintNode::Type("test_req_17".into()),
ConstraintNode::Type("test_req_18".into()),
],
}),
],
};
let request = ProofRequest {
id: "req_nodes_ok".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::None,
action: Some(test_field_element(5)),
created_at: 1_735_689_600,
expires_at: 1_735_689_600,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![
RequestItem {
identifier: "test_req_10".into(),
issuer_schema_id: 10,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_11".into(),
issuer_schema_id: 11,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_12".into(),
issuer_schema_id: 12,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_13".into(),
issuer_schema_id: 13,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_14".into(),
issuer_schema_id: 14,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_15".into(),
issuer_schema_id: 15,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_16".into(),
issuer_schema_id: 16,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_17".into(),
issuer_schema_id: 17,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_18".into(),
issuer_schema_id: 18,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
],
constraints: Some(expr),
};
let response = ProofResponse {
id: "req_nodes_ok".into(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![
ResponseItem::new_uniqueness(
"test_req_10".into(),
10,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1010)),
1_735_689_600,
),
ResponseItem::new_uniqueness(
"test_req_11".into(),
11,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1011)),
1_735_689_600,
),
ResponseItem::new_uniqueness(
"test_req_15".into(),
15,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1015)),
1_735_689_600,
),
],
};
assert!(request.validate_response(&response).is_ok());
}
#[test]
#[allow(clippy::too_many_lines)]
fn constraint_node_limit_exceeded_fails() {
let expr = ConstraintExpr::All {
all: vec![
ConstraintNode::Type("t0".into()),
ConstraintNode::Expr(ConstraintExpr::Any {
any: vec![
ConstraintNode::Type("t1".into()),
ConstraintNode::Type("t2".into()),
ConstraintNode::Type("t3".into()),
ConstraintNode::Type("t4".into()),
],
}),
ConstraintNode::Expr(ConstraintExpr::Any {
any: vec![
ConstraintNode::Type("t5".into()),
ConstraintNode::Type("t6".into()),
ConstraintNode::Type("t7".into()),
ConstraintNode::Type("t8".into()),
ConstraintNode::Type("t9".into()),
],
}),
],
};
let request = ProofRequest {
id: "req_nodes_too_many".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::None,
action: Some(test_field_element(1)),
created_at: 1_735_689_600,
expires_at: 1_735_689_600,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![
RequestItem {
identifier: "test_req_20".into(),
issuer_schema_id: 20,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_21".into(),
issuer_schema_id: 21,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_22".into(),
issuer_schema_id: 22,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_23".into(),
issuer_schema_id: 23,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_24".into(),
issuer_schema_id: 24,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_25".into(),
issuer_schema_id: 25,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_26".into(),
issuer_schema_id: 26,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_27".into(),
issuer_schema_id: 27,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_28".into(),
issuer_schema_id: 28,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_29".into(),
issuer_schema_id: 29,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
],
constraints: Some(expr),
};
let response = ProofResponse {
id: "req_nodes_too_many".into(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![ResponseItem::new_uniqueness(
"test_req_20".into(),
20,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1020)),
1_735_689_600,
)],
};
let err = request.validate_response(&response).unwrap_err();
assert!(matches!(err, ValidationError::ConstraintTooLarge));
}
#[test]
fn request_single_credential_parse_and_validate() {
let req = ProofRequest {
id: "req_18c0f7f03e7d".into(),
version: RequestVersion::V1,
proof_type: ProofType::Session,
session_id: SessionRef::Existing(SessionId::default()),
action: None,
created_at: 1_725_381_192,
expires_at: 1_725_381_492,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![RequestItem {
identifier: "test_req_1".into(),
issuer_schema_id: 1,
signal: Some("abcd-efgh-ijkl".into()),
genesis_issued_at_min: Some(1_725_381_192),
expires_at_min: None,
}],
constraints: None,
};
assert_eq!(req.id, "req_18c0f7f03e7d");
assert_eq!(req.requests.len(), 1);
let resp = ProofResponse {
id: req.id.clone(),
version: RequestVersion::V1,
session_id: Some(SessionId::default()),
error: None,
responses: vec![ResponseItem::new_session(
"test_req_1".into(),
1,
ZeroKnowledgeProof::default(),
SessionNullifier::new(test_field_element(1001), test_action(1)).unwrap(),
1_725_381_192,
)],
};
assert!(req.validate_response(&resp).is_ok());
}
#[test]
fn request_multiple_credentials_all_constraint_and_missing() {
let req = ProofRequest {
id: "req_18c0f7f03e7d".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::None,
action: Some(test_field_element(1)),
created_at: 1_725_381_192,
expires_at: 1_725_381_492,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![
RequestItem {
identifier: "test_req_1".into(),
issuer_schema_id: 1,
signal: Some("abcd-efgh-ijkl".into()),
genesis_issued_at_min: Some(1_725_381_192),
expires_at_min: None,
},
RequestItem {
identifier: "test_req_2".into(),
issuer_schema_id: 2,
signal: Some("abcd-efgh-ijkl".into()),
genesis_issued_at_min: Some(1_725_381_192),
expires_at_min: None,
},
],
constraints: Some(ConstraintExpr::All {
all: vec![
ConstraintNode::Type("test_req_1".into()),
ConstraintNode::Type("test_req_2".into()),
],
}),
};
let resp = ProofResponse {
id: req.id.clone(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![ResponseItem::new_uniqueness(
"test_req_2".into(),
2,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1001)),
1_725_381_192,
)],
};
let err = req.validate_response(&resp).unwrap_err();
assert!(matches!(err, ValidationError::ConstraintNotSatisfied));
}
#[test]
fn request_more_complex_constraints_nested_success() {
let req = ProofRequest {
id: "req_18c0f7f03e7d".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::None,
action: Some(test_field_element(1)),
created_at: 1_725_381_192,
expires_at: 1_725_381_492,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![
RequestItem {
identifier: "test_req_1".into(),
issuer_schema_id: 1,
signal: Some("abcd-efgh-ijkl".into()),
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_2".into(),
issuer_schema_id: 2,
signal: Some("mnop-qrst-uvwx".into()),
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_3".into(),
issuer_schema_id: 3,
signal: Some("abcd-efgh-ijkl".into()),
genesis_issued_at_min: None,
expires_at_min: None,
},
],
constraints: Some(ConstraintExpr::All {
all: vec![
ConstraintNode::Type("test_req_3".into()),
ConstraintNode::Expr(ConstraintExpr::Any {
any: vec![
ConstraintNode::Type("test_req_1".into()),
ConstraintNode::Type("test_req_2".into()),
],
}),
],
}),
};
let resp = ProofResponse {
id: req.id.clone(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![
ResponseItem::new_uniqueness(
"test_req_3".into(),
3,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1001)),
1_725_381_192,
),
ResponseItem::new_uniqueness(
"test_req_1".into(),
1,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1002)),
1_725_381_192,
),
],
};
assert!(req.validate_response(&resp).is_ok());
}
#[test]
fn request_validate_response_with_enumerate() {
let req = ProofRequest {
id: "req_enum".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::None,
action: Some(test_field_element(1)),
created_at: 1_725_381_192,
expires_at: 1_725_381_492,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![
RequestItem {
identifier: "passport".into(),
issuer_schema_id: 2,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "national_id".into(),
issuer_schema_id: 3,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
],
constraints: Some(ConstraintExpr::Enumerate {
enumerate: vec![
ConstraintNode::Type("passport".into()),
ConstraintNode::Type("national_id".into()),
],
}),
};
let ok_resp = ProofResponse {
id: req.id.clone(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![ResponseItem::new_uniqueness(
"passport".into(),
2,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(2002)),
1_725_381_192,
)],
};
assert!(req.validate_response(&ok_resp).is_ok());
let fail_resp = ProofResponse {
id: req.id.clone(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![],
};
let err = req.validate_response(&fail_resp).unwrap_err();
assert!(matches!(err, ValidationError::ConstraintNotSatisfied));
}
#[test]
fn request_json_parse() {
let with_signal = r#"{
"id": "req_abc123",
"version": 1,
"created_at": 1725381192,
"expires_at": 1725381492,
"rp_id": "rp_0000000000000001",
"oprf_key_id": "0x1",
"session_id": null,
"action": "0x000000000000000000000000000000000000000000000000000000000000002a",
"signature": "0xa1fd06f0d8ceb541f6096fe2e865063eac1ff085c9d2bac2eedcc9ed03804bfc18d956b38c5ac3a8f7e71fde43deff3bda254d369c699f3c7a3f8e6b8477a5f51c",
"nonce": "0x0000000000000000000000000000000000000000000000000000000000000001",
"proof_requests": [
{
"identifier": "orb",
"issuer_schema_id": 1,
"signal": "0xdeadbeef",
"genesis_issued_at_min": 1725381192,
"expires_at_min": 1725381492
}
]
}"#;
let req = ProofRequest::from_json(with_signal).expect("parse with signal");
assert_eq!(req.id, "req_abc123");
assert_eq!(req.requests.len(), 1);
assert_eq!(req.requests[0].signal, Some(b"\xde\xad\xbe\xef".to_vec()));
assert_eq!(req.requests[0].genesis_issued_at_min, Some(1_725_381_192));
assert_eq!(req.requests[0].expires_at_min, Some(1_725_381_492));
let without_signal = r#"{
"id": "req_abc123",
"version": 1,
"created_at": 1725381192,
"expires_at": 1725381492,
"rp_id": "rp_0000000000000001",
"oprf_key_id": "0x1",
"session_id": null,
"action": "0x000000000000000000000000000000000000000000000000000000000000002a",
"signature": "0xa1fd06f0d8ceb541f6096fe2e865063eac1ff085c9d2bac2eedcc9ed03804bfc18d956b38c5ac3a8f7e71fde43deff3bda254d369c699f3c7a3f8e6b8477a5f51c",
"nonce": "0x0000000000000000000000000000000000000000000000000000000000000001",
"proof_requests": [
{
"identifier": "orb",
"issuer_schema_id": 1
}
]
}"#;
let req = ProofRequest::from_json(without_signal).expect("parse without signal");
assert!(req.requests[0].signal.is_none());
assert_eq!(req.requests[0].signal_hash(), FieldElement::ZERO);
}
#[test]
fn request_json_tolerates_unknown_fields() {
let json = r#"{
"id": "req_abc123",
"version": 1,
"created_at": 1725381192,
"expires_at": 1725381492,
"rp_id": "rp_0000000000000001",
"oprf_key_id": "0x1",
"session_id": null,
"action": "0x000000000000000000000000000000000000000000000000000000000000002a",
"signature": "0xa1fd06f0d8ceb541f6096fe2e865063eac1ff085c9d2bac2eedcc9ed03804bfc18d956b38c5ac3a8f7e71fde43deff3bda254d369c699f3c7a3f8e6b8477a5f51c",
"nonce": "0x0000000000000000000000000000000000000000000000000000000000000001",
"from_the_future": true,
"proof_requests": [
{
"identifier": "orb",
"issuer_schema_id": 1,
"also_from_the_future": {"nested": [1, 2, 3]}
}
]
}"#;
let req = ProofRequest::from_json(json).expect("unknown fields must be tolerated");
assert_eq!(req.id, "req_abc123");
assert_eq!(req.requests.len(), 1);
assert_eq!(req.requests[0].identifier, "orb");
}
#[test]
fn response_json_tolerates_unknown_fields() {
let json = r#"{
"id": "req_18c0f7f03e7d",
"version": 1,
"from_the_future": "yes",
"responses": [
{
"identifier": "orb",
"issuer_schema_id": 100,
"proof": "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000",
"nullifier": "nil_00000000000000000000000000000000000000000000000000000000000003e9",
"expires_at_min": 1725381192,
"claims": ["0x01", "0x02"]
}
]
}"#;
let resp = ProofResponse::from_json(json).expect("unknown fields must be tolerated");
assert_eq!(resp.successful_credentials(), vec![100]);
assert!(resp.responses[0].is_uniqueness());
}
#[test]
fn response_json_parse() {
let ok_json = r#"{
"id": "req_18c0f7f03e7d",
"version": 1,
"responses": [
{
"identifier": "orb",
"issuer_schema_id": 100,
"proof": "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000",
"nullifier": "nil_00000000000000000000000000000000000000000000000000000000000003e9",
"expires_at_min": 1725381192
}
]
}"#;
let ok = ProofResponse::from_json(ok_json).unwrap();
assert_eq!(ok.successful_credentials(), vec![100]);
assert!(ok.responses[0].is_uniqueness());
let canonical_session_nullifier = serde_json::to_string(
&SessionNullifier::new(test_field_element(1001), test_action(42)).unwrap(),
)
.unwrap();
let sess_json_canonical = format!(
r#"{{
"id": "req_18c0f7f03e7d",
"version": 1,
"session_id": "session_00000000000000000000000000000000000000000000000000000000000003ea0100000000000000000000000000000000000000000000000000000000000001",
"responses": [
{{
"identifier": "orb",
"issuer_schema_id": 100,
"proof": "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000",
"session_nullifier": {canonical_session_nullifier},
"expires_at_min": 1725381192
}}
]
}}"#
);
let sess_canonical = ProofResponse::from_json(&sess_json_canonical).unwrap();
assert_eq!(sess_canonical.successful_credentials(), vec![100]);
assert!(sess_canonical.responses[0].is_session());
assert_eq!(
sess_canonical.session_id.unwrap().oprf_seed.to_u256(),
uint!(0x0100000000000000000000000000000000000000000000000000000000000001_U256)
);
}
#[test]
fn request_rejects_duplicate_issuer_schema_ids_on_parse() {
let req = ProofRequest {
id: "req_dup".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::None,
action: Some(test_field_element(5)),
created_at: 1_725_381_192,
expires_at: 1_725_381_492,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![
RequestItem {
identifier: "test_req_1".into(),
issuer_schema_id: 1,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "test_req_2".into(),
issuer_schema_id: 1, signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
],
constraints: None,
};
let json = req.to_json().unwrap();
let err = ProofRequest::from_json(&json).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("duplicate issuer schema id"),
"Expected error message to contain 'duplicate issuer schema id', got: {msg}"
);
}
#[test]
fn response_with_error_has_empty_responses_and_fails_validation() {
let request = ProofRequest {
id: "req_error".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::None,
action: Some(FieldElement::ZERO),
created_at: 1_735_689_600,
expires_at: 1_735_689_600,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![RequestItem {
identifier: "orb".into(),
issuer_schema_id: 1,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
}],
constraints: None,
};
let error_response = ProofResponse {
id: "req_error".into(),
version: RequestVersion::V1,
session_id: None,
error: Some("credential_not_available".into()),
responses: vec![], };
let err = request.validate_response(&error_response).unwrap_err();
assert!(matches!(err, ValidationError::ProofGenerationFailed(_)));
if let ValidationError::ProofGenerationFailed(msg) = err {
assert_eq!(msg, "credential_not_available");
}
assert_eq!(error_response.successful_credentials(), Vec::<u64>::new());
let expr = ConstraintExpr::All {
all: vec![ConstraintNode::Type("orb".into())],
};
assert!(!error_response.constraints_satisfied(&expr));
}
#[test]
fn response_error_json_parse() {
let error_json = r#"{
"id": "req_error",
"version": 1,
"error": "credential_not_available",
"responses": []
}"#;
let error_resp = ProofResponse::from_json(error_json).unwrap();
assert_eq!(error_resp.error, Some("credential_not_available".into()));
assert_eq!(error_resp.responses.len(), 0);
assert_eq!(error_resp.successful_credentials(), Vec::<u64>::new());
}
#[test]
fn credentials_to_prove_none_constraints_requires_all_and_drops_if_missing() {
let req = ProofRequest {
id: "req".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::None,
action: Some(test_field_element(5)),
created_at: 1_735_689_600,
expires_at: 1_735_689_600, rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![
RequestItem {
identifier: "orb".into(),
issuer_schema_id: 100,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "passport".into(),
issuer_schema_id: 101,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
],
constraints: None,
};
let available_ok: HashSet<u64> = [100, 101].into_iter().collect();
let sel_ok = req.credentials_to_prove(&available_ok).unwrap();
assert_eq!(sel_ok.len(), 2);
assert_eq!(sel_ok[0].issuer_schema_id, 100);
assert_eq!(sel_ok[1].issuer_schema_id, 101);
let available_missing: HashSet<u64> = std::iter::once(100).collect();
assert!(req.credentials_to_prove(&available_missing).is_none());
}
#[test]
fn credentials_to_prove_with_constraints_all_and_any() {
let orb_id = 100;
let passport_id = 101;
let national_id = 102;
let req = ProofRequest {
id: "req".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::None,
action: Some(test_field_element(1)),
created_at: 1_735_689_600,
expires_at: 1_735_689_600, rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![
RequestItem {
identifier: "orb".into(),
issuer_schema_id: orb_id,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "passport".into(),
issuer_schema_id: passport_id,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "national_id".into(),
issuer_schema_id: national_id,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
],
constraints: Some(ConstraintExpr::All {
all: vec![
ConstraintNode::Type("orb".into()),
ConstraintNode::Expr(ConstraintExpr::Any {
any: vec![
ConstraintNode::Type("passport".into()),
ConstraintNode::Type("national_id".into()),
],
}),
],
}),
};
let available1: HashSet<u64> = [orb_id, passport_id].into_iter().collect();
let sel1 = req.credentials_to_prove(&available1).unwrap();
assert_eq!(sel1.len(), 2);
assert_eq!(sel1[0].issuer_schema_id, orb_id);
assert_eq!(sel1[1].issuer_schema_id, passport_id);
let available2: HashSet<u64> = [orb_id, national_id].into_iter().collect();
let sel2 = req.credentials_to_prove(&available2).unwrap();
assert_eq!(sel2.len(), 2);
assert_eq!(sel2[0].issuer_schema_id, orb_id);
assert_eq!(sel2[1].issuer_schema_id, national_id);
let available3: HashSet<u64> = std::iter::once(passport_id).collect();
assert!(req.credentials_to_prove(&available3).is_none());
}
#[test]
fn credentials_to_prove_with_constraints_enumerate() {
let orb_id = 100;
let passport_id = 101;
let national_id = 102;
let req = ProofRequest {
id: "req".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::None,
action: Some(test_field_element(1)),
created_at: 1_735_689_600,
expires_at: 1_735_689_600,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![
RequestItem {
identifier: "orb".into(),
issuer_schema_id: orb_id,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "passport".into(),
issuer_schema_id: passport_id,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "national_id".into(),
issuer_schema_id: national_id,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
],
constraints: Some(ConstraintExpr::Enumerate {
enumerate: vec![
ConstraintNode::Type("passport".into()),
ConstraintNode::Type("national_id".into()),
],
}),
};
let available1: HashSet<u64> = [orb_id, passport_id].into_iter().collect();
let sel1 = req.credentials_to_prove(&available1).unwrap();
assert_eq!(sel1.len(), 1);
assert_eq!(sel1[0].issuer_schema_id, passport_id);
let available2: HashSet<u64> = [orb_id, passport_id, national_id].into_iter().collect();
let sel2 = req.credentials_to_prove(&available2).unwrap();
assert_eq!(sel2.len(), 2);
assert_eq!(sel2[0].issuer_schema_id, passport_id);
assert_eq!(sel2[1].issuer_schema_id, national_id);
let available3: HashSet<u64> = std::iter::once(orb_id).collect();
assert!(req.credentials_to_prove(&available3).is_none());
}
#[test]
fn credentials_to_prove_with_constraints_all_and_enumerate() {
let orb_id = 100;
let passport_id = 101;
let national_id = 102;
let req = ProofRequest {
id: "req".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::None,
action: Some(test_field_element(1)),
created_at: 1_735_689_600,
expires_at: 1_735_689_600,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![
RequestItem {
identifier: "orb".into(),
issuer_schema_id: orb_id,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "passport".into(),
issuer_schema_id: passport_id,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
RequestItem {
identifier: "national_id".into(),
issuer_schema_id: national_id,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
},
],
constraints: Some(ConstraintExpr::All {
all: vec![
ConstraintNode::Type("orb".into()),
ConstraintNode::Expr(ConstraintExpr::Enumerate {
enumerate: vec![
ConstraintNode::Type("passport".into()),
ConstraintNode::Type("national_id".into()),
],
}),
],
}),
};
let available1: HashSet<u64> = [orb_id, passport_id].into_iter().collect();
let sel1 = req.credentials_to_prove(&available1).unwrap();
assert_eq!(sel1.len(), 2);
assert_eq!(sel1[0].issuer_schema_id, orb_id);
assert_eq!(sel1[1].issuer_schema_id, passport_id);
let available2: HashSet<u64> = [orb_id, passport_id, national_id].into_iter().collect();
let sel2 = req.credentials_to_prove(&available2).unwrap();
assert_eq!(sel2.len(), 3);
assert_eq!(sel2[0].issuer_schema_id, orb_id);
assert_eq!(sel2[1].issuer_schema_id, passport_id);
assert_eq!(sel2[2].issuer_schema_id, national_id);
let available3: HashSet<u64> = std::iter::once(orb_id).collect();
assert!(req.credentials_to_prove(&available3).is_none());
}
#[test]
fn request_item_effective_expires_at_min_defaults_to_created_at() {
let request_created_at = 1_735_689_600; let custom_expires_at = 1_735_862_400;
let item_with_none = RequestItem {
identifier: "test".into(),
issuer_schema_id: 100,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
};
assert_eq!(
item_with_none.effective_expires_at_min(request_created_at),
request_created_at,
"When expires_at_min is None, should default to request created_at"
);
let item_with_custom = RequestItem {
identifier: "test".into(),
issuer_schema_id: 100,
signal: None,
genesis_issued_at_min: None,
expires_at_min: Some(custom_expires_at),
};
assert_eq!(
item_with_custom.effective_expires_at_min(request_created_at),
custom_expires_at,
"When expires_at_min is Some, should use that explicit value"
);
}
#[test]
fn validate_response_checks_expires_at_min_matches() {
let request_created_at = 1_735_689_600; let custom_expires_at = 1_735_862_400;
let request = ProofRequest {
id: "req_expires_test".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::None,
action: Some(test_field_element(1)),
created_at: request_created_at,
expires_at: request_created_at + 300,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![
RequestItem {
identifier: "orb".into(),
issuer_schema_id: 100,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None, },
RequestItem {
identifier: "document".into(),
issuer_schema_id: 101,
signal: None,
genesis_issued_at_min: None,
expires_at_min: Some(custom_expires_at), },
],
constraints: None,
};
let valid_response = ProofResponse {
id: "req_expires_test".into(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![
ResponseItem::new_uniqueness(
"orb".into(),
100,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1001)),
request_created_at, ),
ResponseItem::new_uniqueness(
"document".into(),
101,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1002)),
custom_expires_at, ),
],
};
assert!(request.validate_response(&valid_response).is_ok());
let invalid_response_1 = ProofResponse {
id: "req_expires_test".into(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![
ResponseItem::new_uniqueness(
"orb".into(),
100,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1001)),
custom_expires_at, ),
ResponseItem::new_uniqueness(
"document".into(),
101,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1002)),
custom_expires_at,
),
],
};
let err1 = request.validate_response(&invalid_response_1).unwrap_err();
assert!(matches!(
err1,
ValidationError::ExpiresAtMinMismatch(_, _, _)
));
if let ValidationError::ExpiresAtMinMismatch(identifier, expected, got) = err1 {
assert_eq!(identifier, "orb");
assert_eq!(expected, request_created_at);
assert_eq!(got, custom_expires_at);
}
let invalid_response_2 = ProofResponse {
id: "req_expires_test".into(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![
ResponseItem::new_uniqueness(
"orb".into(),
100,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1001)),
request_created_at,
),
ResponseItem::new_uniqueness(
"document".into(),
101,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1002)),
request_created_at, ),
],
};
let err2 = request.validate_response(&invalid_response_2).unwrap_err();
assert!(matches!(
err2,
ValidationError::ExpiresAtMinMismatch(_, _, _)
));
if let ValidationError::ExpiresAtMinMismatch(identifier, expected, got) = err2 {
assert_eq!(identifier, "document");
assert_eq!(expected, custom_expires_at);
assert_eq!(got, request_created_at);
}
}
#[test]
fn test_validate_proof_type_is_strict() {
let uniqueness_with_create = ProofRequest {
id: "req_bound_uniqueness".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::Create,
action: Some(FieldElement::ZERO),
created_at: 1_735_689_600,
expires_at: 1_735_689_900,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![RequestItem {
identifier: "orb".into(),
issuer_schema_id: 1,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
}],
constraints: None,
};
assert!(uniqueness_with_create.validate_proof_type().is_ok());
assert!(uniqueness_with_create.binds_session());
assert!(!uniqueness_with_create.is_session_proof());
let uniqueness_without_action = ProofRequest {
action: None,
..uniqueness_with_create.clone()
};
assert!(matches!(
uniqueness_without_action.validate_proof_type(),
Err(PrimitiveError::InvalidInput { attribute, .. }) if attribute == "action"
));
let plain_uniqueness = ProofRequest {
session_id: SessionRef::None,
..uniqueness_with_create.clone()
};
assert!(plain_uniqueness.validate_proof_type().is_ok());
assert!(!plain_uniqueness.binds_session());
for prefix in [OprfPrefix::SessionOprfSeed, OprfPrefix::SessionAction] {
let session_prefixed_action = ProofRequest {
action: Some(test_action_with_prefix(prefix, 42)),
..uniqueness_with_create.clone()
};
assert!(matches!(
session_prefixed_action.validate_proof_type(),
Err(PrimitiveError::InvalidInput { attribute, .. }) if attribute == "action"
));
}
let uniqueness_with_existing = ProofRequest {
session_id: SessionRef::Existing(test_session_id(1)),
..uniqueness_with_create.clone()
};
assert!(matches!(
uniqueness_with_existing.validate_proof_type(),
Err(PrimitiveError::InvalidInput { attribute, .. }) if attribute == "session_id"
));
assert!(!uniqueness_with_existing.binds_session());
let session_without_session = ProofRequest {
proof_type: ProofType::Session,
session_id: SessionRef::None,
action: None,
..uniqueness_with_create.clone()
};
assert!(matches!(
session_without_session.validate_proof_type(),
Err(PrimitiveError::InvalidInput { attribute, .. }) if attribute == "session_id"
));
let session_create = ProofRequest {
proof_type: ProofType::Session,
session_id: SessionRef::Create,
action: None,
..uniqueness_with_create.clone()
};
assert!(session_create.validate_proof_type().is_ok());
assert!(session_create.is_session_proof());
assert!(!session_create.binds_session());
let session_existing = ProofRequest {
proof_type: ProofType::Session,
session_id: SessionRef::Existing(test_session_id(1)),
action: None,
..uniqueness_with_create.clone()
};
assert!(session_existing.validate_proof_type().is_ok());
for session_id in [SessionRef::Create, SessionRef::Existing(test_session_id(1))] {
let session_with_action = ProofRequest {
proof_type: ProofType::Session,
session_id,
action: Some(FieldElement::ZERO),
..uniqueness_with_create.clone()
};
assert!(matches!(
session_with_action.validate_proof_type(),
Err(PrimitiveError::InvalidInput { attribute, .. }) if attribute == "action"
));
}
}
#[test]
fn test_bound_uniqueness_request_parses_with_default_proof_type() {
let request = ProofRequest {
id: "req_bound".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::Create,
action: Some(FieldElement::ZERO),
created_at: 1_735_689_600,
expires_at: 1_735_689_900,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![RequestItem {
identifier: "orb".into(),
issuer_schema_id: 1,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
}],
constraints: None,
};
let mut value: serde_json::Value =
serde_json::from_str(&request.to_json().unwrap()).unwrap();
value.as_object_mut().unwrap().remove("proof_type");
let parsed = ProofRequest::from_json(&value.to_string()).unwrap();
assert_eq!(parsed.proof_type, ProofType::Uniqueness);
assert!(parsed.binds_session());
}
#[test]
fn test_request_absent_session_id_defaults_to_none() {
let request = ProofRequest {
id: "req_plain".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::None,
action: Some(FieldElement::ZERO),
created_at: 1_735_689_600,
expires_at: 1_735_689_900,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![RequestItem {
identifier: "orb".into(),
issuer_schema_id: 1,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
}],
constraints: None,
};
let json = request.to_json().unwrap();
let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(value["session_id"].is_null());
value.as_object_mut().unwrap().remove("session_id");
let parsed = ProofRequest::from_json(&value.to_string()).unwrap();
assert_eq!(parsed.session_id, SessionRef::None);
assert!(!parsed.binds_session());
}
#[test]
fn test_validate_response_uniqueness_rejects_existing_session() {
let session_id = test_session_id(7);
let request = ProofRequest {
id: "req_bound".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::Existing(session_id),
action: Some(FieldElement::ZERO),
created_at: 1_735_689_600,
expires_at: 1_735_689_900,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![RequestItem {
identifier: "orb".into(),
issuer_schema_id: 1,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
}],
constraints: None,
};
let response = ProofResponse {
id: request.id.clone(),
version: RequestVersion::V1,
session_id: Some(session_id),
error: None,
responses: vec![ResponseItem::new_uniqueness(
"orb".into(),
1,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1001)),
1_735_689_600,
)],
};
assert!(matches!(
request.validate_response(&response),
Err(ValidationError::InvalidProofRequest(_))
));
let plain_request = ProofRequest {
session_id: SessionRef::None,
..request
};
assert!(matches!(
plain_request.validate_response(&response),
Err(ValidationError::UnexpectedSessionId)
));
}
#[test]
fn proof_type_wire_encoding_is_stable() {
assert_eq!(
serde_json::to_string(&ProofType::Uniqueness).unwrap(),
"\"uniqueness\""
);
assert_eq!(
serde_json::to_string(&ProofType::Session).unwrap(),
"\"session\""
);
assert_eq!(
serde_json::from_str::<ProofType>("\"uniqueness\"").unwrap(),
ProofType::Uniqueness
);
assert_eq!(
serde_json::from_str::<ProofType>("\"session\"").unwrap(),
ProofType::Session
);
}
#[test]
fn proof_type_byte_encoding_is_stable() {
assert_eq!(ProofType::Uniqueness as u8, 0x00);
assert_eq!(ProofType::Session as u8, 0x02);
}
#[test]
fn test_validate_response_session_create_requires_minted_session_id() {
let request = ProofRequest {
id: "req_create_session".into(),
version: RequestVersion::V1,
proof_type: ProofType::Session,
session_id: SessionRef::Create,
action: None,
created_at: 1_735_689_600,
expires_at: 1_735_689_900,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![RequestItem {
identifier: "orb".into(),
issuer_schema_id: 1,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
}],
constraints: None,
};
let missing_session = ProofResponse {
id: request.id.clone(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![ResponseItem::new_session(
"orb".into(),
1,
ZeroKnowledgeProof::default(),
SessionNullifier::new(test_field_element(1001), test_action(42)).unwrap(),
1_735_689_600,
)],
};
assert!(matches!(
request.validate_response(&missing_session),
Err(ValidationError::MissingSessionId)
));
let valid_response = ProofResponse {
session_id: Some(SessionId::default()),
..missing_session
};
assert!(request.validate_response(&valid_response).is_ok());
}
#[test]
fn test_validate_response_uniqueness_create_requires_minted_session_id() {
let request = ProofRequest {
id: "req_uniqueness_create".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::Create,
action: Some(FieldElement::ZERO),
created_at: 1_735_689_600,
expires_at: 1_735_689_900,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![RequestItem {
identifier: "orb".into(),
issuer_schema_id: 1,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
}],
constraints: None,
};
let missing_session = ProofResponse {
id: request.id.clone(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![ResponseItem::new_uniqueness(
"orb".into(),
1,
ZeroKnowledgeProof::default(),
Nullifier::new(test_field_element(1001)),
1_735_689_600,
)],
};
assert!(matches!(
request.validate_response(&missing_session),
Err(ValidationError::MissingSessionId)
));
let valid_response = ProofResponse {
session_id: Some(SessionId::default()),
..missing_session
};
assert!(request.validate_response(&valid_response).is_ok());
}
#[test]
fn test_validate_response_requires_session_id_in_response() {
let request = ProofRequest {
id: "req_session".into(),
version: RequestVersion::V1,
proof_type: ProofType::Session,
session_id: SessionRef::Existing(SessionId::default()),
action: None,
created_at: 1_735_689_600,
expires_at: 1_735_689_900,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![RequestItem {
identifier: "orb".into(),
issuer_schema_id: 1,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
}],
constraints: None,
};
let response_missing_session_id = ProofResponse {
id: "req_session".into(),
version: RequestVersion::V1,
session_id: None, error: None,
responses: vec![ResponseItem::new_session(
"orb".into(),
1,
ZeroKnowledgeProof::default(),
SessionNullifier::new(test_field_element(1001), test_action(42)).unwrap(),
1_735_689_600,
)],
};
let err = request
.validate_response(&response_missing_session_id)
.unwrap_err();
assert!(matches!(err, ValidationError::SessionIdMismatch));
}
#[test]
fn test_validate_response_requires_session_nullifier_for_session_proof() {
let request = ProofRequest {
id: "req_session".into(),
version: RequestVersion::V1,
proof_type: ProofType::Session,
session_id: SessionRef::Existing(SessionId::default()),
action: None,
created_at: 1_735_689_600,
expires_at: 1_735_689_900,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![RequestItem {
identifier: "orb".into(),
issuer_schema_id: 1,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
}],
constraints: None,
};
let response_wrong_nullifier_type = ProofResponse {
id: "req_session".into(),
version: RequestVersion::V1,
session_id: Some(SessionId::default()),
error: None,
responses: vec![ResponseItem::new_uniqueness(
"orb".into(),
1,
ZeroKnowledgeProof::default(),
Nullifier::from(test_field_element(1001)), 1_735_689_600,
)],
};
let err = request
.validate_response(&response_wrong_nullifier_type)
.unwrap_err();
assert!(matches!(
err,
ValidationError::MissingSessionNullifier(ref id) if id == "orb"
));
}
#[test]
fn test_validate_response_requires_nullifier_for_uniqueness_proof() {
let request = ProofRequest {
id: "req_uniqueness".into(),
version: RequestVersion::V1,
proof_type: ProofType::Uniqueness,
session_id: SessionRef::None,
action: Some(test_field_element(42)),
created_at: 1_735_689_600,
expires_at: 1_735_689_900,
rp_id: RpId::new(1),
oprf_key_id: OprfKeyId::new(uint!(1_U160)),
signature: test_signature(),
nonce: test_nonce(),
requests: vec![RequestItem {
identifier: "orb".into(),
issuer_schema_id: 1,
signal: None,
genesis_issued_at_min: None,
expires_at_min: None,
}],
constraints: None,
};
let response_wrong_nullifier_type = ProofResponse {
id: "req_uniqueness".into(),
version: RequestVersion::V1,
session_id: None,
error: None,
responses: vec![ResponseItem::new_session(
"orb".into(),
1,
ZeroKnowledgeProof::default(),
SessionNullifier::new(test_field_element(1001), test_action(42)).unwrap(), 1_735_689_600,
)],
};
let err = request
.validate_response(&response_wrong_nullifier_type)
.unwrap_err();
assert!(matches!(
err,
ValidationError::MissingNullifier(ref id) if id == "orb"
));
}
}