world_id_core/
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;
6use std::time::Duration;
7
8use crate::requests::ProofRequest;
9use crate::types::{
10    AccountInclusionProof, CreateAccountRequest, GatewayRequestState, GatewayStatusResponse,
11    IndexerErrorCode, IndexerPackedAccountRequest, IndexerPackedAccountResponse,
12    IndexerQueryRequest, IndexerSignatureNonceResponse, InsertAuthenticatorRequest,
13    RemoveAuthenticatorRequest, ServiceApiError, UpdateAuthenticatorRequest,
14};
15use crate::world_id_registry::WorldIdRegistry::{self, WorldIdRegistryInstance};
16use crate::world_id_registry::{
17    domain, sign_insert_authenticator, sign_remove_authenticator, sign_update_authenticator,
18};
19use crate::{Credential, FieldElement, Signer};
20use alloy::primitives::{Address, U256};
21use alloy::providers::{DynProvider, Provider, ProviderBuilder};
22use alloy::uint;
23use ark_babyjubjub::EdwardsAffine;
24use ark_bn254::Bn254;
25use ark_serialize::CanonicalSerialize;
26use backon::{ExponentialBuilder, Retryable};
27use circom_types::groth16::Proof;
28use eddsa_babyjubjub::{EdDSAPublicKey, EdDSASignature};
29use reqwest::StatusCode;
30use rustls::{ClientConfig, RootCertStore};
31use secrecy::ExposeSecret;
32use taceo_oprf_client::Connector;
33use taceo_oprf_types::ShareEpoch;
34use world_id_primitives::authenticator::AuthenticatorPublicKeySet;
35use world_id_primitives::merkle::MerkleInclusionProof;
36use world_id_primitives::proof::SingleProofInput;
37use world_id_primitives::PrimitiveError;
38pub use world_id_primitives::{authenticator::ProtocolSigner, Config, TREE_DEPTH};
39
40static MASK_RECOVERY_COUNTER: U256 =
41    uint!(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000_U256);
42static MASK_PUBKEY_ID: U256 =
43    uint!(0x00000000FFFFFFFF000000000000000000000000000000000000000000000000_U256);
44static MASK_LEAF_INDEX: U256 =
45    uint!(0x0000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF_U256);
46
47type UniquenessProof = (Proof<Bn254>, FieldElement);
48
49/// An Authenticator is the base layer with which a user interacts with the Protocol.
50pub struct Authenticator {
51    /// General configuration for the Authenticator.
52    pub config: Config,
53    /// The packed account data for the holder's World ID is a `uint256` defined in the `WorldIDRegistry` contract as:
54    /// `recovery_counter` (32 bits) | `pubkey_id` (commitment to all off-chain public keys) (32 bits) | `leaf_index` (192 bits)
55    pub packed_account_data: U256,
56    signer: Signer,
57    registry: Option<Arc<WorldIdRegistryInstance<DynProvider>>>,
58    http_client: reqwest::Client,
59    ws_connector: Connector,
60}
61
62#[expect(clippy::missing_fields_in_debug)]
63impl std::fmt::Debug for Authenticator {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        f.debug_struct("Authenticator")
66            .field("config", &self.config)
67            .field("packed_account_data", &self.packed_account_data)
68            .field("signer", &self.signer)
69            .finish()
70    }
71}
72
73impl Authenticator {
74    /// Initialize an Authenticator from a seed and config.
75    ///
76    /// This method will error if the World ID account does not exist on the registry.
77    ///
78    /// # Errors
79    /// - Will error if the provided seed is invalid (not 32 bytes).
80    /// - Will error if the RPC URL is invalid.
81    /// - Will error if there are contract call failures.
82    /// - Will error if the account does not exist (`AccountDoesNotExist`).
83    pub async fn init(seed: &[u8], config: Config) -> Result<Self, AuthenticatorError> {
84        let signer = Signer::from_seed_bytes(seed)?;
85
86        let registry = config.rpc_url().map_or_else(
87            || None,
88            |rpc_url| {
89                let provider = ProviderBuilder::new()
90                    .with_chain_id(config.chain_id())
91                    .connect_http(rpc_url.clone());
92                Some(WorldIdRegistry::new(
93                    *config.registry_address(),
94                    provider.erased(),
95                ))
96            },
97        );
98
99        let http_client = reqwest::Client::new();
100
101        let packed_account_data = Self::get_packed_account_data(
102            signer.onchain_signer_address(),
103            registry.as_ref(),
104            &config,
105            &http_client,
106        )
107        .await?;
108
109        let mut root_store = RootCertStore::empty();
110        root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
111        let rustls_config = ClientConfig::builder()
112            .with_root_certificates(root_store)
113            .with_no_client_auth();
114        let ws_connector = Connector::Rustls(Arc::new(rustls_config));
115
116        Ok(Self {
117            packed_account_data,
118            signer,
119            config,
120            registry: registry.map(Arc::new),
121            http_client,
122            ws_connector,
123        })
124    }
125
126    /// Registers a new World ID in the `WorldIDRegistry`.
127    ///
128    /// Given the registration process is asynchronous, this method will return a `InitializingAuthenticator`
129    /// object.
130    ///
131    /// # Errors
132    /// - See `init` for additional error details.
133    pub async fn register(
134        seed: &[u8],
135        config: Config,
136        recovery_address: Option<Address>,
137    ) -> Result<InitializingAuthenticator, AuthenticatorError> {
138        let http_client = reqwest::Client::new();
139        InitializingAuthenticator::new(seed, config, recovery_address, http_client).await
140    }
141
142    /// Initializes (if the World ID already exists in the registry) or registers a new World ID.
143    ///
144    /// The registration process is asynchronous and may take some time. This method will block
145    /// the thread until the registration is in a final state (success or terminal error). For better
146    /// user experience in end authenticator clients, it is recommended to implement custom polling logic.
147    ///
148    /// Explicit `init` or `register` calls are also recommended as the authenticator should know
149    /// if a new World ID should be truly created. For example, an authenticator may have been revoked
150    /// access to an existing World ID.
151    ///
152    /// # Errors
153    /// - See `init` for additional error details.
154    pub async fn init_or_register(
155        seed: &[u8],
156        config: Config,
157        recovery_address: Option<Address>,
158    ) -> Result<Self, AuthenticatorError> {
159        match Self::init(seed, config.clone()).await {
160            Ok(authenticator) => Ok(authenticator),
161            Err(AuthenticatorError::AccountDoesNotExist) => {
162                // Authenticator is not registered, create it.
163                let http_client = reqwest::Client::new();
164                let initializing_authenticator = InitializingAuthenticator::new(
165                    seed,
166                    config.clone(),
167                    recovery_address,
168                    http_client,
169                )
170                .await?;
171
172                let backoff = ExponentialBuilder::default()
173                    .with_min_delay(Duration::from_millis(800))
174                    .with_factor(1.5)
175                    .without_max_times()
176                    .with_total_delay(Some(Duration::from_secs(120)));
177
178                let poller = || async {
179                    let poll_status = initializing_authenticator.poll_status().await;
180                    let result = match poll_status {
181                        Ok(GatewayRequestState::Finalized { .. }) => Ok(()),
182                        Ok(GatewayRequestState::Failed { error_code, error }) => Err(
183                            PollResult::TerminalError(AuthenticatorError::RegistrationError {
184                                error_code: error_code.map(|v| v.to_string()).unwrap_or_default(),
185                                error_message: error,
186                            }),
187                        ),
188                        Err(AuthenticatorError::GatewayError { status, body }) => {
189                            if status.is_client_error() {
190                                Err(PollResult::TerminalError(
191                                    AuthenticatorError::GatewayError { status, body },
192                                ))
193                            } else {
194                                Err(PollResult::Retryable)
195                            }
196                        }
197                        _ => Err(PollResult::Retryable),
198                    };
199
200                    match result {
201                        Ok(()) => match Self::init(seed, config.clone()).await {
202                            Ok(auth) => Ok(auth),
203                            Err(AuthenticatorError::AccountDoesNotExist) => {
204                                Err(PollResult::Retryable)
205                            }
206                            Err(e) => Err(PollResult::TerminalError(e)),
207                        },
208                        Err(e) => Err(e),
209                    }
210                };
211
212                let result = poller
213                    .retry(backoff)
214                    .when(|e| matches!(e, PollResult::Retryable))
215                    .await;
216
217                match result {
218                    Ok(authenticator) => Ok(authenticator),
219                    Err(PollResult::TerminalError(e)) => Err(e),
220                    Err(PollResult::Retryable) => Err(AuthenticatorError::Timeout),
221                }
222            }
223            Err(e) => Err(e),
224        }
225    }
226
227    /// Returns the packed account data for the holder's World ID.
228    ///
229    /// The packed account data is a 256 bit integer which includes the World ID's leaf index, their recovery counter,
230    /// and their pubkey id/commitment.
231    ///
232    /// # Errors
233    /// Will error if the network call fails or if the account does not exist.
234    pub async fn get_packed_account_data(
235        onchain_signer_address: Address,
236        registry: Option<&WorldIdRegistryInstance<DynProvider>>,
237        config: &Config,
238        http_client: &reqwest::Client,
239    ) -> Result<U256, AuthenticatorError> {
240        // If the registry is available through direct RPC calls, use it. Otherwise fallback to the indexer.
241        let raw_index = if let Some(registry) = registry {
242            registry
243                .authenticatorAddressToPackedAccountData(onchain_signer_address)
244                .call()
245                .await?
246        } else {
247            let url = format!("{}/packed-account", config.indexer_url());
248            let req = IndexerPackedAccountRequest {
249                authenticator_address: onchain_signer_address,
250            };
251            let resp = http_client.post(&url).json(&req).send().await?;
252
253            let status = resp.status();
254            if !status.is_success() {
255                // Try to parse the error response
256                if let Ok(error_resp) = resp.json::<ServiceApiError<IndexerErrorCode>>().await {
257                    return match error_resp.code {
258                        IndexerErrorCode::AccountDoesNotExist => {
259                            Err(AuthenticatorError::AccountDoesNotExist)
260                        }
261                        _ => Err(AuthenticatorError::IndexerError {
262                            status,
263                            body: error_resp.message,
264                        }),
265                    };
266                }
267                return Err(AuthenticatorError::IndexerError {
268                    status,
269                    body: "Failed to parse indexer error response".to_string(),
270                });
271            }
272
273            let response: IndexerPackedAccountResponse = resp.json().await?;
274            response.packed_account_data
275        };
276
277        if raw_index == U256::ZERO {
278            return Err(AuthenticatorError::AccountDoesNotExist);
279        }
280
281        Ok(raw_index)
282    }
283
284    /// Returns the k256 public key of the Authenticator signer which is used to verify on-chain operations,
285    /// chiefly with the `WorldIdRegistry` contract.
286    #[must_use]
287    pub const fn onchain_address(&self) -> Address {
288        self.signer.onchain_signer_address()
289    }
290
291    /// Returns the `EdDSA` public key of the Authenticator signer which is used to verify off-chain operations. For example,
292    /// the Nullifier Oracle uses it to verify requests for nullifiers.
293    #[must_use]
294    pub fn offchain_pubkey(&self) -> EdDSAPublicKey {
295        self.signer.offchain_signer_pubkey()
296    }
297
298    /// Returns the compressed `EdDSA` public key of the Authenticator signer which is used to verify off-chain operations.
299    /// For example, the Nullifier Oracle uses it to verify requests for nullifiers.
300    /// # Errors
301    /// Will error if the public key cannot be serialized.
302    pub fn offchain_pubkey_compressed(&self) -> Result<U256, AuthenticatorError> {
303        let pk = self.signer.offchain_signer_pubkey().pk;
304        let mut compressed_bytes = Vec::new();
305        pk.serialize_compressed(&mut compressed_bytes)
306            .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
307        Ok(U256::from_le_slice(&compressed_bytes))
308    }
309
310    /// Returns a reference to the `WorldIdRegistry` contract instance.
311    #[must_use]
312    pub fn registry(&self) -> Option<Arc<WorldIdRegistryInstance<DynProvider>>> {
313        self.registry.clone()
314    }
315
316    /// Returns the account index for the holder's World ID.
317    ///
318    /// This is the index at the Merkle tree where the holder's World ID account is registered.
319    #[must_use]
320    pub fn leaf_index(&self) -> U256 {
321        self.packed_account_data & MASK_LEAF_INDEX
322    }
323
324    /// Returns the recovery counter for the holder's World ID.
325    ///
326    /// The recovery counter is used to efficiently invalidate all the old keys when an account is recovered.
327    #[must_use]
328    pub fn recovery_counter(&self) -> U256 {
329        let recovery_counter = self.packed_account_data & MASK_RECOVERY_COUNTER;
330        recovery_counter >> 224
331    }
332
333    /// Returns the pubkey id (or commitment) for the holder's World ID.
334    ///
335    /// This is a commitment to all the off-chain public keys that are authorized to act on behalf of the holder.
336    #[must_use]
337    pub fn pubkey_id(&self) -> U256 {
338        let pubkey_id = self.packed_account_data & MASK_PUBKEY_ID;
339        pubkey_id >> 192
340    }
341
342    /// Fetches a Merkle inclusion proof for the holder's World ID given their account index.
343    ///
344    /// # Errors
345    /// - Will error if the provided indexer URL is not valid or if there are HTTP call failures.
346    /// - Will error if the user is not registered on the registry.
347    pub async fn fetch_inclusion_proof(
348        &self,
349    ) -> Result<(MerkleInclusionProof<TREE_DEPTH>, AuthenticatorPublicKeySet), AuthenticatorError>
350    {
351        let url = format!("{}/inclusion-proof", self.config.indexer_url());
352        let req = IndexerQueryRequest {
353            leaf_index: self.leaf_index(),
354        };
355        let response = self.http_client.post(&url).json(&req).send().await?;
356        let response = response.json::<AccountInclusionProof<TREE_DEPTH>>().await?;
357
358        Ok((response.inclusion_proof, response.authenticator_pubkeys))
359    }
360
361    /// Returns the signing nonce for the holder's World ID.
362    ///
363    /// # Errors
364    /// Will return an error if the registry contract call fails.
365    pub async fn signing_nonce(&self) -> Result<U256, AuthenticatorError> {
366        let registry = self.registry();
367        if let Some(registry) = registry {
368            let nonce = registry
369                .leafIndexToSignatureNonce(self.leaf_index())
370                .call()
371                .await?;
372            Ok(nonce)
373        } else {
374            let url = format!("{}/signature-nonce", self.config.indexer_url());
375            let req = IndexerQueryRequest {
376                leaf_index: self.leaf_index(),
377            };
378            let resp = self.http_client.post(&url).json(&req).send().await?;
379
380            let status = resp.status();
381            if !status.is_success() {
382                return Err(AuthenticatorError::IndexerError {
383                    status,
384                    body: resp
385                        .json()
386                        .await
387                        .unwrap_or_else(|_| "Unable to parse response".to_string()),
388                });
389            }
390
391            let response: IndexerSignatureNonceResponse = resp.json().await?;
392            Ok(response.signature_nonce)
393        }
394    }
395
396    /// Generates a single World ID Proof from a provided `[ProofRequest]` and `[Credential]`.
397    ///
398    /// This assumes the Authenticator has already parsed the `[ProofRequest]` and determined
399    /// which `[Credential]` is appropriate for the request.
400    ///
401    /// # Errors
402    /// - Will error if the any of the provided parameters are not valid.
403    /// - Will error if any of the required network requests fail.
404    /// - Will error if the user does not have a registered World ID.
405    #[allow(clippy::future_not_send)]
406    pub async fn generate_proof(
407        &self,
408        proof_request: ProofRequest,
409        credential: Credential,
410        credential_sub_blinding_factor: FieldElement,
411    ) -> Result<UniquenessProof, AuthenticatorError> {
412        let (inclusion_proof, key_set) = self.fetch_inclusion_proof().await?;
413        let key_index = key_set
414            .iter()
415            .position(|pk| pk.pk == self.offchain_pubkey().pk)
416            .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
417
418        // TODO: load once and from bytes
419        let query_material = crate::proof::load_embedded_query_material();
420        let nullifier_material = crate::proof::load_embedded_nullifier_material();
421
422        let request_item = proof_request
423            .find_request_by_issuer_schema_id(credential.issuer_schema_id.into())
424            .ok_or(AuthenticatorError::InvalidCredentialForProofRequest)?;
425
426        let mut rng = rand::thread_rng();
427
428        let session_id_r_seed = FieldElement::random(&mut rng);
429
430        let args = SingleProofInput::<TREE_DEPTH> {
431            credential,
432            inclusion_proof,
433            key_set,
434            key_index,
435            session_id_r_seed,
436            credential_sub_blinding_factor,
437            rp_id: proof_request.rp_id,
438            share_epoch: ShareEpoch::default().into_inner(),
439            action: proof_request.action,
440            nonce: proof_request.nonce,
441            current_timestamp: proof_request.created_at,
442            rp_signature: proof_request.signature,
443            oprf_public_key: proof_request.oprf_public_key,
444            signal_hash: request_item.signal_hash(),
445            genesis_issued_at_min: request_item.genesis_issued_at_min.unwrap_or(0), // When not provided, the minimum is set to 0 to "ignore" the constraint
446        };
447
448        let private_key = self.signer.offchain_signer_private_key().expose_secret();
449
450        let services = self.config.nullifier_oracle_urls();
451        if services.is_empty() {
452            return Err(AuthenticatorError::Generic(
453                "No nullifier oracle URLs configured".to_string(),
454            ));
455        }
456        let requested_threshold = self.config.nullifier_oracle_threshold();
457        if requested_threshold == 0 {
458            return Err(AuthenticatorError::InvalidConfig {
459                attribute: "nullifier_oracle_threshold",
460                reason: "must be at least 1".to_string(),
461            });
462        }
463        let threshold = requested_threshold.min(services.len());
464
465        let mut rng = rand::thread_rng();
466        let (proof, _public, nullifier, _id_commitment) = crate::proof::nullifier(
467            services,
468            threshold,
469            &query_material,
470            &nullifier_material,
471            args,
472            private_key,
473            self.ws_connector.clone(),
474            &mut rng,
475        )
476        .await
477        .map_err(|e| AuthenticatorError::Generic(format!("Failed to generate nullifier: {e}")))?;
478
479        Ok((proof, nullifier.into()))
480    }
481
482    /// Inserts a new authenticator to the account.
483    ///
484    /// # Errors
485    /// Will error if the provided RPC URL is not valid or if there are HTTP call failures.
486    ///
487    /// # Note
488    /// TODO: After successfully inserting an authenticator, the `packed_account_data` should be
489    /// refreshed from the registry to reflect the new `pubkey_id` commitment.
490    pub async fn insert_authenticator(
491        &mut self,
492        new_authenticator_pubkey: EdDSAPublicKey,
493        new_authenticator_address: Address,
494    ) -> Result<String, AuthenticatorError> {
495        let leaf_index = self.leaf_index();
496        let nonce = self.signing_nonce().await?;
497        let (inclusion_proof, mut key_set) = self.fetch_inclusion_proof().await?;
498        let old_offchain_signer_commitment = key_set.leaf_hash();
499        key_set.try_push(new_authenticator_pubkey.clone())?;
500        let index = key_set.len() - 1;
501        let new_offchain_signer_commitment = key_set.leaf_hash();
502
503        let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
504
505        let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
506
507        #[allow(clippy::cast_possible_truncation)]
508        // truncating is intentional, and index will always fit in 32 bits
509        let signature = sign_insert_authenticator(
510            &self.signer.onchain_signer(),
511            leaf_index,
512            new_authenticator_address,
513            index as u32,
514            encoded_offchain_pubkey,
515            new_offchain_signer_commitment.into(),
516            nonce,
517            &eip712_domain,
518        )
519        .await
520        .map_err(|e| {
521            AuthenticatorError::Generic(format!("Failed to sign insert authenticator: {e}"))
522        })?;
523
524        #[allow(clippy::cast_possible_truncation)]
525        // truncating is intentional, and index will always fit in 32 bits
526        let req = InsertAuthenticatorRequest {
527            leaf_index,
528            new_authenticator_address,
529            pubkey_id: index as u32,
530            new_authenticator_pubkey: encoded_offchain_pubkey,
531            old_offchain_signer_commitment: old_offchain_signer_commitment.into(),
532            new_offchain_signer_commitment: new_offchain_signer_commitment.into(),
533            sibling_nodes: inclusion_proof
534                .siblings
535                .iter()
536                .map(|s| (*s).into())
537                .collect(),
538            signature: signature.as_bytes().to_vec(),
539            nonce,
540        };
541
542        let resp = self
543            .http_client
544            .post(format!(
545                "{}/insert-authenticator",
546                self.config.gateway_url()
547            ))
548            .json(&req)
549            .send()
550            .await?;
551
552        let status = resp.status();
553        if status.is_success() {
554            let body: GatewayStatusResponse = resp.json().await?;
555            Ok(body.request_id)
556        } else {
557            let body_text = resp.text().await.unwrap_or_else(|_| String::new());
558            Err(AuthenticatorError::GatewayError {
559                status,
560                body: body_text,
561            })
562        }
563    }
564
565    /// Updates an existing authenticator slot with a new authenticator.
566    ///
567    /// # Errors
568    /// Returns an error if the gateway rejects the request or a network error occurs.
569    ///
570    /// # Note
571    /// TODO: After successfully updating an authenticator, the `packed_account_data` should be
572    /// refreshed from the registry to reflect the new `pubkey_id` commitment.
573    pub async fn update_authenticator(
574        &mut self,
575        old_authenticator_address: Address,
576        new_authenticator_address: Address,
577        new_authenticator_pubkey: EdDSAPublicKey,
578        index: u32,
579    ) -> Result<String, AuthenticatorError> {
580        let leaf_index = self.leaf_index();
581        let nonce = self.signing_nonce().await?;
582        let (inclusion_proof, mut key_set) = self.fetch_inclusion_proof().await?;
583        let old_commitment: U256 = key_set.leaf_hash().into();
584        key_set.try_set_at_index(index as usize, new_authenticator_pubkey.clone())?;
585        let new_commitment: U256 = key_set.leaf_hash().into();
586
587        let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
588
589        let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
590
591        let signature = sign_update_authenticator(
592            &self.signer.onchain_signer(),
593            leaf_index,
594            old_authenticator_address,
595            new_authenticator_address,
596            index,
597            encoded_offchain_pubkey,
598            new_commitment,
599            nonce,
600            &eip712_domain,
601        )
602        .await
603        .map_err(|e| {
604            AuthenticatorError::Generic(format!("Failed to sign update authenticator: {e}"))
605        })?;
606
607        let sibling_nodes: Vec<U256> = inclusion_proof
608            .siblings
609            .iter()
610            .map(|s| (*s).into())
611            .collect();
612
613        let req = UpdateAuthenticatorRequest {
614            leaf_index,
615            old_authenticator_address,
616            new_authenticator_address,
617            old_offchain_signer_commitment: old_commitment,
618            new_offchain_signer_commitment: new_commitment,
619            sibling_nodes,
620            signature: signature.as_bytes().to_vec(),
621            nonce,
622            pubkey_id: index,
623            new_authenticator_pubkey: encoded_offchain_pubkey,
624        };
625
626        let resp = self
627            .http_client
628            .post(format!(
629                "{}/update-authenticator",
630                self.config.gateway_url()
631            ))
632            .json(&req)
633            .send()
634            .await?;
635
636        let status = resp.status();
637        if status.is_success() {
638            let gateway_resp: GatewayStatusResponse = resp.json().await?;
639            Ok(gateway_resp.request_id)
640        } else {
641            let body_text = resp.text().await.unwrap_or_else(|_| String::new());
642            Err(AuthenticatorError::GatewayError {
643                status,
644                body: body_text,
645            })
646        }
647    }
648
649    /// Removes an authenticator from the account.
650    ///
651    /// # Errors
652    /// Returns an error if the gateway rejects the request or a network error occurs.
653    ///
654    /// # Note
655    /// TODO: After successfully removing an authenticator, the `packed_account_data` should be
656    /// refreshed from the registry to reflect the new `pubkey_id` commitment.
657    pub async fn remove_authenticator(
658        &mut self,
659        authenticator_address: Address,
660        index: u32,
661    ) -> Result<String, AuthenticatorError> {
662        let leaf_index = self.leaf_index();
663        let nonce = self.signing_nonce().await?;
664        let (inclusion_proof, mut key_set) = self.fetch_inclusion_proof().await?;
665        let old_commitment: U256 = key_set.leaf_hash().into();
666        let existing_pubkey = key_set
667            .get(index as usize)
668            .ok_or(AuthenticatorError::PublicKeyNotFound)?;
669
670        let encoded_old_offchain_pubkey = existing_pubkey.to_ethereum_representation()?;
671
672        key_set[index as usize] = EdDSAPublicKey {
673            pk: EdwardsAffine::default(),
674        };
675        let new_commitment: U256 = key_set.leaf_hash().into();
676
677        let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
678
679        let signature = sign_remove_authenticator(
680            &self.signer.onchain_signer(),
681            leaf_index,
682            authenticator_address,
683            index,
684            encoded_old_offchain_pubkey,
685            new_commitment,
686            nonce,
687            &eip712_domain,
688        )
689        .await
690        .map_err(|e| {
691            AuthenticatorError::Generic(format!("Failed to sign remove authenticator: {e}"))
692        })?;
693
694        let sibling_nodes: Vec<U256> = inclusion_proof
695            .siblings
696            .iter()
697            .map(|s| (*s).into())
698            .collect();
699
700        let req = RemoveAuthenticatorRequest {
701            leaf_index,
702            authenticator_address,
703            old_offchain_signer_commitment: old_commitment,
704            new_offchain_signer_commitment: new_commitment,
705            sibling_nodes,
706            signature: signature.as_bytes().to_vec(),
707            nonce,
708            pubkey_id: Some(index),
709            authenticator_pubkey: Some(encoded_old_offchain_pubkey),
710        };
711
712        let resp = self
713            .http_client
714            .post(format!(
715                "{}/remove-authenticator",
716                self.config.gateway_url()
717            ))
718            .json(&req)
719            .send()
720            .await?;
721
722        let status = resp.status();
723        if status.is_success() {
724            let gateway_resp: GatewayStatusResponse = resp.json().await?;
725            Ok(gateway_resp.request_id)
726        } else {
727            let body_text = resp.text().await.unwrap_or_else(|_| String::new());
728            Err(AuthenticatorError::GatewayError {
729                status,
730                body: body_text,
731            })
732        }
733    }
734}
735
736/// Represents an account in the process of being initialized,
737/// i.e. it is not yet registered in the `WorldIDRegistry` contract.
738pub struct InitializingAuthenticator {
739    request_id: String,
740    http_client: reqwest::Client,
741    config: Config,
742}
743
744impl InitializingAuthenticator {
745    /// Creates a new World ID account by adding it to the registry using the gateway.
746    ///
747    /// # Errors
748    /// - See `Signer::from_seed_bytes` for additional error details.
749    /// - Will error if the gateway rejects the request or a network error occurs.
750    async fn new(
751        seed: &[u8],
752        config: Config,
753        recovery_address: Option<Address>,
754        http_client: reqwest::Client,
755    ) -> Result<Self, AuthenticatorError> {
756        let signer = Signer::from_seed_bytes(seed)?;
757
758        let mut key_set = AuthenticatorPublicKeySet::new(None)?;
759        key_set.try_push(signer.offchain_signer_pubkey())?;
760        let leaf_hash = key_set.leaf_hash();
761
762        let offchain_pubkey_compressed = {
763            let pk = signer.offchain_signer_pubkey().pk;
764            let mut compressed_bytes = Vec::new();
765            pk.serialize_compressed(&mut compressed_bytes)
766                .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
767            U256::from_le_slice(&compressed_bytes)
768        };
769
770        let req = CreateAccountRequest {
771            recovery_address,
772            authenticator_addresses: vec![signer.onchain_signer_address()],
773            authenticator_pubkeys: vec![offchain_pubkey_compressed],
774            offchain_signer_commitment: leaf_hash.into(),
775        };
776
777        let resp = http_client
778            .post(format!("{}/create-account", config.gateway_url()))
779            .json(&req)
780            .send()
781            .await?;
782
783        let status = resp.status();
784        if status.is_success() {
785            let body: GatewayStatusResponse = resp.json().await?;
786            Ok(Self {
787                request_id: body.request_id,
788                http_client,
789                config,
790            })
791        } else {
792            let body_text = resp.text().await.unwrap_or_else(|_| String::new());
793            Err(AuthenticatorError::GatewayError {
794                status,
795                body: body_text,
796            })
797        }
798    }
799
800    /// Poll the status of the World ID creation request.
801    ///
802    /// # Errors
803    /// - Will error if the network request fails.
804    /// - Will error if the gateway returns an error response.
805    pub async fn poll_status(&self) -> Result<GatewayRequestState, AuthenticatorError> {
806        let resp = self
807            .http_client
808            .get(format!(
809                "{}/status/{}",
810                self.config.gateway_url(),
811                self.request_id
812            ))
813            .send()
814            .await?;
815
816        let status = resp.status();
817
818        if status.is_success() {
819            let body: GatewayStatusResponse = resp.json().await?;
820            Ok(body.status)
821        } else {
822            let body_text = resp.text().await.unwrap_or_else(|_| String::new());
823            Err(AuthenticatorError::GatewayError {
824                status,
825                body: body_text,
826            })
827        }
828    }
829}
830
831impl ProtocolSigner for Authenticator {
832    fn sign(&self, message: FieldElement) -> EdDSASignature {
833        self.signer
834            .offchain_signer_private_key()
835            .expose_secret()
836            .sign(*message)
837    }
838}
839
840/// A trait for types that can be represented as a `U256` on-chain.
841pub trait OnchainKeyRepresentable {
842    /// Converts an off-chain public key into a `U256` representation for on-chain use in the `WorldIDRegistry` contract.
843    ///
844    /// The `U256` representation is a 32-byte little-endian encoding of the **compressed** (single point) public key.
845    ///
846    /// # Errors
847    /// Will error if the public key unexpectedly fails to serialize.
848    fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError>;
849}
850
851impl OnchainKeyRepresentable for EdDSAPublicKey {
852    // REVIEW: updating to BE
853    fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError> {
854        let mut compressed_bytes = Vec::new();
855        self.pk
856            .serialize_compressed(&mut compressed_bytes)
857            .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
858        Ok(U256::from_le_slice(&compressed_bytes))
859    }
860}
861
862/// Errors that can occur when interacting with the Authenticator.
863#[derive(Debug, thiserror::Error)]
864pub enum AuthenticatorError {
865    /// Primitive error
866    #[error(transparent)]
867    PrimitiveError(#[from] PrimitiveError),
868
869    /// This operation requires a registered account and an account is not registered
870    /// for this authenticator. Call `create_account` first to register it.
871    #[error("Account is not registered for this authenticator.")]
872    AccountDoesNotExist,
873
874    /// The account already exists for this authenticator. Call `leaf_index` to get the leaf index.
875    #[error("Account already exists for this authenticator.")]
876    AccountAlreadyExists,
877
878    /// An error occurred while interacting with the EVM contract.
879    #[error("Error interacting with EVM contract: {0}")]
880    ContractError(#[from] alloy::contract::Error),
881
882    /// Network/HTTP request error.
883    #[error("Network error: {0}")]
884    NetworkError(#[from] reqwest::Error),
885
886    /// Public key not found in the Authenticator public key set. Usually indicates the local state is out of sync with the registry.
887    #[error("Public key not found.")]
888    PublicKeyNotFound,
889
890    /// Gateway returned an error response.
891    #[error("Gateway error (status {status}): {body}")]
892    GatewayError {
893        /// HTTP status code
894        status: StatusCode,
895        /// Response body
896        body: String,
897    },
898
899    /// Indexer returned an error response.
900    #[error("Indexer error (status {status}): {body}")]
901    IndexerError {
902        /// HTTP status code
903        status: StatusCode,
904        /// Response body
905        body: String,
906    },
907
908    /// Account creation timed out while polling for confirmation.
909    #[error("Account creation timed out")]
910    Timeout,
911
912    /// Configuration is invalid or missing required values.
913    #[error("Invalid configuration for {attribute}: {reason}")]
914    InvalidConfig {
915        /// The config attribute that is invalid.
916        attribute: &'static str,
917        /// Description of why it is invalid.
918        reason: String,
919    },
920
921    /// The provided credential is not valid for the provided proof request.
922    #[error("The provided credential is not valid for the provided proof request")]
923    InvalidCredentialForProofRequest,
924
925    /// Error during the World ID registration process.
926    ///
927    /// This usually occurs from an on-chain revert.
928    #[error("Registration error ({error_code}): {error_message}")]
929    RegistrationError {
930        /// Error code from the registration process.
931        error_code: String,
932        /// Detailed error message.
933        error_message: String,
934    },
935
936    /// Generic error for other unexpected issues.
937    #[error("{0}")]
938    Generic(String),
939}
940
941#[derive(Debug)]
942enum PollResult {
943    Retryable,
944    TerminalError(AuthenticatorError),
945}
946
947#[cfg(test)]
948mod tests {
949    use super::*;
950    use alloy::primitives::{address, U256};
951
952    /// Tests that `get_packed_account_data` correctly fetches the packed account data from the indexer
953    /// when no RPC is configured.
954    #[tokio::test]
955    async fn test_get_packed_account_data_from_indexer() {
956        let mut server = mockito::Server::new_async().await;
957        let indexer_url = server.url();
958
959        let test_address = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0");
960        let expected_packed_index = U256::from(42);
961
962        let mock = server
963            .mock("POST", "/packed-account")
964            .match_header("content-type", "application/json")
965            .match_body(mockito::Matcher::JsonString(
966                serde_json::json!({
967                    "authenticator_address": test_address
968                })
969                .to_string(),
970            ))
971            .with_status(200)
972            .with_header("content-type", "application/json")
973            .with_body(
974                serde_json::json!({
975                    "packed_account_data": format!("{:#x}", expected_packed_index)
976                })
977                .to_string(),
978            )
979            .create_async()
980            .await;
981
982        let config = Config::new(
983            None,
984            1,
985            address!("0x0000000000000000000000000000000000000001"),
986            indexer_url,
987            "http://gateway.example.com".to_string(),
988            Vec::new(),
989            2,
990        )
991        .unwrap();
992
993        let http_client = reqwest::Client::new();
994
995        let result = Authenticator::get_packed_account_data(
996            test_address,
997            None, // No registry, force indexer usage
998            &config,
999            &http_client,
1000        )
1001        .await
1002        .unwrap();
1003
1004        assert_eq!(result, expected_packed_index);
1005        mock.assert_async().await;
1006        drop(server);
1007    }
1008
1009    #[tokio::test]
1010    async fn test_get_packed_account_data_from_indexer_error() {
1011        let mut server = mockito::Server::new_async().await;
1012        let indexer_url = server.url();
1013
1014        let test_address = address!("0x0000000000000000000000000000000000000099");
1015
1016        let mock = server
1017            .mock("POST", "/packed-account")
1018            .with_status(400)
1019            .with_header("content-type", "application/json")
1020            .with_body(
1021                serde_json::json!({
1022                    "code": "account_does_not_exist",
1023                    "message": "There is no account for this authenticator address"
1024                })
1025                .to_string(),
1026            )
1027            .create_async()
1028            .await;
1029
1030        let config = Config::new(
1031            None,
1032            1,
1033            address!("0x0000000000000000000000000000000000000001"),
1034            indexer_url,
1035            "http://gateway.example.com".to_string(),
1036            Vec::new(),
1037            2,
1038        )
1039        .unwrap();
1040
1041        let http_client = reqwest::Client::new();
1042
1043        let result =
1044            Authenticator::get_packed_account_data(test_address, None, &config, &http_client).await;
1045
1046        assert!(matches!(
1047            result,
1048            Err(AuthenticatorError::AccountDoesNotExist)
1049        ));
1050        mock.assert_async().await;
1051        drop(server);
1052    }
1053
1054    #[tokio::test]
1055    async fn test_signing_nonce_from_indexer() {
1056        let mut server = mockito::Server::new_async().await;
1057        let indexer_url = server.url();
1058
1059        let leaf_index = U256::from(1);
1060        let expected_nonce = U256::from(5);
1061
1062        let mock = server
1063            .mock("POST", "/signature-nonce")
1064            .match_header("content-type", "application/json")
1065            .match_body(mockito::Matcher::JsonString(
1066                serde_json::json!({
1067                    "leaf_index": format!("{:#x}", leaf_index)
1068                })
1069                .to_string(),
1070            ))
1071            .with_status(200)
1072            .with_header("content-type", "application/json")
1073            .with_body(
1074                serde_json::json!({
1075                    "signature_nonce": format!("{:#x}", expected_nonce)
1076                })
1077                .to_string(),
1078            )
1079            .create_async()
1080            .await;
1081
1082        let config = Config::new(
1083            None,
1084            1,
1085            address!("0x0000000000000000000000000000000000000001"),
1086            indexer_url,
1087            "http://gateway.example.com".to_string(),
1088            Vec::new(),
1089            2,
1090        )
1091        .unwrap();
1092
1093        let authenticator = Authenticator {
1094            config,
1095            packed_account_data: leaf_index, // This sets leaf_index() to 1
1096            signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1097            registry: None, // No registry - forces indexer usage
1098            http_client: reqwest::Client::new(),
1099            ws_connector: Connector::Plain,
1100        };
1101
1102        let nonce = authenticator.signing_nonce().await.unwrap();
1103
1104        assert_eq!(nonce, expected_nonce);
1105        mock.assert_async().await;
1106        drop(server);
1107    }
1108
1109    #[tokio::test]
1110    async fn test_signing_nonce_from_indexer_error() {
1111        let mut server = mockito::Server::new_async().await;
1112        let indexer_url = server.url();
1113
1114        let mock = server
1115            .mock("POST", "/signature-nonce")
1116            .with_status(400)
1117            .with_header("content-type", "application/json")
1118            .with_body(
1119                serde_json::json!({
1120                    "code": "invalid_leaf_index",
1121                    "message": "Account index cannot be zero"
1122                })
1123                .to_string(),
1124            )
1125            .create_async()
1126            .await;
1127
1128        let config = Config::new(
1129            None,
1130            1,
1131            address!("0x0000000000000000000000000000000000000001"),
1132            indexer_url,
1133            "http://gateway.example.com".to_string(),
1134            Vec::new(),
1135            2,
1136        )
1137        .unwrap();
1138
1139        let authenticator = Authenticator {
1140            config,
1141            packed_account_data: U256::ZERO,
1142            signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1143            registry: None,
1144            http_client: reqwest::Client::new(),
1145            ws_connector: Connector::Plain,
1146        };
1147
1148        let result = authenticator.signing_nonce().await;
1149
1150        assert!(matches!(
1151            result,
1152            Err(AuthenticatorError::IndexerError { .. })
1153        ));
1154        mock.assert_async().await;
1155        drop(server);
1156    }
1157}