Skip to main content

walletkit_core/authenticator/
mod.rs

1//! The Authenticator is the main component with which users interact with the World ID Protocol.
2
3use alloy_primitives::Address;
4use rand::rngs::OsRng;
5use std::sync::Arc;
6use world_id_core::{
7    api_types::{GatewayErrorCode, GatewayRequestState},
8    primitives::Config,
9    requests::{ProofResponse as CoreProofResponse, ResponseItem},
10    Authenticator as CoreAuthenticator, Credential as CoreCredential,
11    FieldElement as CoreFieldElement,
12    InitializingAuthenticator as CoreInitializingAuthenticator,
13};
14
15#[cfg(feature = "storage")]
16use crate::storage::{CredentialStore, StoragePaths};
17use crate::{
18    defaults::DefaultConfig,
19    error::WalletKitError,
20    primitives::ParseFromForeignBinding,
21    requests::{ProofRequest, ProofResponse},
22    Environment, FieldElement, Region, U256Wrapper,
23};
24
25#[cfg(feature = "storage")]
26mod with_storage;
27
28type Groth16Materials = (
29    Arc<world_id_core::proof::CircomGroth16Material>,
30    Arc<world_id_core::proof::CircomGroth16Material>,
31);
32
33#[cfg(not(feature = "storage"))]
34/// Loads embedded Groth16 query/nullifier material for authenticator initialization.
35///
36/// # Errors
37/// Returns an error if embedded material cannot be loaded or verified.
38fn load_embedded_materials() -> Result<Groth16Materials, WalletKitError> {
39    let query_material =
40        world_id_core::proof::load_embedded_query_material().map_err(|error| {
41            WalletKitError::Groth16MaterialEmbeddedLoad {
42                error: error.to_string(),
43            }
44        })?;
45    let nullifier_material = world_id_core::proof::load_embedded_nullifier_material()
46        .map_err(|error| {
47        WalletKitError::Groth16MaterialEmbeddedLoad {
48            error: error.to_string(),
49        }
50    })?;
51
52    Ok((Arc::new(query_material), Arc::new(nullifier_material)))
53}
54
55#[cfg(feature = "storage")]
56/// Loads cached Groth16 query/nullifier material from the provided storage paths.
57///
58/// # Errors
59/// Returns an error if cached material cannot be loaded or verified.
60fn load_cached_materials(
61    paths: &StoragePaths,
62) -> Result<Groth16Materials, WalletKitError> {
63    let query_zkey = paths.query_zkey_path();
64    let nullifier_zkey = paths.nullifier_zkey_path();
65    let query_graph = paths.query_graph_path();
66    let nullifier_graph = paths.nullifier_graph_path();
67
68    let query_material = load_query_material_from_cache(&query_zkey, &query_graph)?;
69    let nullifier_material =
70        load_nullifier_material_from_cache(&nullifier_zkey, &nullifier_graph)?;
71
72    Ok((Arc::new(query_material), Arc::new(nullifier_material)))
73}
74
75#[cfg(feature = "storage")]
76/// Loads cached query material from zkey/graph paths.
77///
78/// # Errors
79/// Returns an error if the cached query material cannot be loaded or verified.
80fn load_query_material_from_cache(
81    query_zkey: &std::path::Path,
82    query_graph: &std::path::Path,
83) -> Result<world_id_core::proof::CircomGroth16Material, WalletKitError> {
84    world_id_core::proof::load_query_material_from_paths(query_zkey, query_graph)
85        .map_err(|error| WalletKitError::Groth16MaterialCacheInvalid {
86            path: format!(
87                "{} and {}",
88                query_zkey.to_string_lossy(),
89                query_graph.to_string_lossy()
90            ),
91            error: error.to_string(),
92        })
93}
94
95#[cfg(feature = "storage")]
96#[expect(
97    clippy::unnecessary_wraps,
98    reason = "Temporary wrapper until world-id-core returns Result for nullifier path loader"
99)]
100/// Loads cached nullifier material from zkey/graph paths.
101///
102/// # Errors
103/// This currently mirrors a panicking upstream API and does not return an error path yet.
104/// It is intentionally wrapped in `Result` for forward compatibility with upstream.
105fn load_nullifier_material_from_cache(
106    nullifier_zkey: &std::path::Path,
107    nullifier_graph: &std::path::Path,
108) -> Result<world_id_core::proof::CircomGroth16Material, WalletKitError> {
109    // TODO: Switch to error mapping once world-id-core exposes
110    // `load_nullifier_material_from_paths` as `Result` instead of panicking.
111    Ok(world_id_core::proof::load_nullifier_material_from_paths(
112        nullifier_zkey,
113        nullifier_graph,
114    ))
115}
116
117/// The Authenticator is the main component with which users interact with the World ID Protocol.
118#[derive(Debug, uniffi::Object)]
119pub struct Authenticator {
120    inner: CoreAuthenticator,
121    #[cfg(feature = "storage")]
122    store: Arc<CredentialStore>,
123}
124
125#[uniffi::export(async_runtime = "tokio")]
126impl Authenticator {
127    /// Returns the packed account data for the holder's World ID.
128    ///
129    /// The packed account data is a 256 bit integer which includes the user's leaf index, their recovery counter,
130    /// and their pubkey id/commitment.
131    #[must_use]
132    pub fn packed_account_data(&self) -> U256Wrapper {
133        self.inner.packed_account_data.into()
134    }
135
136    /// Returns the leaf index for the holder's World ID.
137    ///
138    /// This is the index in the Merkle tree where the holder's World ID account is registered. It
139    /// should only be used inside the authenticator and never shared.
140    #[must_use]
141    pub fn leaf_index(&self) -> u64 {
142        self.inner.leaf_index()
143    }
144
145    /// Returns the Authenticator's `onchain_address`.
146    ///
147    /// See `world_id_core::Authenticator::onchain_address` for more details.
148    #[must_use]
149    pub fn onchain_address(&self) -> String {
150        self.inner.onchain_address().to_string()
151    }
152
153    /// Returns the packed account data for the holder's World ID fetching it from the on-chain registry.
154    ///
155    /// # Errors
156    /// Will error if the provided RPC URL is not valid or if there are RPC call failures.
157    pub async fn get_packed_account_data_remote(
158        &self,
159    ) -> Result<U256Wrapper, WalletKitError> {
160        let client = reqwest::Client::new(); // TODO: reuse client
161        let packed_account_data = CoreAuthenticator::get_packed_account_data(
162            self.inner.onchain_address(),
163            self.inner.registry().as_deref(),
164            &self.inner.config,
165            &client,
166        )
167        .await?;
168        Ok(packed_account_data.into())
169    }
170
171    /// Generates a blinding factor for a Credential sub (through OPRF Nodes).
172    ///
173    /// See [`CoreAuthenticator::generate_credential_blinding_factor`] for more details.
174    ///
175    /// # Errors
176    ///
177    /// - Will generally error if there are network issues or if the OPRF Nodes return an error.
178    /// - Raises an error if the OPRF Nodes configuration is not correctly set.
179    pub async fn generate_credential_blinding_factor_remote(
180        &self,
181        issuer_schema_id: u64,
182    ) -> Result<FieldElement, WalletKitError> {
183        Ok(self
184            .inner
185            .generate_credential_blinding_factor(issuer_schema_id)
186            .await
187            .map(Into::into)?)
188    }
189
190    /// Compute the `sub` for a credential from the authenticator's leaf index and a `blinding_factor`.
191    #[must_use]
192    pub fn compute_credential_sub(
193        &self,
194        blinding_factor: &FieldElement,
195    ) -> FieldElement {
196        CoreCredential::compute_sub(self.inner.leaf_index(), blinding_factor.0).into()
197    }
198}
199
200#[cfg(not(feature = "storage"))]
201#[uniffi::export(async_runtime = "tokio")]
202impl Authenticator {
203    /// Initializes a new Authenticator from a seed and with SDK defaults.
204    ///
205    /// The user's World ID must already be registered in the `WorldIDRegistry`,
206    /// otherwise a [`WalletKitError::AccountDoesNotExist`] error will be returned.
207    ///
208    /// # Errors
209    /// See `CoreAuthenticator::init` for potential errors.
210    #[uniffi::constructor]
211    pub async fn init_with_defaults(
212        seed: &[u8],
213        rpc_url: Option<String>,
214        environment: &Environment,
215        region: Option<Region>,
216    ) -> Result<Self, WalletKitError> {
217        let config = Config::from_environment(environment, rpc_url, region)?;
218        let (query_material, nullifier_material) = load_embedded_materials()?;
219        let authenticator =
220            CoreAuthenticator::init(seed, config, query_material, nullifier_material)
221                .await?;
222        Ok(Self {
223            inner: authenticator,
224        })
225    }
226
227    /// Initializes a new Authenticator from a seed and config.
228    ///
229    /// The user's World ID must already be registered in the `WorldIDRegistry`,
230    /// otherwise a [`WalletKitError::AccountDoesNotExist`] error will be returned.
231    ///
232    /// # Errors
233    /// Will error if the provided seed is not valid or if the config is not valid.
234    #[uniffi::constructor]
235    pub async fn init(seed: &[u8], config: &str) -> Result<Self, WalletKitError> {
236        let config =
237            Config::from_json(config).map_err(|_| WalletKitError::InvalidInput {
238                attribute: "config".to_string(),
239                reason: "Invalid config".to_string(),
240            })?;
241        let (query_material, nullifier_material) = load_embedded_materials()?;
242        let authenticator =
243            CoreAuthenticator::init(seed, config, query_material, nullifier_material)
244                .await?;
245        Ok(Self {
246            inner: authenticator,
247        })
248    }
249}
250
251#[cfg(feature = "storage")]
252#[uniffi::export(async_runtime = "tokio")]
253impl Authenticator {
254    /// Initializes a new Authenticator from a seed and with SDK defaults.
255    ///
256    /// The user's World ID must already be registered in the `WorldIDRegistry`,
257    /// otherwise a [`WalletKitError::AccountDoesNotExist`] error will be returned.
258    ///
259    /// # Errors
260    /// See `CoreAuthenticator::init` for potential errors.
261    #[uniffi::constructor]
262    pub async fn init_with_defaults(
263        seed: &[u8],
264        rpc_url: Option<String>,
265        environment: &Environment,
266        region: Option<Region>,
267        paths: Arc<StoragePaths>,
268        store: Arc<CredentialStore>,
269    ) -> Result<Self, WalletKitError> {
270        let config = Config::from_environment(environment, rpc_url, region)?;
271        let (query_material, nullifier_material) =
272            load_cached_materials(paths.as_ref())?;
273        let authenticator =
274            CoreAuthenticator::init(seed, config, query_material, nullifier_material)
275                .await?;
276        Ok(Self {
277            inner: authenticator,
278            store,
279        })
280    }
281
282    /// Initializes a new Authenticator from a seed and config.
283    ///
284    /// The user's World ID must already be registered in the `WorldIDRegistry`,
285    /// otherwise a [`WalletKitError::AccountDoesNotExist`] error will be returned.
286    ///
287    /// # Errors
288    /// Will error if the provided seed is not valid or if the config is not valid.
289    #[uniffi::constructor]
290    pub async fn init(
291        seed: &[u8],
292        config: &str,
293        paths: Arc<StoragePaths>,
294        store: Arc<CredentialStore>,
295    ) -> Result<Self, WalletKitError> {
296        let config =
297            Config::from_json(config).map_err(|_| WalletKitError::InvalidInput {
298                attribute: "config".to_string(),
299                reason: "Invalid config".to_string(),
300            })?;
301        let (query_material, nullifier_material) =
302            load_cached_materials(paths.as_ref())?;
303        let authenticator =
304            CoreAuthenticator::init(seed, config, query_material, nullifier_material)
305                .await?;
306        Ok(Self {
307            inner: authenticator,
308            store,
309        })
310    }
311
312    /// Generates a proof for the given proof request.
313    ///
314    /// # Errors
315    /// Returns an error if proof generation fails.
316    pub async fn generate_proof(
317        &self,
318        proof_request: &ProofRequest,
319        now: Option<u64>,
320    ) -> Result<ProofResponse, WalletKitError> {
321        let now = if let Some(n) = now {
322            n
323        } else {
324            let start = std::time::SystemTime::now();
325            start
326                .duration_since(std::time::UNIX_EPOCH)
327                .map_err(|e| WalletKitError::Generic {
328                    error: format!("Critical. Unable to determine SystemTime: {e}"),
329                })?
330                .as_secs()
331        };
332
333        // First check if the request can be fulfilled and which credentials should be used
334        let credential_list = self.store.list_credentials(None, now)?;
335        let credential_list = credential_list
336            .into_iter()
337            .map(|cred| cred.issuer_schema_id)
338            .collect::<std::collections::HashSet<_>>();
339        let credentials_to_prove = proof_request
340            .0
341            .credentials_to_prove(&credential_list)
342            .ok_or(WalletKitError::UnfulfillableRequest)?;
343
344        let (inclusion_proof, key_set) =
345            self.fetch_inclusion_proof_with_cache(now).await?;
346
347        // Next, generate the nullifier and check the replay guard
348        let nullifier = self
349            .inner
350            .generate_nullifier(&proof_request.0, inclusion_proof, key_set)
351            .await?;
352
353        // NOTE: In a normal flow this error can not be triggered since OPRF nodes have their own
354        // replay protection so the function will fail before this when attempting to generate the nullifier
355        if self
356            .store
357            .is_nullifier_replay(nullifier.verifiable_oprf_output.output.into(), now)?
358        {
359            return Err(WalletKitError::NullifierReplay);
360        }
361
362        let mut responses: Vec<ResponseItem> = vec![];
363
364        for request_item in credentials_to_prove {
365            let (credential, blinding_factor) = self
366                .store
367                .get_credential(request_item.issuer_schema_id, now)?
368                .ok_or(WalletKitError::CredentialNotIssued)?;
369
370            let session_id_r_seed = CoreFieldElement::random(&mut OsRng); // TODO: Properly fetch session seed from cache
371
372            let response_item = self.inner.generate_single_proof(
373                nullifier.clone(),
374                request_item,
375                &credential,
376                blinding_factor.0,
377                session_id_r_seed,
378                proof_request.0.session_id,
379                proof_request.0.created_at,
380            )?;
381            responses.push(response_item);
382        }
383
384        let response = CoreProofResponse {
385            id: proof_request.0.id.clone(),
386            version: world_id_core::requests::RequestVersion::V1,
387            responses,
388            error: None,
389            session_id: None, // TODO: This needs to be computed to be shareable
390        };
391
392        proof_request
393            .0
394            .validate_response(&response)
395            .map_err(|err| WalletKitError::ResponseValidation(err.to_string()))?;
396
397        self.store
398            .replay_guard_set(nullifier.verifiable_oprf_output.output.into(), now)?;
399
400        Ok(response.into())
401    }
402}
403
404/// Registration status for a World ID being created through the gateway.
405#[derive(Debug, Clone, uniffi::Enum)]
406pub enum RegistrationStatus {
407    /// Request queued but not yet batched.
408    Queued,
409    /// Request currently being batched.
410    Batching,
411    /// Request submitted on-chain.
412    Submitted,
413    /// Request finalized on-chain. The World ID is now registered.
414    Finalized,
415    /// Request failed during processing.
416    Failed {
417        /// Error message returned by the gateway.
418        error: String,
419        /// Specific error code, if available.
420        error_code: Option<String>,
421    },
422}
423
424impl From<GatewayRequestState> for RegistrationStatus {
425    fn from(state: GatewayRequestState) -> Self {
426        match state {
427            GatewayRequestState::Queued => Self::Queued,
428            GatewayRequestState::Batching => Self::Batching,
429            GatewayRequestState::Submitted { .. } => Self::Submitted,
430            GatewayRequestState::Finalized { .. } => Self::Finalized,
431            GatewayRequestState::Failed { error, error_code } => Self::Failed {
432                error,
433                error_code: error_code.map(|c: GatewayErrorCode| c.to_string()),
434            },
435        }
436    }
437}
438
439/// Represents an Authenticator in the process of being initialized.
440///
441/// The account is not yet registered in the `WorldIDRegistry` contract.
442/// Use this for non-blocking registration flows where you want to poll the status yourself.
443#[derive(uniffi::Object)]
444pub struct InitializingAuthenticator(CoreInitializingAuthenticator);
445
446#[uniffi::export(async_runtime = "tokio")]
447impl InitializingAuthenticator {
448    /// Registers a new World ID with SDK defaults.
449    ///
450    /// This returns immediately and does not wait for registration to complete.
451    /// The returned `InitializingAuthenticator` can be used to poll the registration status.
452    ///
453    /// # Errors
454    /// See `CoreAuthenticator::register` for potential errors.
455    #[uniffi::constructor]
456    pub async fn register_with_defaults(
457        seed: &[u8],
458        rpc_url: Option<String>,
459        environment: &Environment,
460        region: Option<Region>,
461        recovery_address: Option<String>,
462    ) -> Result<Self, WalletKitError> {
463        let recovery_address =
464            Address::parse_from_ffi_optional(recovery_address, "recovery_address")?;
465
466        let config = Config::from_environment(environment, rpc_url, region)?;
467
468        let initializing_authenticator =
469            CoreAuthenticator::register(seed, config, recovery_address).await?;
470
471        Ok(Self(initializing_authenticator))
472    }
473
474    /// Registers a new World ID.
475    ///
476    /// This returns immediately and does not wait for registration to complete.
477    /// The returned `InitializingAuthenticator` can be used to poll the registration status.
478    ///
479    /// # Errors
480    /// See `CoreAuthenticator::register` for potential errors.
481    #[uniffi::constructor]
482    pub async fn register(
483        seed: &[u8],
484        config: &str,
485        recovery_address: Option<String>,
486    ) -> Result<Self, WalletKitError> {
487        let recovery_address =
488            Address::parse_from_ffi_optional(recovery_address, "recovery_address")?;
489
490        let config =
491            Config::from_json(config).map_err(|_| WalletKitError::InvalidInput {
492                attribute: "config".to_string(),
493                reason: "Invalid config".to_string(),
494            })?;
495
496        let initializing_authenticator =
497            CoreAuthenticator::register(seed, config, recovery_address).await?;
498
499        Ok(Self(initializing_authenticator))
500    }
501
502    /// Polls the registration status from the gateway.
503    ///
504    /// # Errors
505    /// Will error if the network request fails or the gateway returns an error.
506    pub async fn poll_status(&self) -> Result<RegistrationStatus, WalletKitError> {
507        let status = self.0.poll_status().await?;
508        Ok(status.into())
509    }
510}
511
512#[cfg(all(test, feature = "storage"))]
513mod tests {
514    use super::*;
515    use crate::storage::cache_embedded_groth16_material;
516    use crate::storage::tests_utils::{
517        cleanup_test_storage, temp_root_path, InMemoryStorageProvider,
518    };
519    use alloy::primitives::address;
520
521    #[tokio::test]
522    async fn test_init_with_config_and_storage() {
523        // Install default crypto provider for rustls
524        let _ = rustls::crypto::ring::default_provider().install_default();
525
526        let mut mock_server = mockito::Server::new_async().await;
527
528        // Mock eth_call to return account data indicating account exists
529        mock_server
530            .mock("POST", "/")
531            .with_status(200)
532            .with_header("content-type", "application/json")
533            .with_body(
534                serde_json::json!({
535                    "jsonrpc": "2.0",
536                    "id": 1,
537                    "result": "0x0000000000000000000000000000000000000000000000000000000000000001"
538                })
539                .to_string(),
540            )
541            .create_async()
542            .await;
543
544        let seed = [2u8; 32];
545        let config = Config::new(
546            Some(mock_server.url()),
547            480,
548            address!("0x969947cFED008bFb5e3F32a25A1A2CDdf64d46fe"),
549            "https://world-id-indexer.stage-crypto.worldcoin.org".to_string(),
550            "https://world-id-gateway.stage-crypto.worldcoin.org".to_string(),
551            vec![],
552            2,
553        )
554        .unwrap();
555        let config = serde_json::to_string(&config).unwrap();
556
557        let root = temp_root_path();
558        let provider = InMemoryStorageProvider::new(&root);
559        let store = CredentialStore::from_provider(&provider).expect("store");
560        store.init(42, 100).expect("init storage");
561        cache_embedded_groth16_material(store.storage_paths().expect("paths"))
562            .expect("cache material");
563
564        let paths = store.storage_paths().expect("paths");
565        Authenticator::init(&seed, &config, paths, Arc::new(store))
566            .await
567            .unwrap();
568        drop(mock_server);
569        cleanup_test_storage(&root);
570    }
571}