Skip to main content

taceo_groth16_material/
circom.rs

1//! Provides utilities for loading Circom Groth16 proving keys and circuits, computing witnesses,
2//! and generating/verifying Groth16 proofs using the `arkworks` ecosystem. Only the `bn254` curve is supported.
3
4use ark_bn254::Bn254;
5use ark_ff::AdditiveGroup as _;
6use ark_ff::Field as _;
7use ark_ff::LegendreSymbol;
8use ark_ff::UniformRand as _;
9use ark_serialize::CanonicalDeserialize;
10use ark_serialize::CanonicalSerialize;
11use circom_witness_rs::Graph;
12use groth16::CircomReduction;
13use groth16::Groth16;
14use rand::{CryptoRng, Rng};
15use ruint::aliases::U256;
16use sha2::Digest as _;
17use std::io::Write as _;
18use std::ops::Shr;
19use std::sync::Arc;
20use std::{collections::HashMap, path::Path};
21
22use crate::Groth16Error;
23
24pub use ark_groth16::Proof;
25pub use ark_serialize::Compress;
26pub use ark_serialize::Validate;
27pub use circom_types::groth16::ArkZkey;
28pub use circom_witness_rs::BlackBoxFunction;
29
30/// Trait for preparing proof inputs for zk-SNARK circuits.
31///
32/// The `prepare_input` method converts the implementing type into a
33/// `HashMap<String, Vec<U256>>`, which is the expected format for proof inputs for Circom.
34pub trait ProofInput {
35    /// Prepares the input for zk-SNARK proof generation.
36    ///
37    /// Returns a `HashMap<String, Vec<U256>>` representing the input.
38    ///
39    /// # Example
40    /// ```rust
41    /// # use std::collections::HashMap;
42    /// # use ruint::aliases::U256;
43    /// # use taceo_groth16_material::circom::ProofInput;
44    ///
45    /// struct MyInput {
46    ///    a: U256,
47    ///    b: U256,
48    /// }
49    ///
50    /// impl ProofInput for MyInput {
51    ///     fn prepare_input(&self) -> HashMap<String, Vec<U256>> {
52    ///         let mut input = HashMap::new();
53    ///         input.insert("a".to_string(), vec![self.a]);
54    ///         input.insert("b".to_string(), vec![self.b]);
55    ///         input
56    ///     }
57    /// }
58    /// ```
59    fn prepare_input(&self) -> HashMap<String, Vec<U256>>;
60}
61
62impl ProofInput for HashMap<String, Vec<U256>> {
63    fn prepare_input(&self) -> HashMap<String, Vec<U256>> {
64        self.to_owned()
65    }
66}
67
68/// Errors that can occur while loading or parsing a `.zkey` or graph file.
69#[derive(Debug, thiserror::Error)]
70pub enum ZkeyError {
71    /// The SHA-256 fingerprint of the `.zkey` did not match the expected value.
72    #[error("invalid zkey - wrong sha256 fingerprint: {0}")]
73    ZkeyFingerprintMismatch(String),
74    /// The SHA-256 fingerprint of the witness graph did not match the expected value.
75    #[error("invalid graph - wrong sha256 fingerprint: {0}")]
76    GraphFingerprintMismatch(String),
77    /// Could not parse the `.zkey` file.
78    #[error("Could not parse zkey - see wrapped error")]
79    ZkeyInvalid(#[source] eyre::Report),
80    /// Could not parse the graph file.
81    #[error(transparent)]
82    GraphInvalid(#[from] eyre::Report),
83    /// Any I/O error encountered while reading the `.zkey` or graph file
84    #[error(transparent)]
85    IoError(#[from] std::io::Error),
86}
87
88/// Errors that can occur while serializing [`CircomGroth16Material`] into bytes/files.
89#[derive(Debug, thiserror::Error)]
90pub enum MaterialSerializationError {
91    /// Could not serialize the `.zkey` bytes.
92    #[error("could not serialize zkey - see wrapped error")]
93    ZkeySerialization(#[source] ark_serialize::SerializationError),
94    /// Could not serialize the witness graph bytes.
95    #[error("could not serialize graph - see wrapped error")]
96    GraphSerialization(#[source] postcard::Error),
97    /// Any I/O error encountered while writing the serialized bytes.
98    #[error(transparent)]
99    IoError(#[from] std::io::Error),
100}
101
102#[cfg(any(feature = "reqwest", feature = "reqwest-blocking"))]
103impl From<reqwest::Error> for ZkeyError {
104    fn from(value: reqwest::Error) -> Self {
105        Self::IoError(std::io::Error::new(std::io::ErrorKind::InvalidData, value))
106    }
107}
108
109impl From<circom_types::ZkeyParserError> for ZkeyError {
110    fn from(value: circom_types::ZkeyParserError) -> Self {
111        Self::ZkeyInvalid(eyre::eyre!(value))
112    }
113}
114
115impl From<ark_serialize::SerializationError> for ZkeyError {
116    fn from(value: ark_serialize::SerializationError) -> Self {
117        Self::ZkeyInvalid(eyre::eyre!(value))
118    }
119}
120
121/// Core material for generating groth-16 zero-knowledge proofs based on Circom. Currently we only support `bn254` material, because the underlying witness extension library only support `bn254`.
122///
123/// Holds the proving keys, constraint matrices and graphs for the witness extension.
124/// Provides methods to:
125/// - Generate proofs from structured inputs
126/// - Verify proofs internally immediately after generation
127#[derive(Clone)]
128pub struct CircomGroth16Material {
129    zkey: ArkZkey<Bn254>,
130    /// The graph for witness extension
131    graph: Graph,
132    /// The black-box functions needed for witness extension
133    bbfs: HashMap<String, BlackBoxFunction>,
134}
135
136/// Builder for `CircomGroth16Material`.
137/// Allows configuring options like compression, validation, fingerprints, and black-box functions.
138///
139/// # Example
140///
141/// ```rust,no_run
142/// # use taceo_groth16_material::circom::{CircomGroth16MaterialBuilder, Compress, Validate};
143/// # use std::collections::HashMap;
144///
145/// let material = CircomGroth16MaterialBuilder::new()
146///                  .compress(Compress::No)
147///                  .validate(Validate::Yes)
148///                  .fingerprint_zkey("d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82a1".to_owned())
149///                  .build_from_paths("./circuit.zkey", "./circuit.graph");
150/// ```
151pub struct CircomGroth16MaterialBuilder {
152    compress: Compress,
153    validate: Validate,
154    fingerprint_zkey: Option<String>,
155    fingerprint_graph: Option<String>,
156    bbfs: HashMap<String, BlackBoxFunction>,
157}
158
159/// Builder-style serializer for exporting [`CircomGroth16Material`] into binary representations.
160pub struct CircomGroth16MaterialSerializer<'a> {
161    material: &'a CircomGroth16Material,
162    compress: Compress,
163}
164
165impl Default for CircomGroth16MaterialBuilder {
166    fn default() -> Self {
167        Self {
168            compress: Compress::No,
169            validate: Validate::Yes,
170            fingerprint_zkey: None,
171            fingerprint_graph: None,
172            bbfs: HashMap::default(),
173        }
174    }
175}
176
177impl CircomGroth16MaterialBuilder {
178    /// Creates a new `CircomGroth16MaterialBuilder` with default settings.
179    ///
180    /// Defaults:
181    /// - `compress`: `Compress::No`
182    /// - `validate`: `Validate::Yes`
183    /// - `fingerprint_zkey`: No fingerprint verification of the Zkey.
184    /// - `fingerprint_graph`: No fingerprint verification of the graph.
185    /// - `bbfs`: No black-box functions.
186    pub fn new() -> Self {
187        Self::default()
188    }
189
190    /// Sets the compression mode for deserializing the `.zkey` file. See [ark_serialize::Compress] for details.
191    pub fn compress(mut self, compress: Compress) -> Self {
192        self.compress = compress;
193        self
194    }
195
196    /// Sets the validation mode for deserializing the `.zkey` file. See [ark_serialize::Validate] for details.
197    pub fn validate(mut self, validate: Validate) -> Self {
198        self.validate = validate;
199        self
200    }
201
202    /// Sets the expected SHA-256 fingerprint for the `.zkey` file. If provided, the fingerprint will be verified during loading.
203    pub fn fingerprint_zkey(mut self, fingerprint_zkey: String) -> Self {
204        self.fingerprint_zkey = Some(fingerprint_zkey);
205        self
206    }
207
208    /// Sets the expected SHA-256 fingerprint for the graph file. If provided, the fingerprint will be verified during loading.
209    pub fn fingerprint_graph(mut self, fingerprint_graph: String) -> Self {
210        self.fingerprint_graph = Some(fingerprint_graph);
211        self
212    }
213
214    /// Adds custom black-box functions for witness extension. See [circom_witness_rs::BlackBoxFunction] for details.
215    pub fn add_bbfs(mut self, bbfs: HashMap<String, BlackBoxFunction>) -> Self {
216        self.bbfs.extend(bbfs);
217        self
218    }
219
220    /// Adds the standard black-box function `bbf_inv` for field inversion.
221    ///
222    /// The function implements the following Circom logic:
223    /// ```text
224    /// function bbf_inv(in) {
225    ///     return in!=0 ? 1/in : 0;
226    /// }
227    /// ```
228    pub fn bbf_inv(mut self) -> Self {
229        self.bbfs.insert(
230            "bbf_inv".to_string(),
231            Arc::new(move |args: &[ark_bn254::Fr]| -> ark_bn254::Fr {
232                // function bbf_inv(in) {
233                //     return in!=0 ? 1/in : 0;
234                // }
235                args[0].inverse().unwrap_or(ark_bn254::Fr::ZERO)
236            }),
237        );
238
239        self
240    }
241
242    /// Adds a black-box function `bbf_legendre` for computing the legendre symbol.
243    ///
244    /// The function implements the following Circom logic:
245    /// ```text
246    /// function bbf_legendre(in) {
247    ///     return in!=0 ? 1/in : 0;
248    /// }
249    /// ```
250    pub fn bbf_legendre(mut self) -> Self {
251        self.bbfs.insert(
252            "bbf_legendre".to_string(),
253            Arc::new(move |args: &[ark_bn254::Fr]| -> ark_bn254::Fr {
254                match args[0].legendre() {
255                    LegendreSymbol::Zero => ark_bn254::Fr::from(0u64),
256                    LegendreSymbol::QuadraticResidue => ark_bn254::Fr::from(1u64),
257                    LegendreSymbol::QuadraticNonResidue => -ark_bn254::Fr::from(1u64),
258                }
259            }),
260        );
261
262        self
263    }
264
265    /// Adds a black-box function `bbf_sqrt_unchecked` for computing the square root of a field element.
266    pub fn bbf_sqrt_unchecked(mut self) -> Self {
267        self.bbfs.insert(
268            "bbf_sqrt_unchecked".to_string(),
269            Arc::new(move |args: &[ark_bn254::Fr]| -> ark_bn254::Fr {
270                args[0].sqrt().unwrap_or(ark_bn254::Fr::ZERO)
271            }),
272        );
273        self
274    }
275
276    /// Adds a black-box function `bbf_sqrt_input` for selecting between two field elements based on a condition.
277    ///
278    /// The function implements the following Circom logic:
279    /// ```text
280    /// function bbf_sqrt_input(l, a, na) {
281    ///   if (l != -1) {
282    ///     return a;
283    ///   } else {
284    ///     return na;
285    ///   }
286    /// }
287    /// ```
288    pub fn bbf_sqrt_input(mut self) -> Self {
289        self.bbfs.insert(
290            "bbf_sqrt_input".to_string(),
291            Arc::new(move |args: &[ark_bn254::Fr]| -> ark_bn254::Fr {
292                // function bbf_sqrt_input(l, a, na) {
293                //     if (l != -1) {
294                //         return a;
295                //     } else {
296                //         return na;
297                //     }
298                // }
299                if args[0] != -ark_bn254::Fr::ONE {
300                    args[1]
301                } else {
302                    args[2]
303                }
304            }),
305        );
306        self
307    }
308
309    /// Adds a black-box function `bbf_num_2_bits_helper`.
310    ///
311    /// The function implements the following Circom logic:
312    /// ```text
313    /// function bbf_num_2_bits_helper(in, i) {
314    ///     return (in >> i) & 1;
315    /// }
316    /// ```
317    pub fn bbf_num_2_bits_helper(mut self) -> Self {
318        self.bbfs.insert(
319            "bbf_num_2_bits_helper".to_string(),
320            Arc::new(move |args: &[ark_bn254::Fr]| -> ark_bn254::Fr {
321                // function bbf_num_2_bits_helper(in, i) {
322                //     return (in >> i) & 1;
323                // }
324                let a: U256 = args[0].into();
325                let b: U256 = args[1].into();
326                let ls_limb = b.as_limbs()[0];
327                ark_bn254::Fr::new((a.shr(ls_limb as usize) & U256::from(1)).into())
328            }),
329        );
330        self
331    }
332
333    /// Loads the Groth16 material from `.zkey` and graph files and verifies their fingerprints if provided.
334    pub fn build_from_paths(
335        self,
336        zkey_path: impl AsRef<Path>,
337        graph_path: impl AsRef<Path>,
338    ) -> Result<CircomGroth16Material, ZkeyError> {
339        let zkey_bytes = std::fs::read(zkey_path)?;
340        let graph_bytes = std::fs::read(graph_path)?;
341        self.build_from_bytes(&zkey_bytes, &graph_bytes)
342    }
343
344    /// Builds Groth16 material directly from `.zkey` and graph readers.
345    pub fn build_from_reader(
346        self,
347        mut zkey_reader: impl std::io::Read,
348        mut graph_reader: impl std::io::Read,
349    ) -> Result<CircomGroth16Material, ZkeyError> {
350        let mut zkey_bytes = Vec::new();
351        zkey_reader.read_to_end(&mut zkey_bytes)?;
352        let mut graph_bytes = Vec::new();
353        graph_reader.read_to_end(&mut graph_bytes)?;
354        self.build_from_bytes(&zkey_bytes, &graph_bytes)
355    }
356
357    /// Builds Groth16 material directly from in-memory `.zkey` and graph bytes.
358    pub fn build_from_bytes(
359        self,
360        zkey_bytes: &[u8],
361        graph_bytes: &[u8],
362    ) -> Result<CircomGroth16Material, ZkeyError> {
363        let validate = if let Some(should_fingerprint) = self.fingerprint_zkey {
364            let is_fingerprint = hex::encode(sha2::Sha256::digest(zkey_bytes));
365            if is_fingerprint != should_fingerprint {
366                return Err(ZkeyError::ZkeyFingerprintMismatch(is_fingerprint));
367            }
368            Validate::No
369        } else {
370            self.validate
371        };
372
373        let zkey = circom_types::groth16::ArkZkey::deserialize_with_mode(
374            zkey_bytes,
375            self.compress,
376            validate,
377        )?;
378        if let Some(should_fingerprint) = self.fingerprint_graph {
379            let is_fingerprint = hex::encode(sha2::Sha256::digest(graph_bytes));
380            if is_fingerprint != should_fingerprint {
381                return Err(ZkeyError::GraphFingerprintMismatch(is_fingerprint));
382            }
383        }
384        let graph = circom_witness_rs::init_graph(graph_bytes).map_err(ZkeyError::GraphInvalid)?;
385        Ok(CircomGroth16Material {
386            zkey,
387            graph,
388            bbfs: self.bbfs,
389        })
390    }
391
392    /// Downloads `.zkey` and graph files from the provided URLs and builds the Groth16 material.
393    #[cfg(feature = "reqwest")]
394    pub async fn build_from_urls(
395        self,
396        zkey_url: impl reqwest::IntoUrl,
397        graph_url: impl reqwest::IntoUrl,
398    ) -> Result<CircomGroth16Material, ZkeyError> {
399        let zkey_bytes = reqwest::get(zkey_url).await?.bytes().await?;
400        let graph_bytes = reqwest::get(graph_url).await?.bytes().await?;
401        self.build_from_bytes(&zkey_bytes, &graph_bytes)
402    }
403
404    /// Downloads `.zkey` and graph files from the provided URLs and builds the Groth16 material. Uses the blocking reqwest client.
405    #[cfg(feature = "reqwest-blocking")]
406    pub fn build_from_urls_blocking(
407        self,
408        zkey_url: impl reqwest::IntoUrl,
409        graph_url: impl reqwest::IntoUrl,
410    ) -> Result<CircomGroth16Material, ZkeyError> {
411        let zkey_bytes = reqwest::blocking::get(zkey_url)?.bytes()?;
412        let graph_bytes = reqwest::blocking::get(graph_url)?.bytes()?;
413        self.build_from_bytes(&zkey_bytes, &graph_bytes)
414    }
415}
416
417impl CircomGroth16Material {
418    /// Creates a serializer for exporting the loaded `.zkey` and graph into binary blobs.
419    ///
420    /// By default, `.zkey` bytes are serialized with [`Compress::No`].
421    ///
422    /// # Example
423    /// ```rust,no_run
424    /// # use taceo_groth16_material::circom::{CircomGroth16Material, Compress};
425    /// # let material: CircomGroth16Material = unimplemented!();
426    /// let (zkey_bytes, graph_bytes) = material
427    ///     .serializer()
428    ///     .compress(Compress::No)
429    ///     .to_bytes()
430    ///     .expect("can serialize material");
431    /// # let _ = (zkey_bytes, graph_bytes);
432    /// ```
433    pub fn serializer(&self) -> CircomGroth16MaterialSerializer<'_> {
434        CircomGroth16MaterialSerializer {
435            material: self,
436            compress: Compress::No,
437        }
438    }
439
440    /// Returns a reference to the underlying [ArkZkey].
441    pub fn zkey(&self) -> &ArkZkey<Bn254> {
442        &self.zkey
443    }
444
445    /// Computes a witness vector from a circuit graph and inputs.
446    pub fn generate_witness(
447        &self,
448        inputs: &impl ProofInput,
449    ) -> Result<Vec<ark_bn254::Fr>, Groth16Error> {
450        let witness = circom_witness_rs::calculate_witness(
451            inputs.prepare_input(),
452            &self.graph,
453            Some(&self.bbfs),
454        )
455        .map_err(Groth16Error::WitnessGeneration)?
456        .into_iter()
457        .map(|v| ark_bn254::Fr::from(ark_ff::BigInt(v.into_limbs())))
458        .collect::<Vec<_>>();
459        Ok(witness)
460    }
461
462    /// Generates a Groth16 proof from a witness and verifies it.
463    ///
464    /// Doesn't verify the proof internally.
465    pub fn generate_proof_from_witness<R: Rng + CryptoRng>(
466        &self,
467        witness: &[ark_bn254::Fr],
468        rng: &mut R,
469    ) -> Result<(Proof<Bn254>, Vec<ark_bn254::Fr>), Groth16Error> {
470        let r = ark_bn254::Fr::rand(rng);
471        let s = ark_bn254::Fr::rand(rng);
472
473        let (matrices, pk) = self.zkey.as_inner();
474        let proof = Groth16::prove::<CircomReduction>(pk, r, s, matrices, witness)
475            .map_err(Groth16Error::ProofGeneration)?;
476
477        let inputs = witness[1..matrices.num_instance_variables].to_vec();
478        Ok((proof, inputs))
479    }
480
481    /// Generates a Groth16 proof from structured inputs.
482    ///
483    /// This internally computes the witness using the provided inputs and then generates the proof.
484    pub fn generate_proof<R: Rng + CryptoRng>(
485        &self,
486        inputs: &impl ProofInput,
487        rng: &mut R,
488    ) -> Result<(Proof<Bn254>, Vec<ark_bn254::Fr>), Groth16Error> {
489        let witness = self.generate_witness(inputs)?;
490        self.generate_proof_from_witness(&witness, rng)
491    }
492
493    /// Verifies a Groth16 proof and accompanying public inputs using the verification key.
494    pub fn verify_proof(
495        &self,
496        proof: &Proof<Bn254>,
497        public_inputs: &[ark_bn254::Fr],
498    ) -> Result<(), Groth16Error> {
499        Groth16::verify(&self.zkey.pk.vk, proof, public_inputs)
500            .map_err(|_| Groth16Error::InvalidProof)
501    }
502}
503
504impl<'a> CircomGroth16MaterialSerializer<'a> {
505    /// Sets the compression mode for serializing the `.zkey` bytes.
506    pub fn compress(mut self, compress: Compress) -> Self {
507        self.compress = compress;
508        self
509    }
510
511    /// Serializes the material into `(zkey_bytes, graph_bytes)`.
512    ///
513    /// The graph bytes are encoded with `postcard` as `(nodes, signals, input_mapping)`.
514    ///
515    /// # Example
516    /// ```rust,no_run
517    /// # use taceo_groth16_material::circom::{CircomGroth16Material, Compress};
518    /// # let material: CircomGroth16Material = unimplemented!();
519    /// let (_zkey_bytes, _graph_bytes) = material
520    ///     .serializer()
521    ///     .compress(Compress::No)
522    ///     .to_bytes()
523    ///     .expect("can export bytes");
524    /// ```
525    pub fn to_bytes(self) -> Result<(Vec<u8>, Vec<u8>), MaterialSerializationError> {
526        let mut zkey_bytes = Vec::new();
527        let mut graph_bytes = Vec::new();
528        self.to_writer(&mut zkey_bytes, &mut graph_bytes)?;
529        Ok((zkey_bytes, graph_bytes))
530    }
531
532    /// Serializes and writes the material into `.zkey` and graph writers.
533    pub fn to_writer(
534        self,
535        mut zkey_writer: impl std::io::Write,
536        mut graph_writer: impl std::io::Write,
537    ) -> Result<(), MaterialSerializationError> {
538        self.material
539            .zkey
540            .serialize_with_mode(&mut zkey_writer, self.compress)
541            .map_err(MaterialSerializationError::ZkeySerialization)?;
542        postcard::to_io(
543            &(
544                &self.material.graph.nodes,
545                &self.material.graph.signals,
546                &self.material.graph.input_mapping,
547            ),
548            &mut graph_writer,
549        )
550        .map_err(MaterialSerializationError::GraphSerialization)?;
551        Ok(())
552    }
553
554    /// Serializes and writes the material into `.zkey` and graph files.
555    pub fn to_paths(
556        self,
557        zkey_path: impl AsRef<Path>,
558        graph_path: impl AsRef<Path>,
559    ) -> Result<(), MaterialSerializationError> {
560        let zkey_file = std::fs::File::create(zkey_path)?;
561        let graph_file = std::fs::File::create(graph_path)?;
562        let mut zkey_writer = std::io::BufWriter::new(zkey_file);
563        let mut graph_writer = std::io::BufWriter::new(graph_file);
564        self.to_writer(&mut zkey_writer, &mut graph_writer)?;
565        zkey_writer.flush()?;
566        graph_writer.flush()?;
567        Ok(())
568    }
569}