world-id-primitives 0.14.0

Contains the raw base primitives (without implementations) for the World ID Protocol.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
use crate::serde_utils;
use alloy_primitives::U256;
use ark_bn254::Bn254;
use circom_types::groth16::Proof;
use serde::{Deserialize, Serialize};
use taceo_oprf::types::api::{CloseFrameMessage, OprfRequestAuthenticatorError};

use crate::{FieldElement, rp::RpId};

/// The most significant byte (MSB) of an OPRF input field element, which separates
/// the domains in which the input may be used.
///
/// All three prefixes share one OPRF key and query structure, so the MSB is what keeps
/// their outputs from colliding. The variants are exhaustive for the nullifier and
/// session OPRF inputs.
///
/// These variants are the authoritative OPRF input domains.
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OprfPrefix {
    /// The default domain: uniqueness actions, and any other OPRF input that is not
    /// session-scoped.
    ///
    /// Unlike the session prefixes, `0x00` elements are not minted by the protocol —
    /// uniqueness actions are chosen freely by the RP. Checking for this prefix
    /// therefore only rules out session reuse; it implies nothing else about the value.
    Uniqueness = 0x00,
    /// The [`crate::SessionId::oprf_seed`].
    SessionOprfSeed = 0x01,
    /// The action used to compute the inner nullifier in a [`crate::SessionNullifier`].
    SessionAction = 0x02,
}

/// Generation and validation of domain-prefixed OPRF inputs. See [`OprfPrefix`].
pub trait OprfPrefixedFieldElement {
    /// Generate a randomized field element carrying the given OPRF prefix.
    fn random_with_prefix<R: rand::CryptoRng + rand::RngCore>(
        rng: &mut R,
        prefix: OprfPrefix,
    ) -> FieldElement;

    /// Returns whether the field element carries the given OPRF prefix.
    fn has_prefix(&self, prefix: OprfPrefix) -> bool;
}

impl OprfPrefixedFieldElement for FieldElement {
    fn random_with_prefix<R: rand::CryptoRng + rand::RngCore>(
        rng: &mut R,
        prefix: OprfPrefix,
    ) -> FieldElement {
        let mut bytes = [0u8; 32];
        rng.fill_bytes(&mut bytes);
        bytes[0] = prefix as u8;
        Self::from_be_bytes(&bytes).expect(
            "should always fit in the field because with 0x02 or lower as the MSB, the field element < babyjubjub modulus",
        )
    }

    fn has_prefix(&self, prefix: OprfPrefix) -> bool {
        self.to_be_bytes()[0] == prefix as u8
    }
}

/// A module identifier for OPRF evaluations.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum OprfModule {
    /// Oprf module for generating nullifiers
    Nullifier,
    /// Oprf module for generating credential blinding factors
    CredentialBlindingFactor,
    /// Oprf module for generating internal nullifiers for sessions proofs and the `session_id_r_seed`
    Session,
}

/// Additional data needed to reconstruct the message covered by an RP signature.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RpSignatureVerification {
    /// A uniqueness action covered by the RP signature.
    ///
    /// This is used on create-and-bind session-seed queries, whose OPRF action is the
    /// session seed rather than the uniqueness action included in the signed message.
    UniquenessAction {
        /// The RP-signed uniqueness action (MSB `0x00`).
        action: FieldElement,
    },
}

impl std::fmt::Display for OprfModule {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Nullifier => write!(f, "nullifier"),
            Self::CredentialBlindingFactor => write!(f, "credential_blinding_factor"),
            Self::Session => write!(f, "session"),
        }
    }
}

/// A request sent by a client for OPRF nullifier authentication.
#[derive(Clone, Serialize, Deserialize)]
pub struct NullifierOprfRequestAuthV1 {
    /// Zero-knowledge proof provided by the user.
    pub proof: Proof<Bn254>,
    /// The action
    #[serde(with = "ark_serde_compat::field")]
    pub action: ark_babyjubjub::Fq,
    /// The nonce
    #[serde(with = "ark_serde_compat::field")]
    pub nonce: ark_babyjubjub::Fq,
    /// The Merkle root associated with this request.
    #[serde(with = "ark_serde_compat::field")]
    pub merkle_root: ark_babyjubjub::Fq,
    /// The current time stamp (unix secs)
    #[serde(alias = "current_time_stamp")]
    pub created_at: u64,
    /// Expiration timestamp of the request (unix secs)
    #[serde(alias = "expiration_timestamp")]
    pub expires_at: u64,
    /// The RP's signature on the request, see `compute_rp_signature_msg` for details.
    ///
    /// Can be `None` if the RP is a WIP101 conform contract.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub signature: Option<alloy_primitives::Signature>,
    /// The `rp_id`
    pub rp_id: RpId,
    /// Auxiliary data for WIP101 verification.
    ///
    /// Maximum length of this field is 1024 bytes. If the RP is not backed by a WIP101 signer contract, you can omit this value is it will be ignored by the OPRF-nodes anyways.
    ///
    /// If the RP signer is an WIP101 backed contract, this data is send verbatim to the contract without any form of validation (except size).
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        with = "serde_utils::hex_bytes_opt"
    )]
    pub wip101_data: Option<Vec<u8>>,
    /// Additional data needed to reconstruct the RP-signed message.
    ///
    /// Currently only valid on create-and-bind session-seed queries (see
    /// [`OprfPrefix::SessionOprfSeed`]) from EOA-backed RPs.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rp_signature_verification: Option<RpSignatureVerification>,
}

/// A request sent by a client for OPRF credential blinding factor authentication.
#[derive(Clone, Serialize, Deserialize)]
pub struct CredentialBlindingFactorOprfRequestAuthV1 {
    /// Zero-knowledge proof provided by the user.
    pub proof: Proof<Bn254>,
    /// The action
    #[serde(with = "ark_serde_compat::field")]
    pub action: ark_babyjubjub::Fq,
    /// The nonce
    #[serde(with = "ark_serde_compat::field")]
    pub nonce: ark_babyjubjub::Fq,
    /// The Merkle root associated with this request.
    #[serde(with = "ark_serde_compat::field")]
    pub merkle_root: ark_babyjubjub::Fq,
    /// The `issuer_schema_id` in the `CredentialSchemaIssuerRegistry` contract
    pub issuer_schema_id: u64,
}

/// Concrete error type returned by OPRF request authentication.
///
/// Variants map 1-to-1 with the numeric close-frame error codes in [`error_codes`], which are
/// sent to the client over the WebSocket connection when authentication fails.
#[derive(Copy, Clone, Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WorldIdRequestAuthError {
    /// Unknown RP. The RP is likely not registerd in the `RpRegistry`.
    #[error("unknown_rp")]
    UnknownRp,
    /// Inactive RP. The RP was deactivated in the `RpRegistry`. Inactive RPs cannot
    /// request proofs. If you are the RP, call `updateRp` to re-activate.
    #[error("inactive_rp")]
    InactiveRp,
    /// **Only valid for Credential Blinding Factor generation**.
    ///
    /// The `issuerSchemaId` provided to generate a blinding factor is not valid. The
    /// value is either incorrect or the `issuerSchemaId` is not correctly registered in
    /// the `CredentialSchemaIssuerRegistry`.
    #[error("unknown_schema_issuer_id")]
    UnknownSchemaIssuerId,
    /// The request timestamp is too old. If you are the RP please sign a request with
    /// a fresh timestamp.
    #[error("timestamp_too_old")]
    CreatedAtTooOld,
    /// The request timestamp is too far in the future. If you are the RP please sign a request with
    /// a fresh timestamp.
    #[error("timestamp_too_far_in_future")]
    CreatedAtTooFarInFuture,
    /// The expires_at timestamp is too far in the future. If you are the RP please sign a new request with
    /// a new expires at.
    #[error("expires_at_too_far_in_future")]
    ExpiresAtTooFarInFuture,
    /// The timestamp cannot be parsed as it was not a valid unix epoch timestamp.
    #[error("invalid_timestamp")]
    InvalidTimestamp,
    /// The RP signature has expired. If you are the RP please sign a request
    /// with a fresh timestamp.
    #[error("rp_signature_expired")]
    RpSignatureExpired,
    /// The RP's signature on the request could not be verified. The signature may be
    /// incorrect, the wrong public key used, or does not match the expected message.
    #[error("invalid_rp_signature")]
    InvalidRpSignature,
    /// Requester did not provide a signature of the RP, but the RP's signer
    /// is an EOA.
    /// Empty signatures are only supported for WIP101 backed RPs.
    #[error("rp_signature_missing")]
    RpSignatureMissing,
    /// RP signer is an EOA but request had auxiliary data.
    #[error("wip101_aux_data_on_eoa")]
    Wip101AuxDataOnEoa,
    /// A duplicate nonce was detected. Duplicate nonces are not allowed to prevent
    /// replay attacks. If you are the RP please generate a new nonce.
    #[error("duplicate_nonce")]
    DuplicateNonce,
    /// The provided Merkle root is not valid for the `WorldIDRegistry`. This can happen
    /// when the inclusion proof is too old. Please compute a new inclusion proof.
    #[error("invalid_merkle_root")]
    InvalidMerkleRoot,
    /// The client Query Proof, used to authenticate the user did not verify correctly
    /// for the provided inputs.
    #[error("invalid_query_proof")]
    InvalidQueryProof,
    /// **Only valid for Credential Blinding Factor generation**.
    ///
    /// The provided action for the credential issuer blinding factor computation is not valid.
    #[error("invalid_action_for_blinding_factor")]
    InvalidActionSchemaIssuer,
    /// The provided action for the nullifier computation is not valid. Nullifier actions must
    /// start with `0x00` (MSB).
    #[error("invalid_action_for_nullifier")]
    InvalidActionNullifier,
    /// **Only valid for Session Proofs**.
    ///
    /// The provided action for the Session Proof is invalid. See [`OprfPrefix`] for the valid action
    /// prefixes.
    #[error("invalid_action_for_session")]
    InvalidActionSession,
    /// The provided RP signature verification data is invalid or not allowed on this query.
    ///
    /// Verification data is only valid on create-and-bind session-seed queries from
    /// EOA-backed RPs and must carry a uniqueness action (MSB `0x00`).
    #[error("invalid_rp_signature_verification")]
    InvalidRpSignatureVerification,
    /// The RP signer is a contract but does not implement the WIP101 interface.
    #[error("wip101_incompatible_rp_signer")]
    Wip101IncompatibleRpSigner,
    /// The WIP101 signer contract rejected the request.
    ///
    /// The contract may optionally return a rejection code (`U256`), which is captured in this error as `Some(code)`. If no additional code is provided, this will be `None`.
    ///
    /// When constructing this variant from just the `CloseFrame`'s `code`, the contract's additional code will be lost. The additional code, if any, is sent as `reason` in the `CloseFrame`.
    #[error("wip101_verification_failed")]
    Wip101VerificationFailed(Option<U256>),
    /// Invalid custom revert for WIP101 contract.
    ///
    /// WIP101 specifies that contracts must revert with `error RpInvalidRequest(uint256 code)` but contract reverted with unknown error.
    #[error("wip101_custom_revert")]
    Wip101CustomRevert,
    /// Provided auxiliary data is too large.
    ///
    /// WIP101 specifies that provided `data` must be smaller than 1024 bytes.
    #[error("wip101_aux_data_too_large")]
    Wip101AuxDataTooLarge,
    /// WIP101 signature verification ran into timeout.
    #[error("wip101_verification_timeout")]
    Wip101VerificationTimeout,
    /// Doing WIP101/ERC165 check on RP's signer ran into timeout.
    #[error("wip101_account_check_timeout")]
    Wip101AccountCheckTimeout,
    /// Internal server error.
    #[error("internal_server_error")]
    Internal,
    /// Unknown error code not mapped to a known variant.
    #[error("unknown_error_{0}")]
    Unknown(u16),
}

/// The actor where a provided OPRF error likely originated and with ability
/// to fix it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorActor {
    /// The Relying Party requesting a Proof
    Rp,
    /// The Issuer of a Credential
    Issuer,
    /// The Authenticator of the user
    Authenticator,
    /// Error attributable to an OPRF node
    OprfNode,
}

impl WorldIdRequestAuthError {
    /// Return the [`ErrorActor`] associated for this error.
    #[must_use]
    pub const fn as_actor(&self) -> ErrorActor {
        match self {
            Self::UnknownRp
            | Self::InactiveRp
            | Self::CreatedAtTooOld
            | Self::CreatedAtTooFarInFuture
            | Self::ExpiresAtTooFarInFuture
            | Self::InvalidTimestamp
            | Self::RpSignatureExpired
            | Self::InvalidRpSignature
            | Self::DuplicateNonce
            | Self::InvalidActionNullifier
            | Self::Wip101IncompatibleRpSigner
            | Self::Wip101VerificationFailed(_)
            | Self::Wip101CustomRevert
            | Self::Wip101VerificationTimeout
            | Self::Wip101AuxDataOnEoa
            | Self::Wip101AuxDataTooLarge
            | Self::Wip101AccountCheckTimeout => ErrorActor::Rp,
            Self::UnknownSchemaIssuerId => ErrorActor::Issuer,
            Self::InvalidMerkleRoot
            | Self::InvalidQueryProof
            | Self::InvalidActionSchemaIssuer
            | Self::InvalidActionSession
            | Self::InvalidRpSignatureVerification
            | Self::RpSignatureMissing => ErrorActor::Authenticator,
            Self::Internal | Self::Unknown(_) => ErrorActor::OprfNode,
        }
    }
}

impl From<u16> for WorldIdRequestAuthError {
    fn from(value: u16) -> Self {
        match value {
            error_codes::UNKNOWN_RP => Self::UnknownRp,
            error_codes::INACTIVE_RP => Self::InactiveRp,
            error_codes::CREATED_AT_TOO_OLD => Self::CreatedAtTooOld,
            error_codes::INVALID_RP_SIGNATURE => Self::InvalidRpSignature,
            error_codes::DUPLICATE_NONCE => Self::DuplicateNonce,
            error_codes::INVALID_MERKLE_ROOT => Self::InvalidMerkleRoot,
            error_codes::INVALID_QUERY_PROOF => Self::InvalidQueryProof,
            error_codes::INVALID_ACTION_SCHEMA_ISSUER => Self::InvalidActionSchemaIssuer,
            error_codes::UNKNOWN_SCHEMA_ISSUER => Self::UnknownSchemaIssuerId,
            error_codes::INVALID_ACTION_NULLIFIER => Self::InvalidActionNullifier,
            error_codes::INVALID_ACTION_SESSION => Self::InvalidActionSession,
            error_codes::INVALID_RP_SIGNATURE_VERIFICATION => Self::InvalidRpSignatureVerification,
            error_codes::RP_SIGNATURE_EXPIRED => Self::RpSignatureExpired,
            error_codes::RP_SIGNATURE_MISSING => Self::RpSignatureMissing,
            error_codes::INVALID_TIMESTAMP => Self::InvalidTimestamp,
            error_codes::CREATED_AT_TOO_FAR_IN_FUTURE => Self::CreatedAtTooFarInFuture,
            error_codes::EXPIRES_AT_TOO_FAR_IN_FUTURE => Self::ExpiresAtTooFarInFuture,
            error_codes::WIP101_INCOMPATIBLE_RP_SIGNER => Self::Wip101IncompatibleRpSigner,
            error_codes::WIP101_VERIFICATION_TIMEOUT => Self::Wip101VerificationTimeout,
            error_codes::WIP101_ACCOUNT_CHECK_TIMEOUT => Self::Wip101AccountCheckTimeout,
            // we lost the additional code when converting from just the u16
            error_codes::WIP101_VERIFICATION_FAILED => Self::Wip101VerificationFailed(None),
            error_codes::WIP101_CUSTOM_REVERT => Self::Wip101CustomRevert,
            error_codes::INTERNAL => Self::Internal,
            other => Self::Unknown(other),
        }
    }
}

impl From<WorldIdRequestAuthError> for u16 {
    fn from(value: WorldIdRequestAuthError) -> Self {
        match value {
            WorldIdRequestAuthError::UnknownRp => error_codes::UNKNOWN_RP,
            WorldIdRequestAuthError::InactiveRp => error_codes::INACTIVE_RP,
            WorldIdRequestAuthError::CreatedAtTooOld => error_codes::CREATED_AT_TOO_OLD,
            WorldIdRequestAuthError::ExpiresAtTooFarInFuture => {
                error_codes::EXPIRES_AT_TOO_FAR_IN_FUTURE
            }
            WorldIdRequestAuthError::InvalidTimestamp => error_codes::INVALID_TIMESTAMP,
            WorldIdRequestAuthError::InvalidRpSignature => error_codes::INVALID_RP_SIGNATURE,
            WorldIdRequestAuthError::RpSignatureMissing => error_codes::RP_SIGNATURE_MISSING,
            WorldIdRequestAuthError::DuplicateNonce => error_codes::DUPLICATE_NONCE,
            WorldIdRequestAuthError::InvalidMerkleRoot => error_codes::INVALID_MERKLE_ROOT,
            WorldIdRequestAuthError::InvalidQueryProof => error_codes::INVALID_QUERY_PROOF,
            WorldIdRequestAuthError::InvalidActionSchemaIssuer => {
                error_codes::INVALID_ACTION_SCHEMA_ISSUER
            }
            WorldIdRequestAuthError::UnknownSchemaIssuerId => error_codes::UNKNOWN_SCHEMA_ISSUER,
            WorldIdRequestAuthError::InvalidActionNullifier => {
                error_codes::INVALID_ACTION_NULLIFIER
            }
            WorldIdRequestAuthError::InvalidActionSession => error_codes::INVALID_ACTION_SESSION,
            WorldIdRequestAuthError::InvalidRpSignatureVerification => {
                error_codes::INVALID_RP_SIGNATURE_VERIFICATION
            }
            WorldIdRequestAuthError::RpSignatureExpired => error_codes::RP_SIGNATURE_EXPIRED,
            WorldIdRequestAuthError::CreatedAtTooFarInFuture => {
                error_codes::CREATED_AT_TOO_FAR_IN_FUTURE
            }
            WorldIdRequestAuthError::Wip101IncompatibleRpSigner => {
                error_codes::WIP101_INCOMPATIBLE_RP_SIGNER
            }
            WorldIdRequestAuthError::Wip101VerificationFailed(_) => {
                error_codes::WIP101_VERIFICATION_FAILED
            }
            WorldIdRequestAuthError::Wip101VerificationTimeout => {
                error_codes::WIP101_VERIFICATION_TIMEOUT
            }
            WorldIdRequestAuthError::Wip101CustomRevert => error_codes::WIP101_CUSTOM_REVERT,
            WorldIdRequestAuthError::Wip101AuxDataOnEoa => error_codes::WIP101_AUX_DATA_ON_EOA,
            WorldIdRequestAuthError::Wip101AuxDataTooLarge => {
                error_codes::WIP101_AUX_DATA_TOO_LARGE
            }
            WorldIdRequestAuthError::Wip101AccountCheckTimeout => {
                error_codes::WIP101_ACCOUNT_CHECK_TIMEOUT
            }
            WorldIdRequestAuthError::Internal => error_codes::INTERNAL,
            WorldIdRequestAuthError::Unknown(other) => other,
        }
    }
}

/// Numeric close-frame error codes sent to the client when [`WorldIdRequestAuthError`] occurs.
pub mod error_codes {
    /// Error code for [`super::WorldIdRequestAuthError::UnknownRp`].
    pub const UNKNOWN_RP: u16 = 4500;
    /// Error code for [`super::WorldIdRequestAuthError::CreatedAtTooOld`].
    pub const CREATED_AT_TOO_OLD: u16 = 4501;
    /// Error code for [`super::WorldIdRequestAuthError::InvalidRpSignature`].
    pub const INVALID_RP_SIGNATURE: u16 = 4502;
    /// Error code for [`super::WorldIdRequestAuthError::DuplicateNonce`].
    pub const DUPLICATE_NONCE: u16 = 4503;
    /// Error code for [`super::WorldIdRequestAuthError::InvalidMerkleRoot`].
    pub const INVALID_MERKLE_ROOT: u16 = 4504;
    /// Error code for [`super::WorldIdRequestAuthError::InvalidQueryProof`].
    pub const INVALID_QUERY_PROOF: u16 = 4505;
    /// Error code for [`super::WorldIdRequestAuthError::InvalidActionSchemaIssuer`].
    pub const INVALID_ACTION_SCHEMA_ISSUER: u16 = 4506;
    /// Error code for [`super::WorldIdRequestAuthError::UnknownSchemaIssuerId`].
    pub const UNKNOWN_SCHEMA_ISSUER: u16 = 4507;
    /// Error code for [`super::WorldIdRequestAuthError::InvalidActionNullifier`].
    pub const INVALID_ACTION_NULLIFIER: u16 = 4508;
    /// Error code for [`super::WorldIdRequestAuthError::InvalidActionSession`].
    pub const INVALID_ACTION_SESSION: u16 = 4509;
    /// Error code for [`super::WorldIdRequestAuthError::InactiveRp`].
    pub const INACTIVE_RP: u16 = 4510;
    /// Error code for [`super::WorldIdRequestAuthError::RpSignatureExpired`].
    pub const RP_SIGNATURE_EXPIRED: u16 = 4511;
    /// Error code for [`super::WorldIdRequestAuthError::InvalidTimestamp`].
    pub const INVALID_TIMESTAMP: u16 = 4512;
    /// Error code for [`super::WorldIdRequestAuthError::CreatedAtTooFarInFuture`].
    pub const CREATED_AT_TOO_FAR_IN_FUTURE: u16 = 4513;
    /// Error code for [`super::WorldIdRequestAuthError::Wip101IncompatibleRpSigner`].
    pub const WIP101_INCOMPATIBLE_RP_SIGNER: u16 = 4514;
    /// Error code for [`super::WorldIdRequestAuthError::Wip101VerificationFailed`].
    pub const WIP101_VERIFICATION_FAILED: u16 = 4515;
    /// Error code for [`super::WorldIdRequestAuthError::Wip101CustomRevert`].
    pub const WIP101_CUSTOM_REVERT: u16 = 4516;
    /// Error code for [`super::WorldIdRequestAuthError::Wip101AuxDataTooLarge`].
    pub const WIP101_AUX_DATA_TOO_LARGE: u16 = 4517;
    /// Error code for [`super::WorldIdRequestAuthError::RpSignatureMissing`]
    pub const RP_SIGNATURE_MISSING: u16 = 4518;
    /// Error code for [`super::WorldIdRequestAuthError::Wip101AuxDataOnEoa`]
    pub const WIP101_AUX_DATA_ON_EOA: u16 = 4519;
    /// Error code for [`super::WorldIdRequestAuthError::Wip101VerificationTimeout`]
    pub const WIP101_VERIFICATION_TIMEOUT: u16 = 4520;
    /// Error code for [`super::WorldIdRequestAuthError::Wip101AccountCheckTimeout`]
    pub const WIP101_ACCOUNT_CHECK_TIMEOUT: u16 = 4521;
    /// Error code for [`super::WorldIdRequestAuthError::ExpiresAtTooFarInFuture`].
    pub const EXPIRES_AT_TOO_FAR_IN_FUTURE: u16 = 4523;
    /// Error code for [`super::WorldIdRequestAuthError::InvalidRpSignatureVerification`].
    pub const INVALID_RP_SIGNATURE_VERIFICATION: u16 = 4524;
    /// Error code for [`super::WorldIdRequestAuthError::Internal`].
    pub const INTERNAL: u16 = 1011;
}

impl From<WorldIdRequestAuthError> for OprfRequestAuthenticatorError {
    fn from(value: WorldIdRequestAuthError) -> Self {
        let code = u16::from(value);
        let msg = match value {
            WorldIdRequestAuthError::UnknownRp => {
                taceo_oprf::types::close_frame_message!("unknown RP")
            }
            WorldIdRequestAuthError::CreatedAtTooOld => {
                taceo_oprf::types::close_frame_message!("created_at too old")
            }
            WorldIdRequestAuthError::CreatedAtTooFarInFuture => {
                taceo_oprf::types::close_frame_message!("created_at too far in future")
            }
            WorldIdRequestAuthError::ExpiresAtTooFarInFuture => {
                taceo_oprf::types::close_frame_message!("expires_at too far in the future")
            }
            WorldIdRequestAuthError::InvalidRpSignature => {
                taceo_oprf::types::close_frame_message!("signature from RP cannot be verified")
            }
            WorldIdRequestAuthError::RpSignatureMissing => {
                taceo_oprf::types::close_frame_message!("RP signature missing but signer is an EOA")
            }
            WorldIdRequestAuthError::DuplicateNonce => {
                taceo_oprf::types::close_frame_message!("signature nonce already used")
            }
            WorldIdRequestAuthError::InvalidMerkleRoot => {
                taceo_oprf::types::close_frame_message!("invalid merkle root")
            }
            WorldIdRequestAuthError::InvalidQueryProof => {
                taceo_oprf::types::close_frame_message!("cannot verify query proof")
            }
            WorldIdRequestAuthError::InvalidActionSchemaIssuer => {
                taceo_oprf::types::close_frame_message!(
                    "invalid action for credential sub blinding factor"
                )
            }
            WorldIdRequestAuthError::UnknownSchemaIssuerId => {
                taceo_oprf::types::close_frame_message!("unknown schema issuer id")
            }
            WorldIdRequestAuthError::InvalidActionNullifier => {
                taceo_oprf::types::close_frame_message!("invalid action for nullifier")
            }
            WorldIdRequestAuthError::InvalidActionSession => {
                taceo_oprf::types::close_frame_message!("invalid action for session proofs")
            }
            WorldIdRequestAuthError::InactiveRp => {
                taceo_oprf::types::close_frame_message!("inactive RP")
            }
            WorldIdRequestAuthError::RpSignatureExpired => {
                taceo_oprf::types::close_frame_message!("RP signature expired")
            }
            WorldIdRequestAuthError::InvalidTimestamp => {
                taceo_oprf::types::close_frame_message!("cannot parse timestamp on request")
            }
            WorldIdRequestAuthError::Wip101IncompatibleRpSigner => {
                taceo_oprf::types::close_frame_message!(
                    "RP has a contract backed signer but doesn't conform to WIP101"
                )
            }
            WorldIdRequestAuthError::Wip101CustomRevert => {
                taceo_oprf::types::close_frame_message!(
                    "RP signer contract reverted with custom error (and not error RpInvalidRequest(uint256 code);)"
                )
            }
            WorldIdRequestAuthError::Wip101VerificationFailed(None) => {
                // send empty message so that it is easier to parse the code in case there is any
                taceo_oprf::types::close_frame_message!("")
            }
            WorldIdRequestAuthError::Wip101VerificationTimeout => {
                taceo_oprf::types::close_frame_message!("WIP101 verification ran into timeout")
            }
            WorldIdRequestAuthError::Wip101VerificationFailed(Some(code)) => {
                // this should never truncate as code is a U256 encoded as hex
                CloseFrameMessage::new_truncate(format!("{:#x}", code))
            }
            WorldIdRequestAuthError::InvalidRpSignatureVerification => {
                taceo_oprf::types::close_frame_message!("Invalid RP signature verification data")
            }
            WorldIdRequestAuthError::Wip101AuxDataOnEoa => taceo_oprf::types::close_frame_message!(
                "Auxiliary data must be empty with EOA backed signer"
            ),
            WorldIdRequestAuthError::Wip101AuxDataTooLarge => {
                taceo_oprf::types::close_frame_message!(
                    "Auxiliary data for WIP101 contract too large - max 1024 bytes"
                )
            }
            WorldIdRequestAuthError::Wip101AccountCheckTimeout => {
                taceo_oprf::types::close_frame_message!(
                    "Ran into timeout while doing WIP101/ERC165 check on RP's signer"
                )
            }
            WorldIdRequestAuthError::Internal => {
                taceo_oprf::types::close_frame_message!("internal server error")
            }
            WorldIdRequestAuthError::Unknown(_) => {
                taceo_oprf::types::close_frame_message!("unknown")
            }
        };
        Self::with_message(code, msg)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const ALL_PREFIXES: [OprfPrefix; 3] = [
        OprfPrefix::Uniqueness,
        OprfPrefix::SessionOprfSeed,
        OprfPrefix::SessionAction,
    ];

    #[test]
    fn random_with_prefix_is_recognized_only_by_its_own_prefix() {
        let mut rng = rand::rngs::OsRng;
        for prefix in ALL_PREFIXES {
            let field_element = FieldElement::random_with_prefix(&mut rng, prefix);
            assert_eq!(field_element.to_be_bytes()[0], prefix as u8);
            for other in ALL_PREFIXES {
                assert_eq!(field_element.has_prefix(other), other == prefix);
            }
        }
    }

    /// A structurally valid Groth16 proof (BN254 generator points) for serde tests.
    fn test_proof() -> Proof<Bn254> {
        serde_json::from_value(serde_json::json!({
            "pi_a": ["1", "2", "1"],
            "pi_b": [
                [
                    "10857046999023057135944570762232829481370756359578518086990519993285655852781",
                    "11559732032986387107991004021392285783925812861821192530917403151452391805634"
                ],
                [
                    "8495653923123431417604973247489272438418190587263600148770280649306958101930",
                    "4082367875863433681332203403145435568316851327593401208105741076214120093531"
                ],
                ["1", "0"]
            ],
            "pi_c": ["1", "2", "1"],
            "protocol": "groth16",
            "curve": "bn128"
        }))
        .expect("valid test proof")
    }

    fn test_auth(
        rp_signature_verification: Option<RpSignatureVerification>,
    ) -> NullifierOprfRequestAuthV1 {
        NullifierOprfRequestAuthV1 {
            proof: test_proof(),
            action: ark_babyjubjub::Fq::from(1u64),
            nonce: ark_babyjubjub::Fq::from(2u64),
            merkle_root: ark_babyjubjub::Fq::from(3u64),
            created_at: 4,
            expires_at: 5,
            signature: None,
            rp_id: RpId::new(6),
            wip101_data: None,
            rp_signature_verification,
        }
    }

    #[test]
    fn nullifier_auth_rp_signature_verification_json_roundtrip() {
        let verification = RpSignatureVerification::UniquenessAction {
            action: FieldElement::from(42u64),
        };
        let auth = test_auth(Some(verification));
        let json = serde_json::to_string(&auth).unwrap();
        let parsed: NullifierOprfRequestAuthV1 = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.rp_signature_verification, Some(verification));
    }

    #[test]
    fn nullifier_auth_rp_signature_verification_cbor_roundtrip() {
        let verification = RpSignatureVerification::UniquenessAction {
            action: FieldElement::from(42u64),
        };
        let auth = test_auth(Some(verification));
        let mut bytes = Vec::new();
        ciborium::into_writer(&auth, &mut bytes).unwrap();
        let parsed: NullifierOprfRequestAuthV1 = ciborium::from_reader(bytes.as_slice()).unwrap();
        assert_eq!(parsed.rp_signature_verification, Some(verification));
    }

    #[test]
    fn error_code_roundtrip() {
        let codes: &[u16] = &[
            error_codes::UNKNOWN_RP,
            error_codes::CREATED_AT_TOO_OLD,
            error_codes::CREATED_AT_TOO_FAR_IN_FUTURE,
            error_codes::INVALID_RP_SIGNATURE,
            error_codes::DUPLICATE_NONCE,
            error_codes::INVALID_MERKLE_ROOT,
            error_codes::INVALID_QUERY_PROOF,
            error_codes::INVALID_ACTION_SCHEMA_ISSUER,
            error_codes::UNKNOWN_SCHEMA_ISSUER,
            error_codes::INVALID_ACTION_NULLIFIER,
            error_codes::INVALID_ACTION_SESSION,
            error_codes::INVALID_RP_SIGNATURE_VERIFICATION,
            error_codes::INACTIVE_RP,
            error_codes::RP_SIGNATURE_EXPIRED,
            error_codes::INVALID_TIMESTAMP,
            error_codes::WIP101_INCOMPATIBLE_RP_SIGNER,
            error_codes::WIP101_VERIFICATION_FAILED,
            error_codes::WIP101_CUSTOM_REVERT,
            error_codes::WIP101_AUX_DATA_TOO_LARGE,
            error_codes::RP_SIGNATURE_MISSING,
            error_codes::WIP101_AUX_DATA_ON_EOA,
            error_codes::WIP101_VERIFICATION_TIMEOUT,
            error_codes::WIP101_ACCOUNT_CHECK_TIMEOUT,
            error_codes::INTERNAL,
        ];
        for &code in codes {
            let error = WorldIdRequestAuthError::from(code);
            let back: u16 = error.into();
            assert_eq!(code, back, "roundtrip failed for code {code}");
        }
    }
}