Skip to main content

world_id_authenticator/
authenticator.rs

1//! This module contains all the base functionality to support Authenticators in World ID.
2//!
3//! An Authenticator is the application layer with which a user interacts with the Protocol.
4
5use std::sync::Arc;
6
7use crate::api_types::{
8    AccountInclusionProof, CreateAccountRequest, GatewayRequestState, GatewayStatusResponse,
9    IndexerAuthenticatorPubkeysResponse, IndexerErrorCode, IndexerPackedAccountRequest,
10    IndexerPackedAccountResponse, IndexerQueryRequest, IndexerSignatureNonceResponse,
11    InsertAuthenticatorRequest, RemoveAuthenticatorRequest, ServiceApiError,
12    UpdateAuthenticatorRequest,
13};
14use world_id_primitives::{
15    Credential, FieldElement, ProofRequest, RequestItem, ResponseItem, SessionNullifier, Signer,
16};
17
18use crate::registry::{
19    WorldIdRegistry::WorldIdRegistryInstance, domain, sign_insert_authenticator,
20    sign_remove_authenticator, sign_update_authenticator,
21};
22use alloy::{
23    primitives::{Address, U256},
24    providers::DynProvider,
25    uint,
26};
27use ark_serialize::CanonicalSerialize;
28use eddsa_babyjubjub::{EdDSAPublicKey, EdDSASignature};
29use groth16_material::circom::CircomGroth16Material;
30use reqwest::StatusCode;
31use secrecy::ExposeSecret;
32use taceo_oprf::client::Connector;
33pub use world_id_primitives::{Config, TREE_DEPTH, authenticator::ProtocolSigner};
34use world_id_primitives::{
35    PrimitiveError, ZeroKnowledgeProof,
36    authenticator::{
37        AuthenticatorPublicKeySet, SparseAuthenticatorPubkeysError,
38        decode_sparse_authenticator_pubkeys,
39    },
40    merkle::MerkleInclusionProof,
41};
42use world_id_proof::{
43    AuthenticatorProofInput,
44    credential_blinding_factor::OprfCredentialBlindingFactor,
45    nullifier::OprfNullifier,
46    proof::{ProofError, generate_nullifier_proof},
47};
48
49static MASK_RECOVERY_COUNTER: U256 =
50    uint!(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000_U256);
51static MASK_PUBKEY_ID: U256 =
52    uint!(0x00000000FFFFFFFF000000000000000000000000000000000000000000000000_U256);
53static MASK_LEAF_INDEX: U256 =
54    uint!(0x000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFF_U256);
55
56/// An Authenticator is the base layer with which a user interacts with the Protocol.
57pub struct Authenticator {
58    /// General configuration for the Authenticator.
59    pub config: Config,
60    /// The packed account data for the holder's World ID is a `uint256` defined in the `WorldIDRegistry` contract as:
61    /// `recovery_counter` (32 bits) | `pubkey_id` (commitment to all off-chain public keys) (32 bits) | `leaf_index` (192 bits)
62    pub packed_account_data: U256,
63    signer: Signer,
64    registry: Option<Arc<WorldIdRegistryInstance<DynProvider>>>,
65    http_client: reqwest::Client,
66    ws_connector: Connector,
67    query_material: Arc<CircomGroth16Material>,
68    nullifier_material: Arc<CircomGroth16Material>,
69}
70
71#[expect(clippy::missing_fields_in_debug)]
72impl std::fmt::Debug for Authenticator {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        f.debug_struct("Authenticator")
75            .field("config", &self.config)
76            .field("packed_account_data", &self.packed_account_data)
77            .field("signer", &self.signer)
78            .finish()
79    }
80}
81
82impl Authenticator {
83    async fn response_body_or_fallback(response: reqwest::Response) -> String {
84        response
85            .text()
86            .await
87            .unwrap_or_else(|e| format!("Unable to read response body: {e}"))
88    }
89
90    /// Initialize an Authenticator from a seed and config.
91    ///
92    /// This method will error if the World ID account does not exist on the registry.
93    ///
94    /// # Errors
95    /// - Will error if the provided seed is invalid (not 32 bytes).
96    /// - Will error if the RPC URL is invalid.
97    /// - Will error if there are contract call failures.
98    /// - Will error if the account does not exist (`AccountDoesNotExist`).
99    pub async fn init(
100        seed: &[u8],
101        config: Config,
102        query_material: Arc<CircomGroth16Material>,
103        nullifier_material: Arc<CircomGroth16Material>,
104    ) -> Result<Self, AuthenticatorError> {
105        let signer = Signer::from_seed_bytes(seed)?;
106
107        let registry = config.rpc_url().map_or_else(
108            || None,
109            |rpc_url| {
110                let provider = alloy::providers::ProviderBuilder::new()
111                    .with_chain_id(config.chain_id())
112                    .connect_http(rpc_url.clone());
113                Some(crate::registry::WorldIdRegistry::new(
114                    *config.registry_address(),
115                    alloy::providers::Provider::erased(provider),
116                ))
117            },
118        );
119
120        let http_client = reqwest::Client::new();
121
122        let packed_account_data = Self::get_packed_account_data(
123            signer.onchain_signer_address(),
124            registry.as_ref(),
125            &config,
126            &http_client,
127        )
128        .await?;
129
130        let mut root_store = rustls::RootCertStore::empty();
131        root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
132        let rustls_config = rustls::ClientConfig::builder()
133            .with_root_certificates(root_store)
134            .with_no_client_auth();
135        let ws_connector = Connector::Rustls(Arc::new(rustls_config));
136
137        Ok(Self {
138            packed_account_data,
139            signer,
140            config,
141            registry: registry.map(Arc::new),
142            http_client,
143            ws_connector,
144            query_material,
145            nullifier_material,
146        })
147    }
148
149    /// Registers a new World ID in the `WorldIDRegistry`.
150    ///
151    /// Given the registration process is asynchronous, this method will return a `InitializingAuthenticator`
152    /// object.
153    ///
154    /// # Errors
155    /// - See `init` for additional error details.
156    pub async fn register(
157        seed: &[u8],
158        config: Config,
159        recovery_address: Option<Address>,
160    ) -> Result<InitializingAuthenticator, AuthenticatorError> {
161        let http_client = reqwest::Client::new();
162        InitializingAuthenticator::new(seed, config, recovery_address, http_client).await
163    }
164
165    /// Initializes (if the World ID already exists in the registry) or registers a new World ID.
166    ///
167    /// The registration process is asynchronous and may take some time. This method will block
168    /// the thread until the registration is in a final state (success or terminal error). For better
169    /// user experience in end authenticator clients, it is recommended to implement custom polling logic.
170    ///
171    /// Explicit `init` or `register` calls are also recommended as the authenticator should know
172    /// if a new World ID should be truly created. For example, an authenticator may have been revoked
173    /// access to an existing World ID.
174    ///
175    /// # Errors
176    /// - See `init` for additional error details.
177    pub async fn init_or_register(
178        seed: &[u8],
179        config: Config,
180        query_material: Arc<CircomGroth16Material>,
181        nullifier_material: Arc<CircomGroth16Material>,
182        recovery_address: Option<Address>,
183    ) -> Result<Self, AuthenticatorError> {
184        match Self::init(
185            seed,
186            config.clone(),
187            query_material.clone(),
188            nullifier_material.clone(),
189        )
190        .await
191        {
192            Ok(authenticator) => Ok(authenticator),
193            Err(AuthenticatorError::AccountDoesNotExist) => {
194                // Authenticator is not registered, create it.
195                let http_client = reqwest::Client::new();
196                let initializing_authenticator = InitializingAuthenticator::new(
197                    seed,
198                    config.clone(),
199                    recovery_address,
200                    http_client,
201                )
202                .await?;
203
204                let backoff = backon::ExponentialBuilder::default()
205                    .with_min_delay(std::time::Duration::from_millis(800))
206                    .with_factor(1.5)
207                    .without_max_times()
208                    .with_total_delay(Some(std::time::Duration::from_secs(120)));
209
210                let poller = || async {
211                    let poll_status = initializing_authenticator.poll_status().await;
212                    let result = match poll_status {
213                        Ok(GatewayRequestState::Finalized { .. }) => Ok(()),
214                        Ok(GatewayRequestState::Failed { error_code, error }) => Err(
215                            PollResult::TerminalError(AuthenticatorError::RegistrationError {
216                                error_code: error_code.map(|v| v.to_string()).unwrap_or_default(),
217                                error_message: error,
218                            }),
219                        ),
220                        Err(AuthenticatorError::GatewayError { status, body }) => {
221                            if status.is_client_error() {
222                                Err(PollResult::TerminalError(
223                                    AuthenticatorError::GatewayError { status, body },
224                                ))
225                            } else {
226                                Err(PollResult::Retryable)
227                            }
228                        }
229                        _ => Err(PollResult::Retryable),
230                    };
231
232                    match result {
233                        Ok(()) => match Self::init(
234                            seed,
235                            config.clone(),
236                            query_material.clone(),
237                            nullifier_material.clone(),
238                        )
239                        .await
240                        {
241                            Ok(auth) => Ok(auth),
242                            Err(AuthenticatorError::AccountDoesNotExist) => {
243                                Err(PollResult::Retryable)
244                            }
245                            Err(e) => Err(PollResult::TerminalError(e)),
246                        },
247                        Err(e) => Err(e),
248                    }
249                };
250
251                let result = backon::Retryable::retry(poller, backoff)
252                    .when(|e| matches!(e, PollResult::Retryable))
253                    .await;
254
255                match result {
256                    Ok(authenticator) => Ok(authenticator),
257                    Err(PollResult::TerminalError(e)) => Err(e),
258                    Err(PollResult::Retryable) => Err(AuthenticatorError::Timeout),
259                }
260            }
261            Err(e) => Err(e),
262        }
263    }
264
265    /// Returns the packed account data for the holder's World ID.
266    ///
267    /// The packed account data is a 256 bit integer which includes the World ID's leaf index, their recovery counter,
268    /// and their pubkey id/commitment.
269    ///
270    /// # Errors
271    /// Will error if the network call fails or if the account does not exist.
272    pub async fn get_packed_account_data(
273        onchain_signer_address: Address,
274        registry: Option<&WorldIdRegistryInstance<DynProvider>>,
275        config: &Config,
276        http_client: &reqwest::Client,
277    ) -> Result<U256, AuthenticatorError> {
278        // If the registry is available through direct RPC calls, use it. Otherwise fallback to the indexer.
279        let raw_index = if let Some(registry) = registry {
280            // TODO: Better error handling to expose the specific failure
281            registry
282                .getPackedAccountData(onchain_signer_address)
283                .call()
284                .await?
285        } else {
286            let url = format!("{}/packed-account", config.indexer_url());
287            let req = IndexerPackedAccountRequest {
288                authenticator_address: onchain_signer_address,
289            };
290            let resp = http_client.post(&url).json(&req).send().await?;
291            let status = resp.status();
292            if !status.is_success() {
293                let body = Self::response_body_or_fallback(resp).await;
294                if let Ok(error_resp) =
295                    serde_json::from_str::<ServiceApiError<IndexerErrorCode>>(&body)
296                {
297                    return match error_resp.code {
298                        IndexerErrorCode::AccountDoesNotExist => {
299                            Err(AuthenticatorError::AccountDoesNotExist)
300                        }
301                        _ => Err(AuthenticatorError::IndexerError {
302                            status,
303                            body: error_resp.message,
304                        }),
305                    };
306                }
307                return Err(AuthenticatorError::IndexerError { status, body });
308            }
309
310            let response: IndexerPackedAccountResponse = resp.json().await?;
311            response.packed_account_data
312        };
313
314        if raw_index == U256::ZERO {
315            return Err(AuthenticatorError::AccountDoesNotExist);
316        }
317
318        Ok(raw_index)
319    }
320
321    /// Returns the k256 public key of the Authenticator signer which is used to verify on-chain operations,
322    /// chiefly with the `WorldIdRegistry` contract.
323    #[must_use]
324    pub const fn onchain_address(&self) -> Address {
325        self.signer.onchain_signer_address()
326    }
327
328    /// Returns the `EdDSA` public key of the Authenticator signer which is used to verify off-chain operations. For example,
329    /// the Nullifier Oracle uses it to verify requests for nullifiers.
330    #[must_use]
331    pub fn offchain_pubkey(&self) -> EdDSAPublicKey {
332        self.signer.offchain_signer_pubkey()
333    }
334
335    /// Returns the compressed `EdDSA` public key of the Authenticator signer which is used to verify off-chain operations.
336    /// For example, the Nullifier Oracle uses it to verify requests for nullifiers.
337    /// # Errors
338    /// Will error if the public key cannot be serialized.
339    pub fn offchain_pubkey_compressed(&self) -> Result<U256, AuthenticatorError> {
340        let pk = self.signer.offchain_signer_pubkey().pk;
341        let mut compressed_bytes = Vec::new();
342        pk.serialize_compressed(&mut compressed_bytes)
343            .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
344        Ok(U256::from_le_slice(&compressed_bytes))
345    }
346
347    /// Returns a reference to the `WorldIdRegistry` contract instance.
348    #[must_use]
349    pub fn registry(&self) -> Option<Arc<WorldIdRegistryInstance<DynProvider>>> {
350        self.registry.clone()
351    }
352
353    /// Returns the index for the holder's World ID.
354    ///
355    /// # Definition
356    ///
357    /// The `leaf_index` is the main (internal) identifier of a World ID. It is registered in
358    /// the `WorldIDRegistry` and represents the index at the Merkle tree where the World ID
359    /// resides.
360    ///
361    /// # Notes
362    /// - The `leaf_index` is used as input in the nullifier generation, ensuring a nullifier
363    ///   will always be the same for the same RP context and the same World ID (allowing for uniqueness).
364    /// - The `leaf_index` is generally not exposed outside Authenticators. It is not a secret because
365    ///   it's not exposed to RPs outside ZK-circuits, but the only acceptable exposure outside an Authenticator
366    ///   is to fetch Merkle inclusion proofs from an indexer or it may create a pseudonymous identifier.
367    /// - The `leaf_index` is stored as a `uint64` inside packed account data.
368    #[must_use]
369    pub fn leaf_index(&self) -> u64 {
370        (self.packed_account_data & MASK_LEAF_INDEX).to::<u64>()
371    }
372
373    /// Returns the recovery counter for the holder's World ID.
374    ///
375    /// The recovery counter is used to efficiently invalidate all the old keys when an account is recovered.
376    #[must_use]
377    pub fn recovery_counter(&self) -> U256 {
378        let recovery_counter = self.packed_account_data & MASK_RECOVERY_COUNTER;
379        recovery_counter >> 224
380    }
381
382    /// Returns the pubkey id (or commitment) for the holder's World ID.
383    ///
384    /// This is a commitment to all the off-chain public keys that are authorized to act on behalf of the holder.
385    #[must_use]
386    pub fn pubkey_id(&self) -> U256 {
387        let pubkey_id = self.packed_account_data & MASK_PUBKEY_ID;
388        pubkey_id >> 192
389    }
390
391    /// Fetches a Merkle inclusion proof for the holder's World ID given their account index.
392    ///
393    /// # Errors
394    /// - Will error if the provided indexer URL is not valid or if there are HTTP call failures.
395    /// - Will error if the user is not registered on the `WorldIDRegistry`.
396    pub async fn fetch_inclusion_proof(
397        &self,
398    ) -> Result<(MerkleInclusionProof<TREE_DEPTH>, AuthenticatorPublicKeySet), AuthenticatorError>
399    {
400        let url = format!("{}/inclusion-proof", self.config.indexer_url());
401        let req = IndexerQueryRequest {
402            leaf_index: self.leaf_index(),
403        };
404        let response = self.http_client.post(&url).json(&req).send().await?;
405        let status = response.status();
406        if !status.is_success() {
407            return Err(AuthenticatorError::IndexerError {
408                status,
409                body: Self::response_body_or_fallback(response).await,
410            });
411        }
412        let response = response.json::<AccountInclusionProof<TREE_DEPTH>>().await?;
413
414        Ok((response.inclusion_proof, response.authenticator_pubkeys))
415    }
416
417    /// Fetches the current authenticator public key set for the account.
418    ///
419    /// This is used by mutation operations to compute old/new offchain signer commitments
420    /// without requiring Merkle proof generation.
421    ///
422    /// # Errors
423    /// - Will error if the provided indexer URL is not valid or if there are HTTP call failures.
424    /// - Will error if the user is not registered on the `WorldIDRegistry`.
425    pub async fn fetch_authenticator_pubkeys(
426        &self,
427    ) -> Result<AuthenticatorPublicKeySet, AuthenticatorError> {
428        let url = format!("{}/authenticator-pubkeys", self.config.indexer_url());
429        let req = IndexerQueryRequest {
430            leaf_index: self.leaf_index(),
431        };
432        let response = self.http_client.post(&url).json(&req).send().await?;
433        let status = response.status();
434        if !status.is_success() {
435            return Err(AuthenticatorError::IndexerError {
436                status,
437                body: Self::response_body_or_fallback(response).await,
438            });
439        }
440        let response = response
441            .json::<IndexerAuthenticatorPubkeysResponse>()
442            .await?;
443        Self::decode_indexer_pubkeys(response.authenticator_pubkeys)
444    }
445
446    /// Returns the signing nonce for the holder's World ID.
447    ///
448    /// # Errors
449    /// Will return an error if the registry contract call fails.
450    pub async fn signing_nonce(&self) -> Result<U256, AuthenticatorError> {
451        let registry = self.registry();
452        if let Some(registry) = registry {
453            let nonce = registry.getSignatureNonce(self.leaf_index()).call().await?;
454            Ok(nonce)
455        } else {
456            let url = format!("{}/signature-nonce", self.config.indexer_url());
457            let req = IndexerQueryRequest {
458                leaf_index: self.leaf_index(),
459            };
460            let resp = self.http_client.post(&url).json(&req).send().await?;
461
462            let status = resp.status();
463            if !status.is_success() {
464                return Err(AuthenticatorError::IndexerError {
465                    status,
466                    body: Self::response_body_or_fallback(resp).await,
467                });
468            }
469
470            let response: IndexerSignatureNonceResponse = resp.json().await?;
471            Ok(response.signature_nonce)
472        }
473    }
474
475    /// Checks that the OPRF Nodes configuration is valid and returns the list of URLs and the threshold to use.
476    ///
477    /// # Errors
478    /// Will return an error if there are no OPRF Nodes configured or if the threshold is invalid.
479    fn check_oprf_config(&self) -> Result<(&[String], usize), AuthenticatorError> {
480        let services = self.config.nullifier_oracle_urls();
481        if services.is_empty() {
482            return Err(AuthenticatorError::Generic(
483                "No nullifier oracle URLs configured".to_string(),
484            ));
485        }
486        let requested_threshold = self.config.nullifier_oracle_threshold();
487        if requested_threshold == 0 {
488            return Err(AuthenticatorError::InvalidConfig {
489                attribute: "nullifier_oracle_threshold",
490                reason: "must be at least 1".to_string(),
491            });
492        }
493        let threshold = requested_threshold.min(services.len());
494        Ok((services, threshold))
495    }
496
497    fn decode_indexer_pubkeys(
498        pubkeys: Vec<Option<U256>>,
499    ) -> Result<AuthenticatorPublicKeySet, AuthenticatorError> {
500        decode_sparse_authenticator_pubkeys(pubkeys).map_err(|e| match e {
501            SparseAuthenticatorPubkeysError::SlotOutOfBounds {
502                slot_index,
503                max_supported_slot,
504            } => AuthenticatorError::InvalidIndexerPubkeySlot {
505                slot_index,
506                max_supported_slot,
507            },
508            SparseAuthenticatorPubkeysError::InvalidCompressedPubkey { slot_index, reason } => {
509                PrimitiveError::Deserialization(format!(
510                    "invalid authenticator public key returned by indexer at slot {slot_index}: {reason}"
511                ))
512                .into()
513            }
514        })
515    }
516
517    fn insert_or_reuse_authenticator_key(
518        key_set: &mut AuthenticatorPublicKeySet,
519        new_authenticator_pubkey: EdDSAPublicKey,
520    ) -> Result<usize, AuthenticatorError> {
521        if let Some(index) = key_set.iter().position(Option::is_none) {
522            key_set.try_set_at_index(index, new_authenticator_pubkey)?;
523            Ok(index)
524        } else {
525            key_set.try_push(new_authenticator_pubkey)?;
526            Ok(key_set.len() - 1)
527        }
528    }
529
530    /// Generates a nullifier for a World ID Proof (through OPRF Nodes).
531    ///
532    /// A nullifier is a unique, one-time use, anonymous identifier for a World ID
533    /// on a specific RP context. It is used to ensure that a single World ID can only
534    /// perform an action once.
535    ///
536    /// # Errors
537    ///
538    /// - Will raise a [`ProofError`] if there is any issue generating the nullifier. For example,
539    ///   network issues, unexpected incorrect responses from OPRF Nodes.
540    /// - Raises an error if the OPRF Nodes configuration is not correctly set.
541    pub async fn generate_nullifier(
542        &self,
543        proof_request: &ProofRequest,
544        inclusion_proof: MerkleInclusionProof<TREE_DEPTH>,
545        key_set: AuthenticatorPublicKeySet,
546    ) -> Result<OprfNullifier, AuthenticatorError> {
547        let (services, threshold) = self.check_oprf_config()?;
548        let key_index = key_set
549            .iter()
550            .position(|pk| {
551                pk.as_ref()
552                    .is_some_and(|pk| pk.pk == self.offchain_pubkey().pk)
553            })
554            .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
555
556        let authenticator_input = AuthenticatorProofInput::new(
557            key_set,
558            inclusion_proof,
559            self.signer
560                .offchain_signer_private_key()
561                .expose_secret()
562                .clone(),
563            key_index,
564        );
565
566        Ok(OprfNullifier::generate(
567            services,
568            threshold,
569            &self.query_material,
570            authenticator_input,
571            proof_request,
572            self.ws_connector.clone(),
573        )
574        .await?)
575    }
576
577    // TODO add more docs
578    /// Generates a blinding factor for a Credential sub (through OPRF Nodes).
579    ///
580    /// # Errors
581    ///
582    /// - Will raise a [`ProofError`] if there is any issue generating the blinding factor.
583    ///   For example, network issues, unexpected incorrect responses from OPRF Nodes.
584    /// - Raises an error if the OPRF Nodes configuration is not correctly set.
585    pub async fn generate_credential_blinding_factor(
586        &self,
587        issuer_schema_id: u64,
588    ) -> Result<FieldElement, AuthenticatorError> {
589        let (services, threshold) = self.check_oprf_config()?;
590
591        let (inclusion_proof, key_set) = self.fetch_inclusion_proof().await?;
592        let key_index = key_set
593            .iter()
594            .position(|pk| {
595                pk.as_ref()
596                    .is_some_and(|pk| pk.pk == self.offchain_pubkey().pk)
597            })
598            .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
599
600        let authenticator_input = AuthenticatorProofInput::new(
601            key_set,
602            inclusion_proof,
603            self.signer
604                .offchain_signer_private_key()
605                .expose_secret()
606                .clone(),
607            key_index,
608        );
609
610        let blinding_factor = OprfCredentialBlindingFactor::generate(
611            services,
612            threshold,
613            &self.query_material,
614            authenticator_input,
615            issuer_schema_id,
616            FieldElement::ZERO, // for now action is always zero, might change in future
617            self.ws_connector.clone(),
618        )
619        .await?;
620
621        Ok(blinding_factor.verifiable_oprf_output.output.into())
622    }
623
624    /// Generates a single World ID Proof from a provided `[ProofRequest]` and `[Credential]`. This
625    /// method generates the raw proof to be translated into a Uniqueness Proof or a Session Proof for the RP.
626    ///
627    /// This assumes the RP's `[ProofRequest]` has already been parsed to determine
628    /// which `[Credential]` is appropriate for the request. This method responds to a
629    /// specific `[RequestItem]` (a `[ProofRequest]` may contain multiple items).
630    ///
631    /// # Arguments
632    /// - `oprf_nullifier`: The `[OprfNullifier]` output generated from the `generate_nullifier` function.
633    /// - `request_item`: The specific `RequestItem` that is being resolved from the RP's `ProofRequest`.
634    /// - `credential`: The Credential to be used for the proof that fulfills the `RequestItem`.
635    /// - `credential_sub_blinding_factor`: The blinding factor for the Credential's sub.
636    /// - `session_id_r_seed`: The session ID random seed. Obtained from the RP's [`ProofRequest`].
637    /// - `session_id`: The expected session ID provided by the RP. Only needed for Session Proofs. Obtained from the RP's [`ProofRequest`].
638    /// - `request_timestamp`: The timestamp of the request. Obtained from the RP's [`ProofRequest`].
639    ///
640    /// # Errors
641    /// - Will error if the any of the provided parameters are not valid.
642    /// - Will error if any of the required network requests fail.
643    /// - Will error if the user does not have a registered World ID.
644    #[allow(clippy::too_many_arguments)]
645    pub fn generate_single_proof(
646        &self,
647        oprf_nullifier: OprfNullifier,
648        request_item: &RequestItem,
649        credential: &Credential,
650        credential_sub_blinding_factor: FieldElement,
651        session_id_r_seed: FieldElement,
652        session_id: Option<FieldElement>,
653        request_timestamp: u64,
654    ) -> Result<ResponseItem, AuthenticatorError> {
655        let mut rng = rand::rngs::OsRng;
656
657        let merkle_root: FieldElement = oprf_nullifier.query_proof_input.merkle_root.into();
658        let action_from_query: FieldElement = oprf_nullifier.query_proof_input.action.into();
659
660        let expires_at_min = request_item.effective_expires_at_min(request_timestamp);
661
662        let (proof, _public_inputs, nullifier) = generate_nullifier_proof(
663            &self.nullifier_material,
664            &mut rng,
665            credential,
666            credential_sub_blinding_factor,
667            oprf_nullifier,
668            request_item,
669            session_id,
670            session_id_r_seed,
671            expires_at_min,
672        )?;
673
674        let proof = ZeroKnowledgeProof::from_groth16_proof(&proof, merkle_root);
675
676        // Construct the appropriate response item based on proof type
677        let nullifier_fe: FieldElement = nullifier.into();
678        let response_item = if session_id.is_some() {
679            let session_nullifier = SessionNullifier::new(nullifier_fe, action_from_query);
680            ResponseItem::new_session(
681                request_item.identifier.clone(),
682                request_item.issuer_schema_id,
683                proof,
684                session_nullifier,
685                expires_at_min,
686            )
687        } else {
688            ResponseItem::new_uniqueness(
689                request_item.identifier.clone(),
690                request_item.issuer_schema_id,
691                proof,
692                nullifier_fe.into(),
693                expires_at_min,
694            )
695        };
696
697        Ok(response_item)
698    }
699
700    /// Inserts a new authenticator to the account.
701    ///
702    /// # Errors
703    /// Will error if the provided RPC URL is not valid or if there are HTTP call failures.
704    ///
705    /// # Note
706    /// TODO: After successfully inserting an authenticator, the `packed_account_data` should be
707    /// refreshed from the registry to reflect the new `pubkey_id` commitment.
708    pub async fn insert_authenticator(
709        &mut self,
710        new_authenticator_pubkey: EdDSAPublicKey,
711        new_authenticator_address: Address,
712    ) -> Result<String, AuthenticatorError> {
713        let leaf_index = self.leaf_index();
714        let nonce = self.signing_nonce().await?;
715        let mut key_set = self.fetch_authenticator_pubkeys().await?;
716        let old_offchain_signer_commitment = key_set.leaf_hash();
717        let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
718        let index =
719            Self::insert_or_reuse_authenticator_key(&mut key_set, new_authenticator_pubkey)?;
720        let new_offchain_signer_commitment = key_set.leaf_hash();
721
722        let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
723
724        #[allow(clippy::cast_possible_truncation)]
725        // truncating is intentional, and index will always fit in 32 bits
726        let signature = sign_insert_authenticator(
727            &self.signer.onchain_signer(),
728            leaf_index,
729            new_authenticator_address,
730            index as u32,
731            encoded_offchain_pubkey,
732            new_offchain_signer_commitment.into(),
733            nonce,
734            &eip712_domain,
735        )
736        .await
737        .map_err(|e| {
738            AuthenticatorError::Generic(format!("Failed to sign insert authenticator: {e}"))
739        })?;
740
741        #[allow(clippy::cast_possible_truncation)]
742        // truncating is intentional, and index will always fit in 32 bits
743        let req = InsertAuthenticatorRequest {
744            leaf_index,
745            new_authenticator_address,
746            pubkey_id: index as u32,
747            new_authenticator_pubkey: encoded_offchain_pubkey,
748            old_offchain_signer_commitment: old_offchain_signer_commitment.into(),
749            new_offchain_signer_commitment: new_offchain_signer_commitment.into(),
750            signature: signature.as_bytes().to_vec(),
751            nonce,
752        };
753
754        let resp = self
755            .http_client
756            .post(format!(
757                "{}/insert-authenticator",
758                self.config.gateway_url()
759            ))
760            .json(&req)
761            .send()
762            .await?;
763
764        let status = resp.status();
765        if status.is_success() {
766            let body: GatewayStatusResponse = resp.json().await?;
767            Ok(body.request_id)
768        } else {
769            let body_text = Self::response_body_or_fallback(resp).await;
770            Err(AuthenticatorError::GatewayError {
771                status,
772                body: body_text,
773            })
774        }
775    }
776
777    /// Updates an existing authenticator slot with a new authenticator.
778    ///
779    /// # Errors
780    /// Returns an error if the gateway rejects the request or a network error occurs.
781    ///
782    /// # Note
783    /// TODO: After successfully updating an authenticator, the `packed_account_data` should be
784    /// refreshed from the registry to reflect the new `pubkey_id` commitment.
785    pub async fn update_authenticator(
786        &mut self,
787        old_authenticator_address: Address,
788        new_authenticator_address: Address,
789        new_authenticator_pubkey: EdDSAPublicKey,
790        index: u32,
791    ) -> Result<String, AuthenticatorError> {
792        let leaf_index = self.leaf_index();
793        let nonce = self.signing_nonce().await?;
794        let mut key_set = self.fetch_authenticator_pubkeys().await?;
795        let old_commitment: U256 = key_set.leaf_hash().into();
796        let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
797        key_set.try_set_at_index(index as usize, new_authenticator_pubkey)?;
798        let new_commitment: U256 = key_set.leaf_hash().into();
799
800        let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
801
802        let signature = sign_update_authenticator(
803            &self.signer.onchain_signer(),
804            leaf_index,
805            old_authenticator_address,
806            new_authenticator_address,
807            index,
808            encoded_offchain_pubkey,
809            new_commitment,
810            nonce,
811            &eip712_domain,
812        )
813        .await
814        .map_err(|e| {
815            AuthenticatorError::Generic(format!("Failed to sign update authenticator: {e}"))
816        })?;
817
818        let req = UpdateAuthenticatorRequest {
819            leaf_index,
820            old_authenticator_address,
821            new_authenticator_address,
822            old_offchain_signer_commitment: old_commitment,
823            new_offchain_signer_commitment: new_commitment,
824            signature: signature.as_bytes().to_vec(),
825            nonce,
826            pubkey_id: index,
827            new_authenticator_pubkey: encoded_offchain_pubkey,
828        };
829
830        let resp = self
831            .http_client
832            .post(format!(
833                "{}/update-authenticator",
834                self.config.gateway_url()
835            ))
836            .json(&req)
837            .send()
838            .await?;
839
840        let status = resp.status();
841        if status.is_success() {
842            let gateway_resp: GatewayStatusResponse = resp.json().await?;
843            Ok(gateway_resp.request_id)
844        } else {
845            let body_text = Self::response_body_or_fallback(resp).await;
846            Err(AuthenticatorError::GatewayError {
847                status,
848                body: body_text,
849            })
850        }
851    }
852
853    /// Removes an authenticator from the account.
854    ///
855    /// # Errors
856    /// Returns an error if the gateway rejects the request or a network error occurs.
857    ///
858    /// # Note
859    /// TODO: After successfully removing an authenticator, the `packed_account_data` should be
860    /// refreshed from the registry to reflect the new `pubkey_id` commitment.
861    pub async fn remove_authenticator(
862        &mut self,
863        authenticator_address: Address,
864        index: u32,
865    ) -> Result<String, AuthenticatorError> {
866        let leaf_index = self.leaf_index();
867        let nonce = self.signing_nonce().await?;
868        let mut key_set = self.fetch_authenticator_pubkeys().await?;
869        let old_commitment: U256 = key_set.leaf_hash().into();
870        let existing_pubkey = key_set
871            .get(index as usize)
872            .ok_or(AuthenticatorError::PublicKeyNotFound)?;
873
874        let encoded_old_offchain_pubkey = existing_pubkey.to_ethereum_representation()?;
875
876        key_set.try_clear_at_index(index as usize)?;
877        let new_commitment: U256 = key_set.leaf_hash().into();
878
879        let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
880
881        let signature = sign_remove_authenticator(
882            &self.signer.onchain_signer(),
883            leaf_index,
884            authenticator_address,
885            index,
886            encoded_old_offchain_pubkey,
887            new_commitment,
888            nonce,
889            &eip712_domain,
890        )
891        .await
892        .map_err(|e| {
893            AuthenticatorError::Generic(format!("Failed to sign remove authenticator: {e}"))
894        })?;
895
896        let req = RemoveAuthenticatorRequest {
897            leaf_index,
898            authenticator_address,
899            old_offchain_signer_commitment: old_commitment,
900            new_offchain_signer_commitment: new_commitment,
901            signature: signature.as_bytes().to_vec(),
902            nonce,
903            pubkey_id: Some(index),
904            authenticator_pubkey: Some(encoded_old_offchain_pubkey),
905        };
906
907        let resp = self
908            .http_client
909            .post(format!(
910                "{}/remove-authenticator",
911                self.config.gateway_url()
912            ))
913            .json(&req)
914            .send()
915            .await?;
916
917        let status = resp.status();
918        if status.is_success() {
919            let gateway_resp: GatewayStatusResponse = resp.json().await?;
920            Ok(gateway_resp.request_id)
921        } else {
922            let body_text = Self::response_body_or_fallback(resp).await;
923            Err(AuthenticatorError::GatewayError {
924                status,
925                body: body_text,
926            })
927        }
928    }
929}
930
931/// Represents an account in the process of being initialized,
932/// i.e. it is not yet registered in the `WorldIDRegistry` contract.
933pub struct InitializingAuthenticator {
934    request_id: String,
935    http_client: reqwest::Client,
936    config: Config,
937}
938
939impl InitializingAuthenticator {
940    /// Returns the gateway request ID for this pending account creation.
941    #[must_use]
942    pub fn request_id(&self) -> &str {
943        &self.request_id
944    }
945
946    /// Creates a new World ID account by adding it to the registry using the gateway.
947    ///
948    /// # Errors
949    /// - See `Signer::from_seed_bytes` for additional error details.
950    /// - Will error if the gateway rejects the request or a network error occurs.
951    async fn new(
952        seed: &[u8],
953        config: Config,
954        recovery_address: Option<Address>,
955        http_client: reqwest::Client,
956    ) -> Result<Self, AuthenticatorError> {
957        let signer = Signer::from_seed_bytes(seed)?;
958
959        let mut key_set = AuthenticatorPublicKeySet::default();
960        key_set.try_push(signer.offchain_signer_pubkey())?;
961        let leaf_hash = key_set.leaf_hash();
962
963        let offchain_pubkey_compressed = {
964            let pk = signer.offchain_signer_pubkey().pk;
965            let mut compressed_bytes = Vec::new();
966            pk.serialize_compressed(&mut compressed_bytes)
967                .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
968            U256::from_le_slice(&compressed_bytes)
969        };
970
971        let req = CreateAccountRequest {
972            recovery_address,
973            authenticator_addresses: vec![signer.onchain_signer_address()],
974            authenticator_pubkeys: vec![offchain_pubkey_compressed],
975            offchain_signer_commitment: leaf_hash.into(),
976        };
977
978        let resp = http_client
979            .post(format!("{}/create-account", config.gateway_url()))
980            .json(&req)
981            .send()
982            .await?;
983
984        let status = resp.status();
985        if status.is_success() {
986            let body: GatewayStatusResponse = resp.json().await?;
987            Ok(Self {
988                request_id: body.request_id,
989                http_client,
990                config,
991            })
992        } else {
993            let body_text = Authenticator::response_body_or_fallback(resp).await;
994            Err(AuthenticatorError::GatewayError {
995                status,
996                body: body_text,
997            })
998        }
999    }
1000
1001    /// Poll the status of the World ID creation request.
1002    ///
1003    /// # Errors
1004    /// - Will error if the network request fails.
1005    /// - Will error if the gateway returns an error response.
1006    pub async fn poll_status(&self) -> Result<GatewayRequestState, AuthenticatorError> {
1007        let resp = self
1008            .http_client
1009            .get(format!(
1010                "{}/status/{}",
1011                self.config.gateway_url(),
1012                self.request_id
1013            ))
1014            .send()
1015            .await?;
1016
1017        let status = resp.status();
1018
1019        if status.is_success() {
1020            let body: GatewayStatusResponse = resp.json().await?;
1021            Ok(body.status)
1022        } else {
1023            let body_text = Authenticator::response_body_or_fallback(resp).await;
1024            Err(AuthenticatorError::GatewayError {
1025                status,
1026                body: body_text,
1027            })
1028        }
1029    }
1030}
1031
1032impl ProtocolSigner for Authenticator {
1033    fn sign(&self, message: FieldElement) -> EdDSASignature {
1034        self.signer
1035            .offchain_signer_private_key()
1036            .expose_secret()
1037            .sign(*message)
1038    }
1039}
1040
1041/// A trait for types that can be represented as a `U256` on-chain.
1042pub trait OnchainKeyRepresentable {
1043    /// Converts an off-chain public key into a `U256` representation for on-chain use in the `WorldIDRegistry` contract.
1044    ///
1045    /// The `U256` representation is a 32-byte little-endian encoding of the **compressed** (single point) public key.
1046    ///
1047    /// # Errors
1048    /// Will error if the public key unexpectedly fails to serialize.
1049    fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError>;
1050}
1051
1052impl OnchainKeyRepresentable for EdDSAPublicKey {
1053    // REVIEW: updating to BE
1054    fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError> {
1055        let mut compressed_bytes = Vec::new();
1056        self.pk
1057            .serialize_compressed(&mut compressed_bytes)
1058            .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
1059        Ok(U256::from_le_slice(&compressed_bytes))
1060    }
1061}
1062
1063/// Errors that can occur when interacting with the Authenticator.
1064#[derive(Debug, thiserror::Error)]
1065pub enum AuthenticatorError {
1066    /// Primitive error
1067    #[error(transparent)]
1068    PrimitiveError(#[from] PrimitiveError),
1069
1070    /// This operation requires a registered account and an account is not registered
1071    /// for this authenticator. Call `create_account` first to register it.
1072    #[error("Account is not registered for this authenticator.")]
1073    AccountDoesNotExist,
1074
1075    /// The account already exists for this authenticator. Call `leaf_index` to get the leaf index.
1076    #[error("Account already exists for this authenticator.")]
1077    AccountAlreadyExists,
1078
1079    /// An error occurred while interacting with the EVM contract.
1080    #[error("Error interacting with EVM contract: {0}")]
1081    ContractError(#[from] alloy::contract::Error),
1082
1083    /// Network/HTTP request error.
1084    #[error("Network error: {0}")]
1085    NetworkError(#[from] reqwest::Error),
1086
1087    /// Public key not found in the Authenticator public key set. Usually indicates the local state is out of sync with the registry.
1088    #[error("Public key not found.")]
1089    PublicKeyNotFound,
1090
1091    /// Gateway returned an error response.
1092    #[error("Gateway error (status {status}): {body}")]
1093    GatewayError {
1094        /// HTTP status code
1095        status: StatusCode,
1096        /// Response body
1097        body: String,
1098    },
1099
1100    /// Indexer returned an error response.
1101    #[error("Indexer error (status {status}): {body}")]
1102    IndexerError {
1103        /// HTTP status code
1104        status: StatusCode,
1105        /// Response body
1106        body: String,
1107    },
1108
1109    /// Account creation timed out while polling for confirmation.
1110    #[error("Account creation timed out")]
1111    Timeout,
1112
1113    /// Configuration is invalid or missing required values.
1114    #[error("Invalid configuration for {attribute}: {reason}")]
1115    InvalidConfig {
1116        /// The config attribute that is invalid.
1117        attribute: &'static str,
1118        /// Description of why it is invalid.
1119        reason: String,
1120    },
1121
1122    /// The provided credential is not valid for the provided proof request.
1123    #[error("The provided credential is not valid for the provided proof request")]
1124    InvalidCredentialForProofRequest,
1125
1126    /// Error during the World ID registration process.
1127    ///
1128    /// This usually occurs from an on-chain revert.
1129    #[error("Registration error ({error_code}): {error_message}")]
1130    RegistrationError {
1131        /// Error code from the registration process.
1132        error_code: String,
1133        /// Detailed error message.
1134        error_message: String,
1135    },
1136
1137    /// Error on proof generation
1138    #[error(transparent)]
1139    ProofError(#[from] ProofError),
1140
1141    /// Indexer returned an authenticator key slot that exceeds supported key capacity.
1142    #[error(
1143        "Invalid indexer authenticator pubkey slot {slot_index}; max supported slot is {max_supported_slot}"
1144    )]
1145    InvalidIndexerPubkeySlot {
1146        /// Slot index returned by the indexer.
1147        slot_index: usize,
1148        /// Highest supported slot index.
1149        max_supported_slot: usize,
1150    },
1151
1152    /// Generic error for other unexpected issues.
1153    #[error("{0}")]
1154    Generic(String),
1155}
1156
1157#[derive(Debug)]
1158enum PollResult {
1159    Retryable,
1160    TerminalError(AuthenticatorError),
1161}
1162
1163#[cfg(all(test, feature = "embed-zkeys"))]
1164mod tests {
1165    use super::*;
1166    use alloy::primitives::{U256, address};
1167    use std::sync::OnceLock;
1168    use world_id_primitives::authenticator::MAX_AUTHENTICATOR_KEYS;
1169
1170    fn test_materials() -> (Arc<CircomGroth16Material>, Arc<CircomGroth16Material>) {
1171        static QUERY: OnceLock<Arc<CircomGroth16Material>> = OnceLock::new();
1172        static NULLIFIER: OnceLock<Arc<CircomGroth16Material>> = OnceLock::new();
1173
1174        let query = QUERY.get_or_init(|| {
1175            Arc::new(world_id_proof::proof::load_embedded_query_material().unwrap())
1176        });
1177        let nullifier = NULLIFIER.get_or_init(|| {
1178            Arc::new(world_id_proof::proof::load_embedded_nullifier_material().unwrap())
1179        });
1180
1181        (Arc::clone(query), Arc::clone(nullifier))
1182    }
1183
1184    fn test_pubkey(seed_byte: u8) -> EdDSAPublicKey {
1185        Signer::from_seed_bytes(&[seed_byte; 32])
1186            .unwrap()
1187            .offchain_signer_pubkey()
1188    }
1189
1190    fn encoded_test_pubkey(seed_byte: u8) -> U256 {
1191        test_pubkey(seed_byte).to_ethereum_representation().unwrap()
1192    }
1193
1194    #[test]
1195    fn test_insert_or_reuse_authenticator_key_reuses_empty_slot() {
1196        let mut key_set =
1197            AuthenticatorPublicKeySet::new(vec![test_pubkey(1), test_pubkey(2), test_pubkey(4)])
1198                .unwrap();
1199        key_set[1] = None;
1200        let new_key = test_pubkey(3);
1201
1202        let index =
1203            Authenticator::insert_or_reuse_authenticator_key(&mut key_set, new_key).unwrap();
1204
1205        assert_eq!(index, 1);
1206        assert_eq!(key_set.len(), 3);
1207        assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(3).pk);
1208    }
1209
1210    #[test]
1211    fn test_insert_or_reuse_authenticator_key_appends_when_no_empty_slot() {
1212        let mut key_set = AuthenticatorPublicKeySet::new(vec![test_pubkey(1)]).unwrap();
1213        let new_key = test_pubkey(2);
1214
1215        let index =
1216            Authenticator::insert_or_reuse_authenticator_key(&mut key_set, new_key).unwrap();
1217
1218        assert_eq!(index, 1);
1219        assert_eq!(key_set.len(), 2);
1220        assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
1221    }
1222
1223    #[test]
1224    fn test_decode_indexer_pubkeys_trims_trailing_empty_slots() {
1225        let mut encoded_pubkeys = vec![Some(encoded_test_pubkey(1)), Some(encoded_test_pubkey(2))];
1226        encoded_pubkeys.extend(vec![None; MAX_AUTHENTICATOR_KEYS + 5]);
1227
1228        let key_set = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap();
1229
1230        assert_eq!(key_set.len(), 2);
1231        assert_eq!(key_set[0].as_ref().unwrap().pk, test_pubkey(1).pk);
1232        assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
1233    }
1234
1235    #[test]
1236    fn test_decode_indexer_pubkeys_rejects_used_slot_beyond_max() {
1237        let mut encoded_pubkeys = vec![None; MAX_AUTHENTICATOR_KEYS + 1];
1238        encoded_pubkeys[MAX_AUTHENTICATOR_KEYS] = Some(encoded_test_pubkey(1));
1239
1240        let error = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap_err();
1241        assert!(matches!(
1242            error,
1243            AuthenticatorError::InvalidIndexerPubkeySlot {
1244                slot_index,
1245                max_supported_slot
1246            } if slot_index == MAX_AUTHENTICATOR_KEYS && max_supported_slot == MAX_AUTHENTICATOR_KEYS - 1
1247        ));
1248    }
1249
1250    /// Tests that `get_packed_account_data` correctly fetches the packed account data from the indexer
1251    /// when no RPC is configured.
1252    #[tokio::test]
1253    async fn test_get_packed_account_data_from_indexer() {
1254        let mut server = mockito::Server::new_async().await;
1255        let indexer_url = server.url();
1256
1257        let test_address = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0");
1258        let expected_packed_index = U256::from(42);
1259
1260        let mock = server
1261            .mock("POST", "/packed-account")
1262            .match_header("content-type", "application/json")
1263            .match_body(mockito::Matcher::JsonString(
1264                serde_json::json!({
1265                    "authenticator_address": test_address
1266                })
1267                .to_string(),
1268            ))
1269            .with_status(200)
1270            .with_header("content-type", "application/json")
1271            .with_body(
1272                serde_json::json!({
1273                    "packed_account_data": format!("{:#x}", expected_packed_index)
1274                })
1275                .to_string(),
1276            )
1277            .create_async()
1278            .await;
1279
1280        let config = Config::new(
1281            None,
1282            1,
1283            address!("0x0000000000000000000000000000000000000001"),
1284            indexer_url,
1285            "http://gateway.example.com".to_string(),
1286            Vec::new(),
1287            2,
1288        )
1289        .unwrap();
1290
1291        let http_client = reqwest::Client::new();
1292
1293        let result = Authenticator::get_packed_account_data(
1294            test_address,
1295            None, // No registry, force indexer usage
1296            &config,
1297            &http_client,
1298        )
1299        .await
1300        .unwrap();
1301
1302        assert_eq!(result, expected_packed_index);
1303        mock.assert_async().await;
1304        drop(server);
1305    }
1306
1307    #[tokio::test]
1308    async fn test_get_packed_account_data_from_indexer_error() {
1309        let mut server = mockito::Server::new_async().await;
1310        let indexer_url = server.url();
1311
1312        let test_address = address!("0x0000000000000000000000000000000000000099");
1313
1314        let mock = server
1315            .mock("POST", "/packed-account")
1316            .with_status(400)
1317            .with_header("content-type", "application/json")
1318            .with_body(
1319                serde_json::json!({
1320                    "code": "account_does_not_exist",
1321                    "message": "There is no account for this authenticator address"
1322                })
1323                .to_string(),
1324            )
1325            .create_async()
1326            .await;
1327
1328        let config = Config::new(
1329            None,
1330            1,
1331            address!("0x0000000000000000000000000000000000000001"),
1332            indexer_url,
1333            "http://gateway.example.com".to_string(),
1334            Vec::new(),
1335            2,
1336        )
1337        .unwrap();
1338
1339        let http_client = reqwest::Client::new();
1340
1341        let result =
1342            Authenticator::get_packed_account_data(test_address, None, &config, &http_client).await;
1343
1344        assert!(matches!(
1345            result,
1346            Err(AuthenticatorError::AccountDoesNotExist)
1347        ));
1348        mock.assert_async().await;
1349        drop(server);
1350    }
1351
1352    #[tokio::test]
1353    async fn test_signing_nonce_from_indexer() {
1354        let mut server = mockito::Server::new_async().await;
1355        let indexer_url = server.url();
1356
1357        let leaf_index = U256::from(1);
1358        let expected_nonce = U256::from(5);
1359
1360        let mock = server
1361            .mock("POST", "/signature-nonce")
1362            .match_header("content-type", "application/json")
1363            .match_body(mockito::Matcher::JsonString(
1364                serde_json::json!({
1365                    "leaf_index": format!("{:#x}", leaf_index)
1366                })
1367                .to_string(),
1368            ))
1369            .with_status(200)
1370            .with_header("content-type", "application/json")
1371            .with_body(
1372                serde_json::json!({
1373                    "signature_nonce": format!("{:#x}", expected_nonce)
1374                })
1375                .to_string(),
1376            )
1377            .create_async()
1378            .await;
1379
1380        let config = Config::new(
1381            None,
1382            1,
1383            address!("0x0000000000000000000000000000000000000001"),
1384            indexer_url,
1385            "http://gateway.example.com".to_string(),
1386            Vec::new(),
1387            2,
1388        )
1389        .unwrap();
1390
1391        let (query_material, nullifier_material) = test_materials();
1392        let authenticator = Authenticator {
1393            config,
1394            packed_account_data: leaf_index, // This sets leaf_index() to 1
1395            signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1396            registry: None, // No registry - forces indexer usage
1397            http_client: reqwest::Client::new(),
1398            ws_connector: Connector::Plain,
1399            query_material,
1400            nullifier_material,
1401        };
1402
1403        let nonce = authenticator.signing_nonce().await.unwrap();
1404
1405        assert_eq!(nonce, expected_nonce);
1406        mock.assert_async().await;
1407        drop(server);
1408    }
1409
1410    #[tokio::test]
1411    async fn test_signing_nonce_from_indexer_error() {
1412        let mut server = mockito::Server::new_async().await;
1413        let indexer_url = server.url();
1414
1415        let mock = server
1416            .mock("POST", "/signature-nonce")
1417            .with_status(400)
1418            .with_header("content-type", "application/json")
1419            .with_body(
1420                serde_json::json!({
1421                    "code": "invalid_leaf_index",
1422                    "message": "Account index cannot be zero"
1423                })
1424                .to_string(),
1425            )
1426            .create_async()
1427            .await;
1428
1429        let config = Config::new(
1430            None,
1431            1,
1432            address!("0x0000000000000000000000000000000000000001"),
1433            indexer_url,
1434            "http://gateway.example.com".to_string(),
1435            Vec::new(),
1436            2,
1437        )
1438        .unwrap();
1439
1440        let (query_material, nullifier_material) = test_materials();
1441        let authenticator = Authenticator {
1442            config,
1443            packed_account_data: U256::ZERO,
1444            signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1445            registry: None,
1446            http_client: reqwest::Client::new(),
1447            ws_connector: Connector::Plain,
1448            query_material,
1449            nullifier_material,
1450        };
1451
1452        let result = authenticator.signing_nonce().await;
1453
1454        assert!(matches!(
1455            result,
1456            Err(AuthenticatorError::IndexerError { .. })
1457        ));
1458        mock.assert_async().await;
1459        drop(server);
1460    }
1461}