Sitelet https://github.com/zkmopro/mopro/commit/27e144202292fa11f47a7ce2ab29e26cc2641b5a
Skip to content

Commit 27e1442

Browse files
committed
Simplify Garaga calldata API to CircomProofResult and harden BN254 checks.
Validate public-input count field bounds (aicoderabbit suggestion) and fix rustfmt/clippy warnings.
1 parent cf652c3 commit 27e1442

12 files changed

Lines changed: 281 additions & 230 deletions

File tree

‎cli/src/main.rs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ enum Commands {
4949
architectures: Option<Vec<String>>,
5050
#[arg(
5151
long,
52-
num_args = 1..,
5352
help = "Automatically run mopro update after build",
5453
conflicts_with = "no_auto_update"
5554
)]

‎cli/src/template/circom/lib.rs‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ pub use circom::{
77
generate_circom_proof, verify_circom_proof, CircomProof, CircomProofResult, ProofLib, G1, G2,
88
};
99
#[cfg(feature = "garaga")]
10-
pub use circom::{
11-
generate_circom_groth16_garaga_calldata,
12-
generate_circom_groth16_garaga_calldata_from_proof_result,
13-
};
10+
pub use circom::generate_circom_groth16_garaga_calldata;
1411

1512
mod witness {
1613
rust_witness::witness!(multiplier2);

‎cli/src/template/init/src/circom.rs‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ mod garaga_convert;
66
mod snarkjs_types;
77

88
#[cfg(feature = "garaga")]
9-
pub use circom_garaga::{
10-
generate_circom_groth16_garaga_calldata,
11-
generate_circom_groth16_garaga_calldata_from_proof_result,
12-
};
9+
pub use circom_garaga::generate_circom_groth16_garaga_calldata;
1310

1411
use crate::MoproError;
1512
use circom_prover::{

‎cli/src/template/init/src/circom/circom_garaga.rs‎

Lines changed: 24 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
use super::garaga_convert::{to_groth16_proof, to_groth16_proof_from_mopro, to_groth16_vk};
2-
use super::snarkjs_types::{
3-
parse_snarkjs_proof_json, parse_snarkjs_public_json, parse_snarkjs_vk_json, GROTH16_PROTOCOL,
4-
SNARKJS_BN128_CURVE,
5-
};
1+
use super::garaga_convert::{to_groth16_proof_from_mopro, to_groth16_vk};
2+
use super::snarkjs_types::{parse_snarkjs_vk_json, GROTH16_PROTOCOL, SNARKJS_BN128_CURVE};
63
use super::{CircomProof, CircomProofResult};
74
use crate::MoproError;
85
use garaga_rs::calldata::full_proof_with_hints::groth16::{
@@ -20,6 +17,20 @@ fn garaga_calldata_core(
2017
Ok(felts.into_iter().map(|f| f.to_string()).collect())
2118
}
2219

20+
fn validate_public_input_count(
21+
public_inputs: &[String],
22+
n_public: usize,
23+
) -> Result<(), MoproError> {
24+
if public_inputs.len() != n_public {
25+
return Err(MoproError::CircomError(format!(
26+
"Garaga calldata error: public input count mismatch: got {}, nPublic {}",
27+
public_inputs.len(),
28+
n_public
29+
)));
30+
}
31+
Ok(())
32+
}
33+
2334
fn validate_mopro_groth16_bn254(proof: &CircomProof) -> Result<(), String> {
2435
if proof.protocol != GROTH16_PROTOCOL {
2536
return Err(format!(
@@ -53,51 +64,25 @@ fn circom_proof_to_groth16(
5364
)
5465
}
5566

56-
/// Build Starknet-compatible Groth16 calldata (BN254) from SnarkJS JSON artifacts.
57-
///
58-
/// `proof_json` — contents of snarkjs `proof.json`
59-
/// `public_json` — contents of snarkjs `public.json` (array of decimal field elements)
60-
/// `verification_key_json` — contents of snarkjs `verification_key.json`
61-
///
62-
/// Returns each Starknet felt as a decimal string, suitable for Flutter/Dart and
63-
/// `starknet.dart` invoke calldata. Does not send transactions.
64-
///
65-
/// Prefer [`generate_circom_groth16_garaga_calldata_from_proof_result`] when you already
66-
/// have output from [`super::generate_circom_proof`].
67-
#[cfg_attr(feature = "uniffi", uniffi::export)]
68-
pub fn generate_circom_groth16_garaga_calldata(
69-
proof_json: String,
70-
public_json: String,
71-
verification_key_json: String,
72-
) -> Result<Vec<String>, MoproError> {
73-
let proof = parse_snarkjs_proof_json(&proof_json)
74-
.map_err(|e| MoproError::CircomError(format!("Garaga calldata error: {e}")))?;
75-
let public_inputs = parse_snarkjs_public_json(&public_json)
76-
.map_err(|e| MoproError::CircomError(format!("Garaga calldata error: {e}")))?;
77-
let vk = parse_snarkjs_vk_json(&verification_key_json)
78-
.map_err(|e| MoproError::CircomError(format!("Garaga calldata error: {e}")))?;
79-
80-
let garaga_proof = to_groth16_proof(&proof, &public_inputs)
81-
.map_err(|e| MoproError::CircomError(format!("Garaga calldata error: {e}")))?;
82-
let garaga_vk = to_groth16_vk(&vk)
83-
.map_err(|e| MoproError::CircomError(format!("Garaga calldata error: {e}")))?;
84-
85-
garaga_calldata_core(garaga_proof, garaga_vk)
86-
}
87-
8867
/// Build Starknet-compatible Groth16 calldata (BN254) from a [`CircomProofResult`].
8968
///
69+
/// Prove with [`super::generate_circom_proof`], then pass the result here together with
70+
/// `verification_key_json` (contents of snarkjs `verification_key.json` from a one-time
71+
/// zkey export).
72+
///
9073
/// `proof_result.inputs` has the same content as SnarkJS `public.json` (decimal field
9174
/// elements). No separate public-inputs file is required.
9275
///
93-
/// `verification_key_json` — contents of snarkjs `verification_key.json` (one-time zkey export).
76+
/// Returns each Starknet felt as a decimal string, suitable for Flutter/Dart and
77+
/// `starknet.dart` invoke calldata. Does not send transactions.
9478
#[cfg_attr(feature = "uniffi", uniffi::export)]
95-
pub fn generate_circom_groth16_garaga_calldata_from_proof_result(
79+
pub fn generate_circom_groth16_garaga_calldata(
9680
proof_result: CircomProofResult,
9781
verification_key_json: String,
9882
) -> Result<Vec<String>, MoproError> {
9983
let vk = parse_snarkjs_vk_json(&verification_key_json)
10084
.map_err(|e| MoproError::CircomError(format!("Garaga calldata error: {e}")))?;
85+
validate_public_input_count(&proof_result.inputs, vk.n_public)?;
10186

10287
let garaga_proof = circom_proof_to_groth16(&proof_result.proof, &proof_result.inputs)
10388
.map_err(|e| MoproError::CircomError(format!("Garaga calldata error: {e}")))?;

‎cli/src/template/init/src/circom/garaga_convert.rs‎

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,40 @@
1-
use super::snarkjs_types::{SnarkJsProof, SnarkJsVerificationKey};
1+
use super::snarkjs_types::SnarkJsVerificationKey;
22
use garaga_rs::calldata::full_proof_with_hints::groth16::{Groth16Proof, Groth16VerificationKey};
33
use garaga_rs::calldata::{G1PointBigUint, G2PointBigUint};
4+
use garaga_rs::definitions::{get_modulus_from_curve_id, CurveID};
45
use num_bigint::BigUint;
56
use std::str::FromStr;
7+
use std::sync::LazyLock;
68

9+
/// BN254 base-field modulus (Fq). Coordinates must be in `[0, p)`.
10+
static BN254_BASE_FIELD: LazyLock<BigUint> =
11+
LazyLock::new(|| get_modulus_from_curve_id(CurveID::BN254));
12+
13+
/// BN254 scalar-field order (Fr). Public inputs must be in `[0, n)`.
14+
/// Same value as `BN254PrimeField::get_curve_params().n` in garaga_rs.
15+
static BN254_SCALAR_FIELD: LazyLock<BigUint> = LazyLock::new(|| {
16+
BigUint::parse_bytes(
17+
b"30644E72E131A029B85045B68181585D2833E84879B9709143E1F593F0000001",
18+
16,
19+
)
20+
.expect("valid BN254 scalar-field hex")
21+
});
22+
23+
/// Parse a decimal field element and require it to lie in the BN254 base field (Fq).
724
pub(crate) fn parse_biguint(s: &str) -> Result<BigUint, String> {
8-
BigUint::from_str(s).map_err(|e| format!("invalid coordinate '{s}': {e}"))
25+
let value = BigUint::from_str(s).map_err(|e| format!("invalid coordinate '{s}': {e}"))?;
26+
if value >= *BN254_BASE_FIELD {
27+
return Err(format!("coordinate '{s}' is outside BN254 base field"));
28+
}
29+
Ok(value)
30+
}
31+
32+
fn parse_public_input_biguint(s: &str) -> Result<BigUint, String> {
33+
let value = BigUint::from_str(s).map_err(|e| format!("invalid coordinate '{s}': {e}"))?;
34+
if value >= *BN254_SCALAR_FIELD {
35+
return Err(format!("public input '{s}' is outside BN254 scalar field"));
36+
}
37+
Ok(value)
938
}
1039

1140
pub(crate) fn snarkjs_g1_to_garaga(coords: &[String]) -> Result<G1PointBigUint, String> {
@@ -32,19 +61,13 @@ pub(crate) fn snarkjs_g2_to_garaga(rows: &[Vec<String>]) -> Result<G2PointBigUin
3261
}
3362

3463
pub(crate) fn public_inputs_to_biguint(public_inputs: &[String]) -> Result<Vec<BigUint>, String> {
35-
if public_inputs.is_empty() {
36-
return Err("public inputs must not be empty".to_string());
37-
}
3864
public_inputs
3965
.iter()
40-
.map(|s| parse_biguint(s))
66+
.map(|s| parse_public_input_biguint(s))
4167
.collect()
4268
}
4369

44-
pub(crate) fn mopro_g2_to_garaga(
45-
x: &[String],
46-
y: &[String],
47-
) -> Result<G2PointBigUint, String> {
70+
pub(crate) fn mopro_g2_to_garaga(x: &[String], y: &[String]) -> Result<G2PointBigUint, String> {
4871
if x.len() < 2 || y.len() < 2 {
4972
return Err("G2 point must have two x and two y coordinates".to_string());
5073
}
@@ -75,21 +98,6 @@ pub(crate) fn to_groth16_proof_from_mopro(
7598
})
7699
}
77100

78-
pub(crate) fn to_groth16_proof(
79-
proof: &SnarkJsProof,
80-
public_inputs: &[String],
81-
) -> Result<Groth16Proof, String> {
82-
to_groth16_proof_from_mopro(
83-
&proof.pi_a[0],
84-
&proof.pi_a[1],
85-
&proof.pi_b[0],
86-
&proof.pi_b[1],
87-
&proof.pi_c[0],
88-
&proof.pi_c[1],
89-
public_inputs,
90-
)
91-
}
92-
93101
pub(crate) fn to_groth16_vk(vk: &SnarkJsVerificationKey) -> Result<Groth16VerificationKey, String> {
94102
let mut values: Vec<BigUint> = Vec::new();
95103
values.extend(snarkjs_g1_to_garaga(&vk.vk_alpha_1)?.flatten());

‎cli/src/template/init/src/circom/snarkjs_types.rs‎

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,5 @@
11
use serde::Deserialize;
22

3-
#[derive(Debug, Clone, Deserialize)]
4-
pub(crate) struct SnarkJsProof {
5-
pub pi_a: Vec<String>,
6-
pub pi_b: Vec<Vec<String>>,
7-
pub pi_c: Vec<String>,
8-
pub protocol: String,
9-
pub curve: String,
10-
}
11-
123
#[derive(Debug, Clone, Deserialize)]
134
pub(crate) struct SnarkJsVerificationKey {
145
pub protocol: String,
@@ -26,51 +17,13 @@ pub(crate) struct SnarkJsVerificationKey {
2617
pub(crate) const SNARKJS_BN128_CURVE: &str = "bn128";
2718
pub(crate) const GROTH16_PROTOCOL: &str = "groth16";
2819

29-
pub(crate) fn parse_snarkjs_proof_json(s: &str) -> Result<SnarkJsProof, String> {
30-
let proof: SnarkJsProof =
31-
serde_json::from_str(s).map_err(|e| format!("invalid proof JSON: {e}"))?;
32-
validate_proof(&proof)?;
33-
Ok(proof)
34-
}
35-
36-
pub(crate) fn parse_snarkjs_public_json(s: &str) -> Result<Vec<String>, String> {
37-
let inputs: Vec<String> =
38-
serde_json::from_str(s).map_err(|e| format!("invalid public inputs JSON: {e}"))?;
39-
if inputs.is_empty() {
40-
return Err("public inputs must not be empty".to_string());
41-
}
42-
Ok(inputs)
43-
}
44-
4520
pub(crate) fn parse_snarkjs_vk_json(s: &str) -> Result<SnarkJsVerificationKey, String> {
4621
let vk: SnarkJsVerificationKey =
4722
serde_json::from_str(s).map_err(|e| format!("invalid verification key JSON: {e}"))?;
4823
validate_vk(&vk)?;
4924
Ok(vk)
5025
}
5126

52-
fn validate_proof(proof: &SnarkJsProof) -> Result<(), String> {
53-
if proof.protocol != GROTH16_PROTOCOL {
54-
return Err(format!(
55-
"unsupported proof protocol: {} (expected {GROTH16_PROTOCOL})",
56-
proof.protocol
57-
));
58-
}
59-
if proof.curve != SNARKJS_BN128_CURVE {
60-
return Err(format!(
61-
"unsupported curve: {} (BN254/{SNARKJS_BN128_CURVE} only in v1)",
62-
proof.curve
63-
));
64-
}
65-
if proof.pi_a.len() < 2 || proof.pi_c.len() < 2 {
66-
return Err("invalid G1 point in proof".to_string());
67-
}
68-
if proof.pi_b.len() < 2 || proof.pi_b[0].len() < 2 || proof.pi_b[1].len() < 2 {
69-
return Err("invalid G2 point in proof".to_string());
70-
}
71-
Ok(())
72-
}
73-
7427
fn validate_vk(vk: &SnarkJsVerificationKey) -> Result<(), String> {
7528
if vk.protocol != GROTH16_PROTOCOL {
7629
return Err(format!(

‎cli/src/template/init/test-vectors/circom/garaga/bn254/README.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# BN254 Garaga calldata golden fixtures
22

3-
SnarkJS-compatible artifacts used to verify `generate_circom_groth16_garaga_calldata`
3+
Artifacts used to verify `generate_circom_groth16_garaga_calldata`
44
against [Garaga v1.1.0](https://github.com/keep-starknet-strange/garaga/tree/v1.1.0)
55
`get_groth16_calldata_felt`.
66

7-
| File | Source |
8-
|------|--------|
9-
| `proof.json` | Garaga `snarkjs_proof_bn254.json` example |
10-
| `public.json` | Garaga `snarkjs_public_bn254.json` example (SnarkJS JSON import path only) |
11-
| `verification_key.json` | Garaga `snarkjs_vk_bn254.json` example |
7+
| File | Role |
8+
|------|------|
9+
| `proof.json` | Test fixture only — builds a `CircomProofResult` for golden tests (not a runtime API input) |
10+
| `public.json` | Test fixture only — public inputs for the fixture `CircomProofResult` |
11+
| `verification_key.json` | Required at runtime (one-time snarkjs zkey export) |
1212
| `expected_garaga_calldata.json` | Generated via `cargo run -p garaga-calldata-tests --bin gen-garaga-calldata-fixture` |
1313

1414
Regenerate `expected_garaga_calldata.json` after changing parsers or bumping Garaga:

‎docs/docs/adapters/circom.md‎

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -356,11 +356,11 @@ Export the verification key once from your zkey (same format snarkjs uses):
356356
snarkjs zkey export verificationkey circuit_final.zkey verification_key.json
357357
```
358358

359-
### `generateCircomGroth16GaragaCalldataFromProofResult` (recommended)
359+
### `generateCircomGroth16GaragaCalldata`
360360

361361
Prove in-app with Mopro, then build Garaga calldata from the result. `CircomProofResult.inputs`
362362
is a `Vec<String>` with the same content as SnarkJS `public.json` — no separate public-inputs
363-
file is required.
363+
file is required. The only required file input is `verification_key.json`.
364364

365365
```rust
366366
let result = generate_circom_proof(
@@ -369,7 +369,7 @@ let result = generate_circom_proof(
369369
ProofLib::Arkworks,
370370
)?;
371371

372-
let calldata = generate_circom_groth16_garaga_calldata_from_proof_result(
372+
let calldata = generate_circom_groth16_garaga_calldata(
373373
result,
374374
std::fs::read_to_string("verification_key.json")?,
375375
)?;
@@ -379,26 +379,7 @@ let calldata = generate_circom_groth16_garaga_calldata_from_proof_result(
379379
On mobile bindings, pass the `CircomProofResult` returned by `generateCircomProof` directly —
380380
`inputs` is already available as a string list.
381381

382-
### `generateCircomGroth16GaragaCalldata` (SnarkJS JSON import)
383-
384-
For proofs produced outside Mopro (e.g. via snarkjs), pass SnarkJS JSON artifacts:
385-
386-
```sh
387-
snarkjs groth16 prove circuit_final.zkey witness.wtns proof.json public.json
388-
```
389-
390-
```rust
391-
let calldata = generate_circom_groth16_garaga_calldata(
392-
std::fs::read_to_string("proof.json")?,
393-
std::fs::read_to_string("public.json")?,
394-
std::fs::read_to_string("verification_key.json")?,
395-
)?;
396-
```
397-
398-
If you already have public inputs as a `Vec<String>`, serialize them instead of reading
399-
`public.json`: `serde_json::to_string(&inputs)`.
400-
401-
**BN254 only** (`curve: "bn128"` in SnarkJS JSON). The on-chain verifier contract must be generated with Garaga v1.1.0 (`garaga gen`).
382+
**BN254 only** (`curve: "bn128"` / `"bn254"`). The on-chain verifier contract must be generated with Garaga v1.1.0 (`garaga gen`).
402383

403384
### Flutter / `starknet.dart`
404385

@@ -411,7 +392,7 @@ final proofResult = await generateCircomProof(
411392
proofLib: ProofLib.arkworks,
412393
);
413394
414-
final calldata = await generateCircomGroth16GaragaCalldataFromProofResult(
395+
final calldata = await generateCircomGroth16GaragaCalldata(
415396
proofResult: proofResult,
416397
verificationKeyJson: vkJson,
417398
);

‎garaga-calldata-tests/build.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
fn main() {
2+
// Template sources use `cfg(feature = "uniffi")` but this crate has no such
3+
// Cargo feature (a real `uniffi = []` would break `--all-features`). Tell
4+
// rustc the value is expected so check-cfg stays quiet.
5+
println!("cargo::rustc-check-cfg=cfg(feature, values(\"uniffi\"))");
6+
}

0 commit comments

Comments
 (0)