1use std::sync::Arc;
6
7use crate::api_types::{
8 AccountInclusionProof, CreateAccountRequest, GatewayRequestState, GatewayStatusResponse,
9 IndexerErrorCode, IndexerPackedAccountRequest, IndexerPackedAccountResponse,
10 IndexerQueryRequest, IndexerSignatureNonceResponse, InsertAuthenticatorRequest,
11 RemoveAuthenticatorRequest, ServiceApiError, UpdateAuthenticatorRequest,
12};
13use world_id_primitives::{
14 Credential, FieldElement, ProofRequest, RequestItem, ResponseItem, SessionNullifier, Signer,
15};
16
17use crate::registry::{
18 WorldIdRegistry::WorldIdRegistryInstance, domain, sign_insert_authenticator,
19 sign_remove_authenticator, sign_update_authenticator,
20};
21use alloy::{
22 primitives::{Address, U256},
23 providers::DynProvider,
24 uint,
25};
26use ark_babyjubjub::EdwardsAffine;
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, authenticator::AuthenticatorPublicKeySet,
36 merkle::MerkleInclusionProof,
37};
38use world_id_proof::{
39 AuthenticatorProofInput,
40 credential_blinding_factor::OprfCredentialBlindingFactor,
41 nullifier::OprfNullifier,
42 proof::{ProofError, generate_nullifier_proof},
43};
44
45static MASK_RECOVERY_COUNTER: U256 =
46 uint!(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000_U256);
47static MASK_PUBKEY_ID: U256 =
48 uint!(0x00000000FFFFFFFF000000000000000000000000000000000000000000000000_U256);
49static MASK_LEAF_INDEX: U256 =
50 uint!(0x000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFF_U256);
51
52pub struct Authenticator {
54 pub config: Config,
56 pub packed_account_data: U256,
59 signer: Signer,
60 registry: Option<Arc<WorldIdRegistryInstance<DynProvider>>>,
61 http_client: reqwest::Client,
62 ws_connector: Connector,
63 query_material: Arc<CircomGroth16Material>,
64 nullifier_material: Arc<CircomGroth16Material>,
65}
66
67#[expect(clippy::missing_fields_in_debug)]
68impl std::fmt::Debug for Authenticator {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 f.debug_struct("Authenticator")
71 .field("config", &self.config)
72 .field("packed_account_data", &self.packed_account_data)
73 .field("signer", &self.signer)
74 .finish()
75 }
76}
77
78impl Authenticator {
79 #[cfg(feature = "embed-zkeys")]
89 pub async fn init(seed: &[u8], config: Config) -> Result<Self, AuthenticatorError> {
90 let signer = Signer::from_seed_bytes(seed)?;
91
92 let registry = config.rpc_url().map_or_else(
93 || None,
94 |rpc_url| {
95 let provider = alloy::providers::ProviderBuilder::new()
96 .with_chain_id(config.chain_id())
97 .connect_http(rpc_url.clone());
98 Some(crate::registry::WorldIdRegistry::new(
99 *config.registry_address(),
100 alloy::providers::Provider::erased(provider),
101 ))
102 },
103 );
104
105 let http_client = reqwest::Client::new();
106
107 let packed_account_data = Self::get_packed_account_data(
108 signer.onchain_signer_address(),
109 registry.as_ref(),
110 &config,
111 &http_client,
112 )
113 .await?;
114
115 let mut root_store = rustls::RootCertStore::empty();
116 root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
117 let rustls_config = rustls::ClientConfig::builder()
118 .with_root_certificates(root_store)
119 .with_no_client_auth();
120 let ws_connector = Connector::Rustls(Arc::new(rustls_config));
121
122 let cache_dir = config.zkey_cache_dir();
123 let query_material = Arc::new(
124 world_id_proof::proof::load_embedded_query_material(cache_dir).map_err(|e| {
125 AuthenticatorError::Generic(format!("Failed to load cached query material: {e}"))
126 })?,
127 );
128 let nullifier_material = Arc::new(
129 world_id_proof::proof::load_embedded_nullifier_material(cache_dir).map_err(|e| {
130 AuthenticatorError::Generic(format!(
131 "Failed to load cached nullifier material: {e}"
132 ))
133 })?,
134 );
135
136 Ok(Self {
137 packed_account_data,
138 signer,
139 config,
140 registry: registry.map(Arc::new),
141 http_client,
142 ws_connector,
143 query_material,
144 nullifier_material,
145 })
146 }
147
148 pub async fn register(
156 seed: &[u8],
157 config: Config,
158 recovery_address: Option<Address>,
159 ) -> Result<InitializingAuthenticator, AuthenticatorError> {
160 let http_client = reqwest::Client::new();
161 InitializingAuthenticator::new(seed, config, recovery_address, http_client).await
162 }
163
164 #[cfg(feature = "embed-zkeys")]
177 pub async fn init_or_register(
178 seed: &[u8],
179 config: Config,
180 recovery_address: Option<Address>,
181 ) -> Result<Self, AuthenticatorError> {
182 match Self::init(seed, config.clone()).await {
183 Ok(authenticator) => Ok(authenticator),
184 Err(AuthenticatorError::AccountDoesNotExist) => {
185 let http_client = reqwest::Client::new();
187 let initializing_authenticator = InitializingAuthenticator::new(
188 seed,
189 config.clone(),
190 recovery_address,
191 http_client,
192 )
193 .await?;
194
195 let backoff = backon::ExponentialBuilder::default()
196 .with_min_delay(std::time::Duration::from_millis(800))
197 .with_factor(1.5)
198 .without_max_times()
199 .with_total_delay(Some(std::time::Duration::from_secs(120)));
200
201 let poller = || async {
202 let poll_status = initializing_authenticator.poll_status().await;
203 let result = match poll_status {
204 Ok(GatewayRequestState::Finalized { .. }) => Ok(()),
205 Ok(GatewayRequestState::Failed { error_code, error }) => Err(
206 PollResult::TerminalError(AuthenticatorError::RegistrationError {
207 error_code: error_code.map(|v| v.to_string()).unwrap_or_default(),
208 error_message: error,
209 }),
210 ),
211 Err(AuthenticatorError::GatewayError { status, body }) => {
212 if status.is_client_error() {
213 Err(PollResult::TerminalError(
214 AuthenticatorError::GatewayError { status, body },
215 ))
216 } else {
217 Err(PollResult::Retryable)
218 }
219 }
220 _ => Err(PollResult::Retryable),
221 };
222
223 match result {
224 Ok(()) => match Self::init(seed, config.clone()).await {
225 Ok(auth) => Ok(auth),
226 Err(AuthenticatorError::AccountDoesNotExist) => {
227 Err(PollResult::Retryable)
228 }
229 Err(e) => Err(PollResult::TerminalError(e)),
230 },
231 Err(e) => Err(e),
232 }
233 };
234
235 let result = backon::Retryable::retry(poller, backoff)
236 .when(|e| matches!(e, PollResult::Retryable))
237 .await;
238
239 match result {
240 Ok(authenticator) => Ok(authenticator),
241 Err(PollResult::TerminalError(e)) => Err(e),
242 Err(PollResult::Retryable) => Err(AuthenticatorError::Timeout),
243 }
244 }
245 Err(e) => Err(e),
246 }
247 }
248
249 pub async fn get_packed_account_data(
257 onchain_signer_address: Address,
258 registry: Option<&WorldIdRegistryInstance<DynProvider>>,
259 config: &Config,
260 http_client: &reqwest::Client,
261 ) -> Result<U256, AuthenticatorError> {
262 let raw_index = if let Some(registry) = registry {
264 registry
266 .getPackedAccountData(onchain_signer_address)
267 .call()
268 .await?
269 } else {
270 let url = format!("{}/packed-account", config.indexer_url());
271 let req = IndexerPackedAccountRequest {
272 authenticator_address: onchain_signer_address,
273 };
274 let resp = http_client.post(&url).json(&req).send().await?;
275
276 let status = resp.status();
277 if !status.is_success() {
278 if let Ok(error_resp) = resp.json::<ServiceApiError<IndexerErrorCode>>().await {
280 return match error_resp.code {
281 IndexerErrorCode::AccountDoesNotExist => {
282 Err(AuthenticatorError::AccountDoesNotExist)
283 }
284 _ => Err(AuthenticatorError::IndexerError {
285 status,
286 body: error_resp.message,
287 }),
288 };
289 }
290 return Err(AuthenticatorError::IndexerError {
291 status,
292 body: "Failed to parse indexer error response".to_string(),
293 });
294 }
295
296 let response: IndexerPackedAccountResponse = resp.json().await?;
297 response.packed_account_data
298 };
299
300 if raw_index == U256::ZERO {
301 return Err(AuthenticatorError::AccountDoesNotExist);
302 }
303
304 Ok(raw_index)
305 }
306
307 #[must_use]
310 pub const fn onchain_address(&self) -> Address {
311 self.signer.onchain_signer_address()
312 }
313
314 #[must_use]
317 pub fn offchain_pubkey(&self) -> EdDSAPublicKey {
318 self.signer.offchain_signer_pubkey()
319 }
320
321 pub fn offchain_pubkey_compressed(&self) -> Result<U256, AuthenticatorError> {
326 let pk = self.signer.offchain_signer_pubkey().pk;
327 let mut compressed_bytes = Vec::new();
328 pk.serialize_compressed(&mut compressed_bytes)
329 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
330 Ok(U256::from_le_slice(&compressed_bytes))
331 }
332
333 #[must_use]
335 pub fn registry(&self) -> Option<Arc<WorldIdRegistryInstance<DynProvider>>> {
336 self.registry.clone()
337 }
338
339 #[must_use]
355 pub fn leaf_index(&self) -> u64 {
356 (self.packed_account_data & MASK_LEAF_INDEX).to::<u64>()
357 }
358
359 #[must_use]
363 pub fn recovery_counter(&self) -> U256 {
364 let recovery_counter = self.packed_account_data & MASK_RECOVERY_COUNTER;
365 recovery_counter >> 224
366 }
367
368 #[must_use]
372 pub fn pubkey_id(&self) -> U256 {
373 let pubkey_id = self.packed_account_data & MASK_PUBKEY_ID;
374 pubkey_id >> 192
375 }
376
377 pub async fn fetch_inclusion_proof(
383 &self,
384 ) -> Result<(MerkleInclusionProof<TREE_DEPTH>, AuthenticatorPublicKeySet), AuthenticatorError>
385 {
386 let url = format!("{}/inclusion-proof", self.config.indexer_url());
387 let req = IndexerQueryRequest {
388 leaf_index: self.leaf_index(),
389 };
390 let response = self.http_client.post(&url).json(&req).send().await?;
391 let response = response.json::<AccountInclusionProof<TREE_DEPTH>>().await?;
392
393 Ok((response.inclusion_proof, response.authenticator_pubkeys))
394 }
395
396 pub async fn signing_nonce(&self) -> Result<U256, AuthenticatorError> {
401 let registry = self.registry();
402 if let Some(registry) = registry {
403 let nonce = registry.getSignatureNonce(self.leaf_index()).call().await?;
404 Ok(nonce)
405 } else {
406 let url = format!("{}/signature-nonce", self.config.indexer_url());
407 let req = IndexerQueryRequest {
408 leaf_index: self.leaf_index(),
409 };
410 let resp = self.http_client.post(&url).json(&req).send().await?;
411
412 let status = resp.status();
413 if !status.is_success() {
414 return Err(AuthenticatorError::IndexerError {
415 status,
416 body: resp
417 .json()
418 .await
419 .unwrap_or_else(|_| "Unable to parse response".to_string()),
420 });
421 }
422
423 let response: IndexerSignatureNonceResponse = resp.json().await?;
424 Ok(response.signature_nonce)
425 }
426 }
427
428 fn check_oprf_config(&self) -> Result<(&[String], usize), AuthenticatorError> {
433 let services = self.config.nullifier_oracle_urls();
434 if services.is_empty() {
435 return Err(AuthenticatorError::Generic(
436 "No nullifier oracle URLs configured".to_string(),
437 ));
438 }
439 let requested_threshold = self.config.nullifier_oracle_threshold();
440 if requested_threshold == 0 {
441 return Err(AuthenticatorError::InvalidConfig {
442 attribute: "nullifier_oracle_threshold",
443 reason: "must be at least 1".to_string(),
444 });
445 }
446 let threshold = requested_threshold.min(services.len());
447 Ok((services, threshold))
448 }
449
450 pub async fn generate_nullifier(
462 &self,
463 proof_request: &ProofRequest,
464 ) -> Result<OprfNullifier, AuthenticatorError> {
465 let (services, threshold) = self.check_oprf_config()?;
466
467 let (inclusion_proof, key_set) = self.fetch_inclusion_proof().await?;
468 let key_index = key_set
469 .iter()
470 .position(|pk| pk.pk == self.offchain_pubkey().pk)
471 .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
472
473 let authenticator_input = AuthenticatorProofInput::new(
474 key_set,
475 inclusion_proof,
476 self.signer
477 .offchain_signer_private_key()
478 .expose_secret()
479 .clone(),
480 key_index,
481 );
482
483 Ok(OprfNullifier::generate(
484 services,
485 threshold,
486 &self.query_material,
487 authenticator_input,
488 proof_request,
489 self.ws_connector.clone(),
490 )
491 .await?)
492 }
493
494 pub async fn generate_credential_blinding_factor(
503 &self,
504 issuer_schema_id: u64,
505 ) -> Result<FieldElement, AuthenticatorError> {
506 let (services, threshold) = self.check_oprf_config()?;
507
508 let (inclusion_proof, key_set) = self.fetch_inclusion_proof().await?;
509 let key_index = key_set
510 .iter()
511 .position(|pk| pk.pk == self.offchain_pubkey().pk)
512 .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
513
514 let authenticator_input = AuthenticatorProofInput::new(
515 key_set,
516 inclusion_proof,
517 self.signer
518 .offchain_signer_private_key()
519 .expose_secret()
520 .clone(),
521 key_index,
522 );
523
524 let blinding_factor = OprfCredentialBlindingFactor::generate(
525 services,
526 threshold,
527 &self.query_material,
528 authenticator_input,
529 issuer_schema_id,
530 FieldElement::ZERO, self.ws_connector.clone(),
532 )
533 .await?;
534
535 Ok(blinding_factor.verifiable_oprf_output.output.into())
536 }
537
538 #[allow(clippy::too_many_arguments)]
559 pub fn generate_single_proof(
560 &self,
561 oprf_nullifier: OprfNullifier,
562 request_item: &RequestItem,
563 credential: &Credential,
564 credential_sub_blinding_factor: FieldElement,
565 session_id_r_seed: FieldElement,
566 session_id: Option<FieldElement>,
567 request_timestamp: u64,
568 ) -> Result<ResponseItem, AuthenticatorError> {
569 let mut rng = rand::rngs::OsRng;
570
571 let merkle_root: FieldElement = oprf_nullifier.query_proof_input.merkle_root.into();
572 let action_from_query: FieldElement = oprf_nullifier.query_proof_input.action.into();
573
574 let expires_at_min = request_item.effective_expires_at_min(request_timestamp);
575
576 let (proof, _public_inputs, nullifier) = generate_nullifier_proof(
577 &self.nullifier_material,
578 &mut rng,
579 credential,
580 credential_sub_blinding_factor,
581 oprf_nullifier,
582 request_item,
583 session_id,
584 session_id_r_seed,
585 expires_at_min,
586 )?;
587
588 let proof = ZeroKnowledgeProof::from_groth16_proof(&proof, merkle_root);
589
590 let nullifier_fe: FieldElement = nullifier.into();
592 let response_item = if session_id.is_some() {
593 let session_nullifier = SessionNullifier::new(nullifier_fe, action_from_query);
594 ResponseItem::new_session(
595 request_item.identifier.clone(),
596 request_item.issuer_schema_id,
597 proof,
598 session_nullifier,
599 expires_at_min,
600 )
601 } else {
602 ResponseItem::new_uniqueness(
603 request_item.identifier.clone(),
604 request_item.issuer_schema_id,
605 proof,
606 nullifier_fe,
607 expires_at_min,
608 )
609 };
610
611 Ok(response_item)
612 }
613
614 pub async fn insert_authenticator(
623 &mut self,
624 new_authenticator_pubkey: EdDSAPublicKey,
625 new_authenticator_address: Address,
626 ) -> Result<String, AuthenticatorError> {
627 let leaf_index = self.leaf_index();
628 let nonce = self.signing_nonce().await?;
629 let (inclusion_proof, mut key_set) = self.fetch_inclusion_proof().await?;
630 let old_offchain_signer_commitment = key_set.leaf_hash();
631 let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
632 key_set.try_push(new_authenticator_pubkey)?;
633 let index = key_set.len() - 1;
634 let new_offchain_signer_commitment = key_set.leaf_hash();
635
636 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
637
638 #[allow(clippy::cast_possible_truncation)]
639 let signature = sign_insert_authenticator(
641 &self.signer.onchain_signer(),
642 leaf_index,
643 new_authenticator_address,
644 index as u32,
645 encoded_offchain_pubkey,
646 new_offchain_signer_commitment.into(),
647 nonce,
648 &eip712_domain,
649 )
650 .await
651 .map_err(|e| {
652 AuthenticatorError::Generic(format!("Failed to sign insert authenticator: {e}"))
653 })?;
654
655 #[allow(clippy::cast_possible_truncation)]
656 let req = InsertAuthenticatorRequest {
658 leaf_index,
659 new_authenticator_address,
660 pubkey_id: index as u32,
661 new_authenticator_pubkey: encoded_offchain_pubkey,
662 old_offchain_signer_commitment: old_offchain_signer_commitment.into(),
663 new_offchain_signer_commitment: new_offchain_signer_commitment.into(),
664 sibling_nodes: inclusion_proof
665 .siblings
666 .iter()
667 .map(|s| (*s).into())
668 .collect(),
669 signature: signature.as_bytes().to_vec(),
670 nonce,
671 };
672
673 let resp = self
674 .http_client
675 .post(format!(
676 "{}/insert-authenticator",
677 self.config.gateway_url()
678 ))
679 .json(&req)
680 .send()
681 .await?;
682
683 let status = resp.status();
684 if status.is_success() {
685 let body: GatewayStatusResponse = resp.json().await?;
686 Ok(body.request_id)
687 } else {
688 let body_text = resp.text().await.unwrap_or_else(|_| String::new());
689 Err(AuthenticatorError::GatewayError {
690 status,
691 body: body_text,
692 })
693 }
694 }
695
696 pub async fn update_authenticator(
705 &mut self,
706 old_authenticator_address: Address,
707 new_authenticator_address: Address,
708 new_authenticator_pubkey: EdDSAPublicKey,
709 index: u32,
710 ) -> Result<String, AuthenticatorError> {
711 let leaf_index = self.leaf_index();
712 let nonce = self.signing_nonce().await?;
713 let (inclusion_proof, mut key_set) = self.fetch_inclusion_proof().await?;
714 let old_commitment: U256 = key_set.leaf_hash().into();
715 let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
716 key_set.try_set_at_index(index as usize, new_authenticator_pubkey)?;
717 let new_commitment: U256 = key_set.leaf_hash().into();
718
719 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
720
721 let signature = sign_update_authenticator(
722 &self.signer.onchain_signer(),
723 leaf_index,
724 old_authenticator_address,
725 new_authenticator_address,
726 index,
727 encoded_offchain_pubkey,
728 new_commitment,
729 nonce,
730 &eip712_domain,
731 )
732 .await
733 .map_err(|e| {
734 AuthenticatorError::Generic(format!("Failed to sign update authenticator: {e}"))
735 })?;
736
737 let sibling_nodes: Vec<U256> = inclusion_proof
738 .siblings
739 .iter()
740 .map(|s| (*s).into())
741 .collect();
742
743 let req = UpdateAuthenticatorRequest {
744 leaf_index,
745 old_authenticator_address,
746 new_authenticator_address,
747 old_offchain_signer_commitment: old_commitment,
748 new_offchain_signer_commitment: new_commitment,
749 sibling_nodes,
750 signature: signature.as_bytes().to_vec(),
751 nonce,
752 pubkey_id: index,
753 new_authenticator_pubkey: encoded_offchain_pubkey,
754 };
755
756 let resp = self
757 .http_client
758 .post(format!(
759 "{}/update-authenticator",
760 self.config.gateway_url()
761 ))
762 .json(&req)
763 .send()
764 .await?;
765
766 let status = resp.status();
767 if status.is_success() {
768 let gateway_resp: GatewayStatusResponse = resp.json().await?;
769 Ok(gateway_resp.request_id)
770 } else {
771 let body_text = resp.text().await.unwrap_or_else(|_| String::new());
772 Err(AuthenticatorError::GatewayError {
773 status,
774 body: body_text,
775 })
776 }
777 }
778
779 pub async fn remove_authenticator(
788 &mut self,
789 authenticator_address: Address,
790 index: u32,
791 ) -> Result<String, AuthenticatorError> {
792 let leaf_index = self.leaf_index();
793 let nonce = self.signing_nonce().await?;
794 let (inclusion_proof, mut key_set) = self.fetch_inclusion_proof().await?;
795 let old_commitment: U256 = key_set.leaf_hash().into();
796 let existing_pubkey = key_set
797 .get(index as usize)
798 .ok_or(AuthenticatorError::PublicKeyNotFound)?;
799
800 let encoded_old_offchain_pubkey = existing_pubkey.to_ethereum_representation()?;
801
802 key_set[index as usize] = EdDSAPublicKey {
803 pk: EdwardsAffine::default(),
804 };
805 let new_commitment: U256 = key_set.leaf_hash().into();
806
807 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
808
809 let signature = sign_remove_authenticator(
810 &self.signer.onchain_signer(),
811 leaf_index,
812 authenticator_address,
813 index,
814 encoded_old_offchain_pubkey,
815 new_commitment,
816 nonce,
817 &eip712_domain,
818 )
819 .await
820 .map_err(|e| {
821 AuthenticatorError::Generic(format!("Failed to sign remove authenticator: {e}"))
822 })?;
823
824 let sibling_nodes: Vec<U256> = inclusion_proof
825 .siblings
826 .iter()
827 .map(|s| (*s).into())
828 .collect();
829
830 let req = RemoveAuthenticatorRequest {
831 leaf_index,
832 authenticator_address,
833 old_offchain_signer_commitment: old_commitment,
834 new_offchain_signer_commitment: new_commitment,
835 sibling_nodes,
836 signature: signature.as_bytes().to_vec(),
837 nonce,
838 pubkey_id: Some(index),
839 authenticator_pubkey: Some(encoded_old_offchain_pubkey),
840 };
841
842 let resp = self
843 .http_client
844 .post(format!(
845 "{}/remove-authenticator",
846 self.config.gateway_url()
847 ))
848 .json(&req)
849 .send()
850 .await?;
851
852 let status = resp.status();
853 if status.is_success() {
854 let gateway_resp: GatewayStatusResponse = resp.json().await?;
855 Ok(gateway_resp.request_id)
856 } else {
857 let body_text = resp.text().await.unwrap_or_else(|_| String::new());
858 Err(AuthenticatorError::GatewayError {
859 status,
860 body: body_text,
861 })
862 }
863 }
864}
865
866pub struct InitializingAuthenticator {
869 request_id: String,
870 http_client: reqwest::Client,
871 config: Config,
872}
873
874impl InitializingAuthenticator {
875 async fn new(
881 seed: &[u8],
882 config: Config,
883 recovery_address: Option<Address>,
884 http_client: reqwest::Client,
885 ) -> Result<Self, AuthenticatorError> {
886 let signer = Signer::from_seed_bytes(seed)?;
887
888 let mut key_set = AuthenticatorPublicKeySet::new(None)?;
889 key_set.try_push(signer.offchain_signer_pubkey())?;
890 let leaf_hash = key_set.leaf_hash();
891
892 let offchain_pubkey_compressed = {
893 let pk = signer.offchain_signer_pubkey().pk;
894 let mut compressed_bytes = Vec::new();
895 pk.serialize_compressed(&mut compressed_bytes)
896 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
897 U256::from_le_slice(&compressed_bytes)
898 };
899
900 let req = CreateAccountRequest {
901 recovery_address,
902 authenticator_addresses: vec![signer.onchain_signer_address()],
903 authenticator_pubkeys: vec![offchain_pubkey_compressed],
904 offchain_signer_commitment: leaf_hash.into(),
905 };
906
907 let resp = http_client
908 .post(format!("{}/create-account", config.gateway_url()))
909 .json(&req)
910 .send()
911 .await?;
912
913 let status = resp.status();
914 if status.is_success() {
915 let body: GatewayStatusResponse = resp.json().await?;
916 Ok(Self {
917 request_id: body.request_id,
918 http_client,
919 config,
920 })
921 } else {
922 let body_text = resp.text().await.unwrap_or_else(|_| String::new());
923 Err(AuthenticatorError::GatewayError {
924 status,
925 body: body_text,
926 })
927 }
928 }
929
930 pub async fn poll_status(&self) -> Result<GatewayRequestState, AuthenticatorError> {
936 let resp = self
937 .http_client
938 .get(format!(
939 "{}/status/{}",
940 self.config.gateway_url(),
941 self.request_id
942 ))
943 .send()
944 .await?;
945
946 let status = resp.status();
947
948 if status.is_success() {
949 let body: GatewayStatusResponse = resp.json().await?;
950 Ok(body.status)
951 } else {
952 let body_text = resp.text().await.unwrap_or_else(|_| String::new());
953 Err(AuthenticatorError::GatewayError {
954 status,
955 body: body_text,
956 })
957 }
958 }
959}
960
961impl ProtocolSigner for Authenticator {
962 fn sign(&self, message: FieldElement) -> EdDSASignature {
963 self.signer
964 .offchain_signer_private_key()
965 .expose_secret()
966 .sign(*message)
967 }
968}
969
970pub trait OnchainKeyRepresentable {
972 fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError>;
979}
980
981impl OnchainKeyRepresentable for EdDSAPublicKey {
982 fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError> {
984 let mut compressed_bytes = Vec::new();
985 self.pk
986 .serialize_compressed(&mut compressed_bytes)
987 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
988 Ok(U256::from_le_slice(&compressed_bytes))
989 }
990}
991
992#[derive(Debug, thiserror::Error)]
994pub enum AuthenticatorError {
995 #[error(transparent)]
997 PrimitiveError(#[from] PrimitiveError),
998
999 #[error("Account is not registered for this authenticator.")]
1002 AccountDoesNotExist,
1003
1004 #[error("Account already exists for this authenticator.")]
1006 AccountAlreadyExists,
1007
1008 #[error("Error interacting with EVM contract: {0}")]
1010 ContractError(#[from] alloy::contract::Error),
1011
1012 #[error("Network error: {0}")]
1014 NetworkError(#[from] reqwest::Error),
1015
1016 #[error("Public key not found.")]
1018 PublicKeyNotFound,
1019
1020 #[error("Gateway error (status {status}): {body}")]
1022 GatewayError {
1023 status: StatusCode,
1025 body: String,
1027 },
1028
1029 #[error("Indexer error (status {status}): {body}")]
1031 IndexerError {
1032 status: StatusCode,
1034 body: String,
1036 },
1037
1038 #[error("Account creation timed out")]
1040 Timeout,
1041
1042 #[error("Invalid configuration for {attribute}: {reason}")]
1044 InvalidConfig {
1045 attribute: &'static str,
1047 reason: String,
1049 },
1050
1051 #[error("The provided credential is not valid for the provided proof request")]
1053 InvalidCredentialForProofRequest,
1054
1055 #[error("Registration error ({error_code}): {error_message}")]
1059 RegistrationError {
1060 error_code: String,
1062 error_message: String,
1064 },
1065
1066 #[error(transparent)]
1068 ProofError(#[from] ProofError),
1069
1070 #[error("{0}")]
1072 Generic(String),
1073}
1074
1075#[cfg(feature = "embed-zkeys")]
1076#[derive(Debug)]
1077enum PollResult {
1078 Retryable,
1079 TerminalError(AuthenticatorError),
1080}
1081
1082#[cfg(all(test, feature = "embed-zkeys"))]
1083mod tests {
1084 use super::*;
1085 use alloy::primitives::{U256, address};
1086 use std::{path::PathBuf, sync::OnceLock};
1087
1088 fn test_materials() -> (Arc<CircomGroth16Material>, Arc<CircomGroth16Material>) {
1089 static QUERY: OnceLock<Arc<CircomGroth16Material>> = OnceLock::new();
1090 static NULLIFIER: OnceLock<Arc<CircomGroth16Material>> = OnceLock::new();
1091
1092 let query = QUERY.get_or_init(|| {
1093 Arc::new(
1094 world_id_proof::proof::load_embedded_query_material(Option::<PathBuf>::None)
1095 .unwrap(),
1096 )
1097 });
1098 let nullifier = NULLIFIER.get_or_init(|| {
1099 Arc::new(
1100 world_id_proof::proof::load_embedded_nullifier_material(Option::<PathBuf>::None)
1101 .unwrap(),
1102 )
1103 });
1104
1105 (Arc::clone(query), Arc::clone(nullifier))
1106 }
1107
1108 #[tokio::test]
1111 async fn test_get_packed_account_data_from_indexer() {
1112 let mut server = mockito::Server::new_async().await;
1113 let indexer_url = server.url();
1114
1115 let test_address = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0");
1116 let expected_packed_index = U256::from(42);
1117
1118 let mock = server
1119 .mock("POST", "/packed-account")
1120 .match_header("content-type", "application/json")
1121 .match_body(mockito::Matcher::JsonString(
1122 serde_json::json!({
1123 "authenticator_address": test_address
1124 })
1125 .to_string(),
1126 ))
1127 .with_status(200)
1128 .with_header("content-type", "application/json")
1129 .with_body(
1130 serde_json::json!({
1131 "packed_account_data": format!("{:#x}", expected_packed_index)
1132 })
1133 .to_string(),
1134 )
1135 .create_async()
1136 .await;
1137
1138 let config = Config::new(
1139 None,
1140 1,
1141 address!("0x0000000000000000000000000000000000000001"),
1142 indexer_url,
1143 "http://gateway.example.com".to_string(),
1144 Vec::new(),
1145 2,
1146 )
1147 .unwrap();
1148
1149 let http_client = reqwest::Client::new();
1150
1151 let result = Authenticator::get_packed_account_data(
1152 test_address,
1153 None, &config,
1155 &http_client,
1156 )
1157 .await
1158 .unwrap();
1159
1160 assert_eq!(result, expected_packed_index);
1161 mock.assert_async().await;
1162 drop(server);
1163 }
1164
1165 #[tokio::test]
1166 async fn test_get_packed_account_data_from_indexer_error() {
1167 let mut server = mockito::Server::new_async().await;
1168 let indexer_url = server.url();
1169
1170 let test_address = address!("0x0000000000000000000000000000000000000099");
1171
1172 let mock = server
1173 .mock("POST", "/packed-account")
1174 .with_status(400)
1175 .with_header("content-type", "application/json")
1176 .with_body(
1177 serde_json::json!({
1178 "code": "account_does_not_exist",
1179 "message": "There is no account for this authenticator address"
1180 })
1181 .to_string(),
1182 )
1183 .create_async()
1184 .await;
1185
1186 let config = Config::new(
1187 None,
1188 1,
1189 address!("0x0000000000000000000000000000000000000001"),
1190 indexer_url,
1191 "http://gateway.example.com".to_string(),
1192 Vec::new(),
1193 2,
1194 )
1195 .unwrap();
1196
1197 let http_client = reqwest::Client::new();
1198
1199 let result =
1200 Authenticator::get_packed_account_data(test_address, None, &config, &http_client).await;
1201
1202 assert!(matches!(
1203 result,
1204 Err(AuthenticatorError::AccountDoesNotExist)
1205 ));
1206 mock.assert_async().await;
1207 drop(server);
1208 }
1209
1210 #[tokio::test]
1211 async fn test_signing_nonce_from_indexer() {
1212 let mut server = mockito::Server::new_async().await;
1213 let indexer_url = server.url();
1214
1215 let leaf_index = U256::from(1);
1216 let expected_nonce = U256::from(5);
1217
1218 let mock = server
1219 .mock("POST", "/signature-nonce")
1220 .match_header("content-type", "application/json")
1221 .match_body(mockito::Matcher::JsonString(
1222 serde_json::json!({
1223 "leaf_index": format!("{:#x}", leaf_index)
1224 })
1225 .to_string(),
1226 ))
1227 .with_status(200)
1228 .with_header("content-type", "application/json")
1229 .with_body(
1230 serde_json::json!({
1231 "signature_nonce": format!("{:#x}", expected_nonce)
1232 })
1233 .to_string(),
1234 )
1235 .create_async()
1236 .await;
1237
1238 let config = Config::new(
1239 None,
1240 1,
1241 address!("0x0000000000000000000000000000000000000001"),
1242 indexer_url,
1243 "http://gateway.example.com".to_string(),
1244 Vec::new(),
1245 2,
1246 )
1247 .unwrap();
1248
1249 let (query_material, nullifier_material) = test_materials();
1250 let authenticator = Authenticator {
1251 config,
1252 packed_account_data: leaf_index, signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1254 registry: None, http_client: reqwest::Client::new(),
1256 ws_connector: Connector::Plain,
1257 query_material,
1258 nullifier_material,
1259 };
1260
1261 let nonce = authenticator.signing_nonce().await.unwrap();
1262
1263 assert_eq!(nonce, expected_nonce);
1264 mock.assert_async().await;
1265 drop(server);
1266 }
1267
1268 #[tokio::test]
1269 async fn test_signing_nonce_from_indexer_error() {
1270 let mut server = mockito::Server::new_async().await;
1271 let indexer_url = server.url();
1272
1273 let mock = server
1274 .mock("POST", "/signature-nonce")
1275 .with_status(400)
1276 .with_header("content-type", "application/json")
1277 .with_body(
1278 serde_json::json!({
1279 "code": "invalid_leaf_index",
1280 "message": "Account index cannot be zero"
1281 })
1282 .to_string(),
1283 )
1284 .create_async()
1285 .await;
1286
1287 let config = Config::new(
1288 None,
1289 1,
1290 address!("0x0000000000000000000000000000000000000001"),
1291 indexer_url,
1292 "http://gateway.example.com".to_string(),
1293 Vec::new(),
1294 2,
1295 )
1296 .unwrap();
1297
1298 let (query_material, nullifier_material) = test_materials();
1299 let authenticator = Authenticator {
1300 config,
1301 packed_account_data: U256::ZERO,
1302 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1303 registry: None,
1304 http_client: reqwest::Client::new(),
1305 ws_connector: Connector::Plain,
1306 query_material,
1307 nullifier_material,
1308 };
1309
1310 let result = authenticator.signing_nonce().await;
1311
1312 assert!(matches!(
1313 result,
1314 Err(AuthenticatorError::IndexerError { .. })
1315 ));
1316 mock.assert_async().await;
1317 drop(server);
1318 }
1319}