Skip to main content

world_id_proof/
oprf_query.rs

1//! Shared helpers for generating query proofs and executing
2//! distributed generic OPRF computations.
3
4use ark_bn254::Bn254;
5use ark_ff::PrimeField;
6use ark_groth16::Proof;
7use eyre::Context;
8use groth16_material::circom::CircomGroth16Material;
9use serde::Serialize;
10
11use taceo_oprf::{
12    client::{Connector, VerifiableOprfOutput},
13    core::oprf::BlindingFactor,
14    types::ShareEpoch,
15};
16
17use world_id_primitives::{
18    FieldElement, ProofRequest, SessionFieldElement, TREE_DEPTH,
19    circuit_inputs::QueryProofCircuitInput,
20    oprf::{CredentialBlindingFactorOprfRequestAuthV1, NullifierOprfRequestAuthV1, OprfModule},
21};
22
23use crate::{
24    AuthenticatorProofInput,
25    proof::{OPRF_PROOF_DS, ProofError, errors},
26};
27
28#[expect(unused_imports, reason = "used for docs")]
29use world_id_primitives::SessionNullifier;
30
31/// The main entry point to execute OPRF computations using the
32/// OPRF nodes.
33pub struct OprfEntrypoint<'a> {
34    /// The list of endpoints of all OPRF nodes.
35    services: &'a [String],
36    /// The minimum number of OPRF nodes responses required to compute a valid nullifier. The
37    /// source of truth for this value lives in the `OprfKeyRegistry` contract.
38    threshold: usize,
39    /// The material for the query proof circuit.
40    query_material: &'a CircomGroth16Material,
41    /// See [`AuthenticatorProofInput`] for more details.
42    authenticator_input: &'a AuthenticatorProofInput,
43    /// The network connector to make requests.
44    connector: &'a Connector,
45}
46
47/// A complete verifiable OPRF output with the entire inputs to the Query Proof circuit.
48#[derive(Debug, Clone)]
49pub struct FullOprfOutput {
50    /// The raw inputs to the Query Proof circuit
51    pub query_proof_input: QueryProofCircuitInput<TREE_DEPTH>,
52    /// The result of the distributed OPRF protocol.
53    pub verifiable_oprf_output: VerifiableOprfOutput,
54}
55
56impl<'a> OprfEntrypoint<'a> {
57    pub fn new(
58        services: &'a [String],
59        threshold: usize,
60        query_material: &'a CircomGroth16Material,
61        authenticator_input: &'a AuthenticatorProofInput,
62        connector: &'a Connector,
63    ) -> Self {
64        Self {
65            services,
66            threshold,
67            query_material,
68            authenticator_input,
69            connector,
70        }
71    }
72
73    /// Generates a blinding factor for a Credential's `sub` through the OPRF nodes.
74    ///
75    /// This method will handle the signature from the Authenticator authorizing the
76    /// request for the OPRF nodes.
77    ///
78    /// # Arguments
79    /// - `issuer_schema_id`: The schema ID of the credential issuer for which the blinding factor
80    ///   is being generated.
81    ///
82    /// # Errors
83    ///
84    /// Returns [`ProofError`] in the following cases:
85    /// * `PublicKeyNotFound` - the public key for the given authenticator private key is not found in the `key_set`.
86    /// * `InvalidDLogProof` – the `DLog` equality proof could not be verified.
87    /// * Other errors may propagate from network requests, proof generation, or Groth16 verification.
88    pub async fn gen_credential_blinding_factor<R: rand::CryptoRng + rand::RngCore>(
89        &self,
90        rng: &mut R,
91        issuer_schema_id: u64,
92    ) -> Result<(FieldElement, ShareEpoch), ProofError> {
93        // Currently, the `action` (i.e. query) is always zero, this may change in future
94        let action = FieldElement::ZERO;
95
96        let result = Self::generate_query_proof(
97            self.query_material,
98            self.authenticator_input,
99            action,
100            FieldElement::ZERO,
101            issuer_schema_id.into(),
102            rng,
103        )?;
104
105        let auth = CredentialBlindingFactorOprfRequestAuthV1 {
106            proof: result.proof.into(),
107            action: *action,
108            nonce: *FieldElement::ZERO,
109            merkle_root: *self.authenticator_input.inclusion_proof.root,
110            issuer_schema_id,
111        };
112
113        let verifiable_oprf_output = Self::execute_distributed_oprf(
114            self.services,
115            self.threshold,
116            result.query_hash,
117            result.blinding_factor,
118            auth,
119            OprfModule::CredentialBlindingFactor,
120            self.connector.clone(),
121        )
122        .await?;
123        Ok((
124            verifiable_oprf_output.output.into(),
125            verifiable_oprf_output.epoch,
126        ))
127    }
128
129    /// Generates a nullifier through the provided OPRF nodes for
130    /// a specific proof request.
131    ///
132    /// # Note on Session Proofs
133    /// A randomized action is required on Session Proofs to ensure the output nullifier from the Uniqueness Proof
134    /// circuit is unique (otherwise the one-time use property of nullifiers would fail). Please see the "Future"
135    /// section in the [`SessionNullifier`] documentation for more details on how this is expected to be deprecated with
136    /// a future update.
137    ///
138    /// # Errors
139    /// Returns [`ProofError`] in the following cases:
140    /// * `PublicKeyNotFound` - the public key for the given authenticator private key is not found in the `key_set`.
141    /// * `InvalidDLogProof` – the `DLog` equality proof could not be verified.
142    /// * Other errors may propagate from network requests, proof generation, or Groth16 verification.
143    pub async fn gen_nullifier<R: rand::CryptoRng + rand::RngCore>(
144        &self,
145        rng: &mut R,
146        proof_request: &ProofRequest,
147    ) -> Result<FullOprfOutput, ProofError> {
148        let action = if proof_request.is_session_proof() {
149            // For session proofs a random action is used internally. This is opaque to RPs who receive
150            // it within the encoded `SessionNullifier`
151            FieldElement::random_for_session(rng)
152        } else {
153            // If the RP didn't provide an action, we provide a default.
154            proof_request.action.unwrap_or(FieldElement::ZERO)
155        };
156
157        let result = Self::generate_query_proof(
158            self.query_material,
159            self.authenticator_input,
160            action,
161            proof_request.nonce,
162            proof_request.rp_id.into(),
163            rng,
164        )?;
165
166        let auth = NullifierOprfRequestAuthV1 {
167            proof: result.proof.into(),
168            action: *action,
169            nonce: *proof_request.nonce,
170            merkle_root: *self.authenticator_input.inclusion_proof.root,
171            current_time_stamp: proof_request.created_at,
172            expiration_timestamp: proof_request.expires_at,
173            signature: proof_request.signature,
174            rp_id: proof_request.rp_id,
175        };
176
177        let verifiable_oprf_output = Self::execute_distributed_oprf(
178            self.services,
179            self.threshold,
180            result.query_hash,
181            result.blinding_factor,
182            auth,
183            OprfModule::Nullifier,
184            self.connector.clone(),
185        )
186        .await?;
187
188        Ok(FullOprfOutput {
189            query_proof_input: result.query_proof_input,
190            verifiable_oprf_output,
191        })
192    }
193}
194
195impl<'a> OprfEntrypoint<'a> {
196    /// Generates a query proof: creates a blinding factor, computes
197    /// the query hash, signs it, builds `QueryProofCircuitInput`, and
198    /// runs Groth16 prove + verify.
199    fn generate_query_proof<R: rand::CryptoRng + rand::RngCore>(
200        query_material: &CircomGroth16Material,
201        authenticator_input: &AuthenticatorProofInput,
202        action: FieldElement,
203        nonce: FieldElement,
204        scope: FieldElement,
205        rng: &mut R,
206    ) -> Result<QueryProofResult, ProofError> {
207        let blinding_factor = BlindingFactor::rand(rng);
208
209        let siblings: [ark_babyjubjub::Fq; TREE_DEPTH] =
210            authenticator_input.inclusion_proof.siblings.map(|s| *s);
211
212        let query_hash = world_id_primitives::authenticator::oprf_query_digest(
213            authenticator_input.inclusion_proof.leaf_index,
214            action,
215            scope,
216        );
217        let signature = authenticator_input.private_key.sign(*query_hash);
218
219        let query_proof_input = QueryProofCircuitInput::<TREE_DEPTH> {
220            pk: authenticator_input.key_set.as_affine_array(),
221            pk_index: authenticator_input.key_index.into(),
222            s: signature.s,
223            r: signature.r,
224            merkle_root: *authenticator_input.inclusion_proof.root,
225            depth: ark_babyjubjub::Fq::from(TREE_DEPTH as u64),
226            mt_index: authenticator_input.inclusion_proof.leaf_index.into(),
227            siblings,
228            beta: blinding_factor.beta(),
229            rp_id: *scope,
230            action: *action,
231            nonce: *nonce,
232        };
233        let _ = errors::check_query_input_validity(&query_proof_input)?;
234
235        tracing::debug!("generating query proof");
236        let (proof, public_inputs) = query_material.generate_proof(&query_proof_input, rng)?;
237        query_material.verify_proof(&proof, &public_inputs)?;
238        tracing::debug!("generated query proof");
239
240        Ok(QueryProofResult {
241            query_proof_input,
242            proof,
243            query_hash: *query_hash,
244            blinding_factor,
245        })
246    }
247
248    /// Executes the distributed OPRF protocol against the given
249    /// service endpoints.
250    async fn execute_distributed_oprf<A: Clone + Serialize + Send + 'static>(
251        services: &[String],
252        threshold: usize,
253        query_hash: ark_babyjubjub::Fq,
254        blinding_factor: BlindingFactor,
255        auth: A,
256        oprf_module: OprfModule,
257        connector: Connector,
258    ) -> Result<VerifiableOprfOutput, ProofError> {
259        tracing::debug!("executing distributed OPRF");
260
261        let service_uris = taceo_oprf::client::to_oprf_uri_many(services, oprf_module)
262            .context("while building service URI")?;
263
264        let verifiable_oprf_output = taceo_oprf::client::distributed_oprf(
265            &service_uris,
266            threshold,
267            query_hash,
268            blinding_factor,
269            ark_babyjubjub::Fq::from_be_bytes_mod_order(OPRF_PROOF_DS),
270            auth,
271            connector,
272        )
273        .await?;
274
275        Ok(verifiable_oprf_output)
276    }
277}
278
279/// Intermediate result from query proof generation.
280struct QueryProofResult {
281    query_proof_input: QueryProofCircuitInput<TREE_DEPTH>,
282    proof: Proof<Bn254>,
283    query_hash: ark_babyjubjub::Fq,
284    blinding_factor: BlindingFactor,
285}