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