Skip to main content

curvy_prover/
lib.rs

1#![doc = include_str!("../README.md")]
2//!
3//! ## Security model
4//!
5//! The proving key parser is the committed `rs-core` implementation vendored
6//! from `ark-circom` without its Wasmer witness calculator. Bulk query points are
7//! constructed unchecked for fast startup, so every caller must provide a pinned
8//! SHA-256 digest for the zkey before parsing is allowed.
9//!
10//! Native builds enable `std` and Rayon-backed `parallel` support by default.
11//! Portable WASM uses the `wasm` feature; threaded browser builds use
12//! `wasm-threads` and export `initThreadPool(n)` so the host selects the worker
13//! count explicitly.
14
15pub mod qap;
16pub mod wtns;
17pub mod zkey;
18
19use std::io::Cursor;
20
21use ark_bn254::{Bn254, Fq, Fq2, Fr};
22use ark_ff::{BigInteger, PrimeField, UniformRand};
23use ark_groth16::{Groth16, PreparedVerifyingKey, Proof, ProvingKey, prepare_verifying_key};
24use ark_relations::gr1cs::SynthesisError;
25use ark_serialize::SerializationError;
26use curvy_witness::{WitnessError, WitnessGraph};
27use num_bigint::BigUint;
28use sha2::{Digest, Sha256};
29use thiserror::Error;
30
31use qap::CircomReduction;
32use wtns::WtnsError;
33use zkey::ZkeyMatrices;
34
35#[derive(Debug, Error)]
36pub enum ProverError {
37    #[error("expected zkey SHA-256 must be exactly 64 hexadecimal characters")]
38    InvalidExpectedHash,
39    #[error("zkey SHA-256 mismatch: expected {expected}, got {actual}")]
40    ZkeyHashMismatch { expected: String, actual: String },
41    #[error("invalid zkey: {0}")]
42    InvalidZkey(SerializationError),
43    #[error(transparent)]
44    InvalidWitness(#[from] WtnsError),
45    #[error(transparent)]
46    InvalidWitnessGraph(#[from] WitnessError),
47    #[error("witness assignment length mismatch: expected {expected}, got {actual}")]
48    AssignmentLength { expected: usize, actual: usize },
49    #[error("Groth16 proof generation failed: {0}")]
50    ProofGeneration(SynthesisError),
51    #[error("Groth16 verification failed: {0}")]
52    Verification(SynthesisError),
53    #[error("generated Groth16 proof did not verify")]
54    SelfVerificationFailed,
55}
56
57/// Parsed, reusable proving key and constraint matrices for one circuit.
58pub struct Prover {
59    pk: ProvingKey<Bn254>,
60    matrices: ZkeyMatrices<Fr>,
61    pvk: PreparedVerifyingKey<Bn254>,
62    assignment_size: usize,
63}
64
65impl Prover {
66    /// Authenticate and parse one zkey. Hash verification happens before the
67    /// unchecked point parser sees any artifact-controlled curve coordinates.
68    pub fn from_zkey_bytes(bytes: &[u8], expected_sha256: &str) -> Result<Self, ProverError> {
69        verify_sha256(bytes, expected_sha256)?;
70        let mut cursor = Cursor::new(bytes);
71        let (pk, matrices) = zkey::read_zkey(&mut cursor).map_err(ProverError::InvalidZkey)?;
72        let pvk = prepare_verifying_key(&pk.vk);
73        let assignment_size = pk.a_query.len();
74        Ok(Self {
75            pk,
76            matrices,
77            pvk,
78            assignment_size,
79        })
80    }
81
82    pub fn num_constraints(&self) -> usize {
83        self.matrices.num_constraints
84    }
85
86    pub fn num_public(&self) -> usize {
87        self.matrices.num_instance_variables.saturating_sub(1)
88    }
89
90    pub fn prove(&self, full_assignment: &[Fr]) -> Result<Proof<Bn254>, ProverError> {
91        self.validate_assignment(full_assignment)?;
92        let mut rng = ark_std::rand::rngs::OsRng;
93        let r = Fr::rand(&mut rng);
94        let s = Fr::rand(&mut rng);
95        Groth16::<Bn254, CircomReduction>::create_proof_with_reduction_and_matrices(
96            &self.pk,
97            r,
98            s,
99            &self.matrices.matrices,
100            self.matrices.num_instance_variables,
101            self.matrices.num_constraints,
102            full_assignment,
103        )
104        .map_err(ProverError::ProofGeneration)
105    }
106
107    pub fn public_inputs<'a>(&self, full_assignment: &'a [Fr]) -> Result<&'a [Fr], ProverError> {
108        self.validate_assignment(full_assignment)?;
109        Ok(&full_assignment[1..self.matrices.num_instance_variables])
110    }
111
112    pub fn verify(&self, proof: &Proof<Bn254>, public_inputs: &[Fr]) -> Result<bool, ProverError> {
113        Groth16::<Bn254>::verify_proof(&self.pvk, proof, public_inputs)
114            .map_err(ProverError::Verification)
115    }
116
117    /// Decode, prove, and self-verify one snarkjs witness before returning it.
118    pub fn prove_wtns(&self, bytes: &[u8]) -> Result<ProofBundle, ProverError> {
119        let assignment = wtns::read_wtns(bytes)?;
120        self.prove_assignment(&assignment)
121    }
122
123    /// Prove and self-verify one direct arkworks witness assignment.
124    pub fn prove_assignment(&self, assignment: &[Fr]) -> Result<ProofBundle, ProverError> {
125        let proof = self.prove(assignment)?;
126        let public_inputs = self.public_inputs(assignment)?;
127        if !self.verify(&proof, public_inputs)? {
128            return Err(ProverError::SelfVerificationFailed);
129        }
130        Ok(ProofBundle {
131            proof_json: proof_to_snarkjs_json(&proof),
132            public_signals_json: publics_to_json(public_inputs),
133        })
134    }
135
136    fn validate_assignment(&self, full_assignment: &[Fr]) -> Result<(), ProverError> {
137        self.validate_assignment_size(full_assignment.len())
138    }
139
140    fn validate_assignment_size(&self, actual: usize) -> Result<(), ProverError> {
141        if actual != self.assignment_size {
142            return Err(ProverError::AssignmentLength {
143                expected: self.assignment_size,
144                actual,
145            });
146        }
147        Ok(())
148    }
149}
150
151/// Authenticated witness graph and proving key for one immutable circuit bundle.
152pub struct CircuitProver {
153    prover: Prover,
154    witness_graph: WitnessGraph,
155}
156
157impl CircuitProver {
158    pub fn from_artifacts(
159        zkey: &[u8],
160        expected_zkey_sha256: &str,
161        witness_graph: &[u8],
162        expected_graph_sha256: &str,
163    ) -> Result<Self, ProverError> {
164        let prover = Prover::from_zkey_bytes(zkey, expected_zkey_sha256)?;
165        let witness_graph = WitnessGraph::from_bytes(witness_graph, expected_graph_sha256)?;
166        prover.validate_assignment_size(witness_graph.assignment_size())?;
167        Ok(Self {
168            prover,
169            witness_graph,
170        })
171    }
172
173    pub fn num_constraints(&self) -> usize {
174        self.prover.num_constraints()
175    }
176
177    pub fn num_public(&self) -> usize {
178        self.prover.num_public()
179    }
180
181    pub fn r1cs_sha256(&self) -> [u8; 32] {
182        self.witness_graph.r1cs_sha256()
183    }
184
185    /// Evaluate authenticated `curvy-graph-v1` inputs without proving yet.
186    ///
187    /// This split is useful to native operators that report witness and proof
188    /// timings separately. Most callers should use [`Self::prove_json`].
189    pub fn calculate_witness_json(&self, input_json: &str) -> Result<Vec<Fr>, ProverError> {
190        Ok(self.witness_graph.calculate_json(input_json)?)
191    }
192
193    /// Prove and self-verify an assignment produced by this circuit's graph.
194    pub fn prove_assignment(&self, assignment: &[Fr]) -> Result<ProofBundle, ProverError> {
195        self.prover.prove_assignment(assignment)
196    }
197
198    pub fn prove_json(&self, input_json: &str) -> Result<ProofBundle, ProverError> {
199        let assignment = self.calculate_witness_json(input_json)?;
200        self.prove_assignment(&assignment)
201    }
202}
203
204pub struct ProofBundle {
205    pub proof_json: String,
206    pub public_signals_json: String,
207}
208
209fn verify_sha256(bytes: &[u8], expected_sha256: &str) -> Result<(), ProverError> {
210    if expected_sha256.len() != 64 || !expected_sha256.bytes().all(|byte| byte.is_ascii_hexdigit())
211    {
212        return Err(ProverError::InvalidExpectedHash);
213    }
214    let expected = expected_sha256.to_ascii_lowercase();
215    let actual = Sha256::digest(bytes)
216        .iter()
217        .map(|byte| format!("{byte:02x}"))
218        .collect::<String>();
219    if actual != expected {
220        return Err(ProverError::ZkeyHashMismatch { expected, actual });
221    }
222    Ok(())
223}
224
225fn fq_dec(value: &Fq) -> String {
226    BigUint::from_bytes_be(&value.into_bigint().to_bytes_be()).to_str_radix(10)
227}
228
229fn fr_dec(value: &Fr) -> String {
230    BigUint::from_bytes_be(&value.into_bigint().to_bytes_be()).to_str_radix(10)
231}
232
233fn fq2_json(value: &Fq2) -> String {
234    format!("[\"{}\",\"{}\"]", fq_dec(&value.c0), fq_dec(&value.c1))
235}
236
237/// Serialize a proof with the same coordinate order and shape as snarkjs.
238pub fn proof_to_snarkjs_json(proof: &Proof<Bn254>) -> String {
239    format!(
240        "{{\"pi_a\":[\"{}\",\"{}\",\"1\"],\"pi_b\":[{}, {},[\"1\",\"0\"]],\"pi_c\":[\"{}\",\"{}\",\"1\"],\"protocol\":\"groth16\",\"curve\":\"bn128\"}}",
241        fq_dec(&proof.a.x),
242        fq_dec(&proof.a.y),
243        fq2_json(&proof.b.x),
244        fq2_json(&proof.b.y),
245        fq_dec(&proof.c.x),
246        fq_dec(&proof.c.y),
247    )
248}
249
250pub fn publics_to_json(publics: &[Fr]) -> String {
251    let items = publics
252        .iter()
253        .map(|public| format!("\"{}\"", fr_dec(public)))
254        .collect::<Vec<_>>();
255    format!("[{}]", items.join(","))
256}
257
258#[cfg(feature = "wasm-threads")]
259pub use wasm_bindgen_rayon::init_thread_pool;
260
261#[cfg(feature = "wasm")]
262mod wasm_api {
263    use wasm_bindgen::prelude::*;
264
265    #[wasm_bindgen]
266    pub struct WasmCircuitProver(crate::CircuitProver);
267
268    #[wasm_bindgen]
269    impl WasmCircuitProver {
270        #[wasm_bindgen(constructor)]
271        pub fn new(
272            zkey: &[u8],
273            expected_zkey_sha256: &str,
274            witness_graph: &[u8],
275            expected_graph_sha256: &str,
276        ) -> Result<WasmCircuitProver, JsError> {
277            crate::CircuitProver::from_artifacts(
278                zkey,
279                expected_zkey_sha256,
280                witness_graph,
281                expected_graph_sha256,
282            )
283            .map(WasmCircuitProver)
284            .map_err(|error| JsError::new(&error.to_string()))
285        }
286
287        #[wasm_bindgen(getter, js_name = numConstraints)]
288        pub fn num_constraints(&self) -> usize {
289            self.0.num_constraints()
290        }
291
292        #[wasm_bindgen(getter, js_name = numPublic)]
293        pub fn num_public(&self) -> usize {
294            self.0.num_public()
295        }
296
297        /// Calculate, prove, and self-verify directly from circuit input JSON.
298        pub fn prove(&self, input_json: &str) -> Result<String, JsError> {
299            self.0
300                .prove_json(input_json)
301                .map(bundle_json)
302                .map_err(|error| JsError::new(&error.to_string()))
303        }
304    }
305
306    #[wasm_bindgen]
307    pub struct WasmProver(crate::Prover);
308
309    #[wasm_bindgen]
310    impl WasmProver {
311        #[wasm_bindgen(constructor)]
312        pub fn new(zkey: &[u8], expected_sha256: &str) -> Result<WasmProver, JsError> {
313            crate::Prover::from_zkey_bytes(zkey, expected_sha256)
314                .map(WasmProver)
315                .map_err(|error| JsError::new(&error.to_string()))
316        }
317
318        #[wasm_bindgen(getter, js_name = numConstraints)]
319        pub fn num_constraints(&self) -> usize {
320            self.0.num_constraints()
321        }
322
323        #[wasm_bindgen(getter, js_name = numPublic)]
324        pub fn num_public(&self) -> usize {
325            self.0.num_public()
326        }
327
328        /// Return `{"proof": ..., "publicSignals": [...]}` in snarkjs shape.
329        pub fn prove(&self, wtns: &[u8]) -> Result<String, JsError> {
330            self.0
331                .prove_wtns(wtns)
332                .map(bundle_json)
333                .map_err(|error| JsError::new(&error.to_string()))
334        }
335    }
336
337    fn bundle_json(bundle: crate::ProofBundle) -> String {
338        format!(
339            "{{\"proof\":{},\"publicSignals\":{}}}",
340            bundle.proof_json, bundle.public_signals_json
341        )
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use sha2::{Digest, Sha256};
348
349    use super::{Prover, ProverError, verify_sha256};
350
351    #[test]
352    fn rejects_an_untrusted_zkey_before_parsing() {
353        let error = verify_sha256(b"not a zkey", &"00".repeat(32)).expect_err("hash must mismatch");
354        assert!(matches!(error, ProverError::ZkeyHashMismatch { .. }));
355    }
356
357    #[test]
358    fn rejects_a_malformed_expected_hash() {
359        assert!(matches!(
360            verify_sha256(b"anything", "not-a-digest"),
361            Err(ProverError::InvalidExpectedHash)
362        ));
363    }
364
365    #[test]
366    fn rejects_malformed_zkey_after_its_digest_matches() {
367        let bytes = b"not a zkey";
368        let digest = Sha256::digest(bytes)
369            .iter()
370            .map(|byte| format!("{byte:02x}"))
371            .collect::<String>();
372        let error = Prover::from_zkey_bytes(bytes, &digest)
373            .err()
374            .expect("zkey parser must reject junk");
375        assert!(matches!(error, ProverError::InvalidZkey(_)));
376    }
377}