diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 8196cd7f4..4ae403b43 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -120,12 +120,18 @@ jobs: fail-fast: false matrix: os: [macos-latest, ubuntu-latest] - adapter: [circom, halo2, noir, none] + adapter: [circom, halo2, noir, gnark, none] runs-on: ${{ matrix.os }} if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - uses: actions/checkout@v6 + - name: Setup Go (for gnark adapter) + if: matrix.adapter == 'gnark' + uses: actions/setup-go@v5 + with: + go-version: "1.24" + - name: Prepare template project uses: ./.github/actions/mopro-init-project with: @@ -146,12 +152,18 @@ jobs: strategy: fail-fast: false matrix: - adapter: [circom, halo2, noir] + adapter: [circom, halo2, noir, gnark] runs-on: macos-latest if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - uses: actions/checkout@v6 + - name: Setup Go (for gnark adapter) + if: matrix.adapter == 'gnark' + uses: actions/setup-go@v5 + with: + go-version: "1.24" + - name: Prepare template project uses: ./.github/actions/mopro-init-project with: @@ -196,7 +208,7 @@ jobs: strategy: fail-fast: false matrix: - adapter: [circom, halo2, noir] + adapter: [circom, halo2, noir, gnark] runs-on: macos-latest if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: @@ -247,13 +259,19 @@ jobs: strategy: fail-fast: false matrix: - adapter: [circom, halo2, noir] + adapter: [circom, halo2, noir, gnark] runs-on: ubuntu-latest timeout-minutes: 15 if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - uses: actions/checkout@v6 + - name: Setup Go (for gnark adapter) + if: matrix.adapter == 'gnark' + uses: actions/setup-go@v5 + with: + go-version: "1.24" + - name: Android SDK uses: android-actions/setup-android@v3 @@ -318,12 +336,18 @@ jobs: strategy: fail-fast: false matrix: - adapter: [circom, halo2, noir] + adapter: [circom, halo2, noir, gnark] runs-on: ubuntu-latest if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - uses: actions/checkout@v6 + - name: Setup Go (for gnark adapter) + if: matrix.adapter == 'gnark' + uses: actions/setup-go@v5 + with: + go-version: "1.24" + - uses: subosito/flutter-action@v2 with: flutter-version: "3.35.4" @@ -361,12 +385,18 @@ jobs: strategy: fail-fast: false matrix: - adapter: [circom, halo2, noir] + adapter: [circom, halo2, noir, gnark] runs-on: macos-latest if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo_full_name steps: - uses: actions/checkout@v6 + - name: Setup Go (for gnark adapter) + if: matrix.adapter == 'gnark' + uses: actions/setup-go@v5 + with: + go-version: "1.24" + # Build mopro example project for iOS + Android first, and create RN frameworks - name: Prepare template project uses: ./.github/actions/mopro-init-project diff --git a/cli/src/init.rs b/cli/src/init.rs index aaa392c05..d608c84b1 100644 --- a/cli/src/init.rs +++ b/cli/src/init.rs @@ -11,6 +11,7 @@ use std::{env, fs, io::Write, path::Path}; pub mod adapter; mod circom; +mod gnark; mod halo2; mod noir; mod proving_system; diff --git a/cli/src/init/adapter.rs b/cli/src/init/adapter.rs index 7ed5eedf7..b10e04405 100644 --- a/cli/src/init/adapter.rs +++ b/cli/src/init/adapter.rs @@ -1,4 +1,4 @@ -use super::{circom::Circom, halo2::Halo2, noir::Noir}; +use super::{circom::Circom, gnark::Gnark, halo2::Halo2, noir::Noir}; use crate::init::proving_system::{replace_test_bindings_lib_import, ProvingSystem}; use crate::{select::multi_select, style}; @@ -7,13 +7,15 @@ pub enum Adapter { Circom, Halo2, Noir, + Gnark, NoneOfTheAbove, } -pub(super) const ADAPTERS: [Adapter; 4] = [ +pub(super) const ADAPTERS: [Adapter; 5] = [ Adapter::Circom, Adapter::Halo2, Adapter::Noir, + Adapter::Gnark, Adapter::NoneOfTheAbove, ]; @@ -23,6 +25,7 @@ impl Adapter { Adapter::Circom => "circom", Adapter::Halo2 => "halo2", Adapter::Noir => "noir", + Adapter::Gnark => "gnark", Adapter::NoneOfTheAbove => "none of the above", } } @@ -82,6 +85,9 @@ impl AdapterSelector { if self.contains(Adapter::Noir) { Noir::dep_template(cargo_toml_path)?; } + if self.contains(Adapter::Gnark) { + Gnark::dep_template(cargo_toml_path)?; + } Ok(()) } @@ -95,6 +101,9 @@ impl AdapterSelector { if self.contains(Adapter::Noir) { Noir::build_dep_template(cargo_toml_path)?; } + if self.contains(Adapter::Gnark) { + Gnark::build_dep_template(cargo_toml_path)?; + } Ok(()) } @@ -108,6 +117,9 @@ impl AdapterSelector { if self.contains(Adapter::Noir) { Noir::dev_dep_template(cargo_toml_path)?; } + if self.contains(Adapter::Gnark) { + Gnark::dev_dep_template(cargo_toml_path)?; + } Ok(()) } @@ -129,6 +141,12 @@ impl AdapterSelector { } else { Noir::lib_stub_template(lib_rs_path)?; } + + if self.contains(Adapter::Gnark) { + Gnark::lib_template(lib_rs_path)?; + } else { + Gnark::lib_stub_template(lib_rs_path)?; + } Ok(()) } @@ -142,6 +160,9 @@ impl AdapterSelector { if self.contains(Adapter::Noir) { Noir::build_template(build_rs_path)?; } + if self.contains(Adapter::Gnark) { + Gnark::build_template(build_rs_path)?; + } Ok(()) } @@ -159,6 +180,9 @@ impl AdapterSelector { if self.contains(Adapter::Noir) { Noir::build_bindings_lib(bindings_lib_path, project_name)?; } + if self.contains(Adapter::Gnark) { + Gnark::build_bindings_lib(bindings_lib_path, project_name)?; + } // copy ffi bindings test let ffi_bindings_dir_path = format!("{}/{}", bindings_lib_path, "ffi"); replace_test_bindings_lib_import(ffi_bindings_dir_path.as_str(), project_name)?; diff --git a/cli/src/init/gnark.rs b/cli/src/init/gnark.rs new file mode 100644 index 000000000..82005d0a2 --- /dev/null +++ b/cli/src/init/gnark.rs @@ -0,0 +1,16 @@ +use crate::init::adapter::Adapter; +use crate::init::proving_system::ProvingSystem; +use include_dir::include_dir; +use include_dir::Dir; + +pub struct Gnark; + +impl ProvingSystem for Gnark { + const TEMPLATE_DIR: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/src/template/gnark"); + + const ADAPTER: Adapter = Adapter::Gnark; + + const DEPENDENCIES: &'static str = r#" +rust-gnark = "0.0.2" + "#; +} diff --git a/cli/src/init/write_toml.rs b/cli/src/init/write_toml.rs index 62479aa0c..a82a33195 100644 --- a/cli/src/init/write_toml.rs +++ b/cli/src/init/write_toml.rs @@ -23,11 +23,13 @@ anyhow = "1.0.99" # CIRCOM_DEPENDENCIES # HALO2_DEPENDENCIES # NOIR_DEPENDENCIES +# GNARK_DEPENDENCIES [build-dependencies] # CIRCOM_BUILD_DEPENDENCIES # HALO2_BUILD_DEPENDENCIES # NOIR_BUILD_DEPENDENCIES +# GNARK_BUILD_DEPENDENCIES [dev-dependencies] mopro-ffi = { version = "0.3.4", features = ["uniffi-tests"] } @@ -35,6 +37,7 @@ mopro-ffi = { version = "0.3.4", features = ["uniffi-tests"] } # CIRCOM_DEV_DEPENDENCIES # HALO2_DEV_DEPENDENCIES # NOIR_DEV_DEPENDENCIES +# GNARK_DEV_DEPENDENCIES [target.wasm32-unknown-unknown.dependencies] mopro-ffi = { version = "0.3.4", features = ["wasm"] } diff --git a/cli/src/template/gnark/lib.rs b/cli/src/template/gnark/lib.rs new file mode 100644 index 000000000..184fd0b05 --- /dev/null +++ b/cli/src/template/gnark/lib.rs @@ -0,0 +1,42 @@ +#[cfg(not(target_arch = "wasm32"))] +mod gnark; +#[cfg(not(target_arch = "wasm32"))] +pub use gnark::{generate_gnark_proof, verify_gnark_proof, GnarkProofResult}; + +#[cfg(test)] +#[cfg(not(target_arch = "wasm32"))] +mod gnark_tests { + use crate::gnark::{generate_gnark_proof, verify_gnark_proof}; + + const R1CS_PATH: &str = "./test-vectors/gnark/cubic_circuit.r1cs"; + const PK_PATH: &str = "./test-vectors/gnark/cubic_circuit.pk"; + const VK_PATH: &str = "./test-vectors/gnark/cubic_circuit.vk"; + + #[test] + fn test_gnark_cubic_circuit() { + // x=3: x^3 + x + 5 = 27 + 3 + 5 = 35 + let witness_json = r#"{"X": "3", "Y": "35"}"#.to_string(); + + let result = generate_gnark_proof( + R1CS_PATH.to_string(), + PK_PATH.to_string(), + witness_json, + ); + assert!(result.is_ok(), "Proof generation should succeed"); + + let proof_result = result.unwrap(); + assert!(!proof_result.proof.is_empty(), "Proof should not be empty"); + assert!( + !proof_result.public_inputs.is_empty(), + "Public inputs should not be empty" + ); + + let valid = verify_gnark_proof( + R1CS_PATH.to_string(), + VK_PATH.to_string(), + proof_result, + ); + assert!(valid.is_ok(), "Verification should not error"); + assert!(valid.unwrap(), "Proof should be valid"); + } +} diff --git a/cli/src/template/init/build.rs b/cli/src/template/init/build.rs index f77b994b6..c456f3c2c 100644 --- a/cli/src/template/init/build.rs +++ b/cli/src/template/init/build.rs @@ -1,3 +1,4 @@ fn main() { // CIRCOM_TEMPLATE + // GNARK_TEMPLATE } diff --git a/cli/src/template/init/src/error.rs b/cli/src/template/init/src/error.rs index ab52f20d1..45acad117 100644 --- a/cli/src/template/init/src/error.rs +++ b/cli/src/template/init/src/error.rs @@ -7,4 +7,6 @@ pub enum MoproError { Halo2Error(String), #[error("NoirError: {0}")] NoirError(String), + #[error("GnarkError: {0}")] + GnarkError(String), } diff --git a/cli/src/template/init/src/gnark.rs b/cli/src/template/init/src/gnark.rs new file mode 100644 index 000000000..1ea719efb --- /dev/null +++ b/cli/src/template/init/src/gnark.rs @@ -0,0 +1,81 @@ +use crate::MoproError; +use std::sync::Once; + +/// Guards one-time initialization of the gnark Go runtime. +static GNARK_INIT: Once = Once::new(); + +/// Result of a gnark Groth16 BN254 proof generation. +/// +/// Both fields are hex-encoded gnark binary serializations: +/// - `proof`: compressed Groth16 proof +/// - `public_inputs`: public witness +#[derive(Debug, Clone)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +pub struct GnarkProofResult { + pub proof: String, + pub public_inputs: String, +} + +/// Generate a Groth16 BN254 proof using gnark. +/// +/// # Arguments +/// +/// * `r1cs_path` - Path to the `.r1cs` file (CBOR binary) +/// * `pk_path` - Path to the `.pk` file (gnark binary) +/// * `witness_json` - JSON object mapping circuit field names to decimal string values +/// +/// # Errors +/// +/// Returns [`MoproError::GnarkError`] if proof generation fails. +#[cfg_attr(feature = "uniffi", uniffi::export)] +pub fn generate_gnark_proof( + r1cs_path: String, + pk_path: String, + witness_json: String, +) -> Result { + GNARK_INIT.call_once(|| { + rust_gnark::init().expect("Failed to initialize gnark runtime"); + }); + + let result = rust_gnark::groth16_prove(&r1cs_path, &pk_path, &witness_json) + .map_err(|e| MoproError::GnarkError(e.to_string()))?; + + Ok(GnarkProofResult { + proof: result.proof, + public_inputs: result.public_inputs, + }) +} + +/// Verify a Groth16 BN254 proof using gnark. +/// +/// # Arguments +/// +/// * `r1cs_path` - Path to the `.r1cs` file +/// * `vk_path` - Path to the `.vk` file (gnark binary) +/// * `proof_result` - The proof result from [`generate_gnark_proof`] +/// +/// # Returns +/// +/// `true` if the proof is valid, `false` otherwise. +/// +/// # Errors +/// +/// Returns [`MoproError::GnarkError`] if verification encounters an error. +#[cfg_attr(feature = "uniffi", uniffi::export)] +pub fn verify_gnark_proof( + r1cs_path: String, + vk_path: String, + proof_result: GnarkProofResult, +) -> Result { + GNARK_INIT.call_once(|| { + rust_gnark::init().expect("Failed to initialize gnark runtime"); + }); + + let inner = rust_gnark::Groth16ProofResult { + proof: proof_result.proof, + public_inputs: proof_result.public_inputs, + }; + + rust_gnark::groth16_verify(&r1cs_path, &vk_path, &inner) + .map_err(|e| MoproError::GnarkError(e.to_string())) +} diff --git a/cli/src/template/init/src/lib.rs b/cli/src/template/init/src/lib.rs index 77ce74704..c07cfbfa7 100644 --- a/cli/src/template/init/src/lib.rs +++ b/cli/src/template/init/src/lib.rs @@ -41,3 +41,5 @@ mod uniffi_tests { // HALO2_TEMPLATE // NOIR_TEMPLATE + +// GNARK_TEMPLATE diff --git a/cli/src/template/init/src/stubs.rs b/cli/src/template/init/src/stubs.rs index 39773b310..33e11b023 100644 --- a/cli/src/template/init/src/stubs.rs +++ b/cli/src/template/init/src/stubs.rs @@ -149,3 +149,38 @@ macro_rules! noir_stub { }; }; } + +#[macro_export] +macro_rules! gnark_stub { + () => { + mod gnark_stub { + use crate::error::MoproError; + + #[derive(Debug, Clone)] + #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] + pub struct GnarkProofResult { + pub proof: String, + pub public_inputs: String, + } + + #[cfg_attr(feature = "uniffi", uniffi::export)] + pub fn generate_gnark_proof( + _r1cs_path: String, + _pk_path: String, + _witness_json: String, + ) -> Result { + panic!("Gnark is not enabled in this build. Please select \"gnark\" adapter when initializing the project."); + } + + #[cfg_attr(feature = "uniffi", uniffi::export)] + pub fn verify_gnark_proof( + _r1cs_path: String, + _vk_path: String, + _proof_result: GnarkProofResult, + ) -> Result { + panic!("Gnark is not enabled in this build. Please select \"gnark\" adapter when initializing the project."); + } + } + pub use gnark_stub::{generate_gnark_proof, verify_gnark_proof, GnarkProofResult}; + }; +} diff --git a/cli/src/template/init/test-vectors/gnark/cubic_circuit.pk b/cli/src/template/init/test-vectors/gnark/cubic_circuit.pk new file mode 100644 index 000000000..0454dfd29 Binary files /dev/null and b/cli/src/template/init/test-vectors/gnark/cubic_circuit.pk differ diff --git a/cli/src/template/init/test-vectors/gnark/cubic_circuit.r1cs b/cli/src/template/init/test-vectors/gnark/cubic_circuit.r1cs new file mode 100644 index 000000000..f9f8f34fe Binary files /dev/null and b/cli/src/template/init/test-vectors/gnark/cubic_circuit.r1cs differ diff --git a/cli/src/template/init/test-vectors/gnark/cubic_circuit.vk b/cli/src/template/init/test-vectors/gnark/cubic_circuit.vk new file mode 100644 index 000000000..43a9bfeb1 Binary files /dev/null and b/cli/src/template/init/test-vectors/gnark/cubic_circuit.vk differ diff --git a/cli/src/template/init/tests/bindings/gnark/test_gnark_cubic.kts b/cli/src/template/init/tests/bindings/gnark/test_gnark_cubic.kts new file mode 100644 index 000000000..481a1466b --- /dev/null +++ b/cli/src/template/init/tests/bindings/gnark/test_gnark_cubic.kts @@ -0,0 +1,23 @@ +// GENERATED LIB IMPORT PLACEHOLDER + +try { + val r1csPath = "./test-vectors/gnark/cubic_circuit.r1cs" + val pkPath = "./test-vectors/gnark/cubic_circuit.pk" + val vkPath = "./test-vectors/gnark/cubic_circuit.vk" + + // x=3: x^3 + x + 5 = 35 + val witnessJson = "{\"X\": \"3\", \"Y\": \"35\"}" + + // Generate proof + val proofResult = generateGnarkProof(r1csPath, pkPath, witnessJson) + assert(proofResult.proof.isNotEmpty()) { "Proof should not be empty" } + assert(proofResult.publicInputs.isNotEmpty()) { "Public inputs should not be empty" } + + // Verify proof + val isValid = verifyGnarkProof(r1csPath, vkPath, proofResult) + assert(isValid) { "Proof is invalid" } + +} catch (e: Exception) { + println(e) + throw e +} diff --git a/cli/src/template/init/tests/bindings/gnark/test_gnark_cubic.swift b/cli/src/template/init/tests/bindings/gnark/test_gnark_cubic.swift new file mode 100644 index 000000000..07b2bfe18 --- /dev/null +++ b/cli/src/template/init/tests/bindings/gnark/test_gnark_cubic.swift @@ -0,0 +1,29 @@ +import Foundation +// GENERATED LIB IMPORT PLACEHOLDER + +do { + let r1csPath = "../../../test-vectors/gnark/cubic_circuit.r1cs" + let pkPath = "../../../test-vectors/gnark/cubic_circuit.pk" + let vkPath = "../../../test-vectors/gnark/cubic_circuit.vk" + + // x=3: x^3 + x + 5 = 35 + let witnessJson = "{\"X\": \"3\", \"Y\": \"35\"}" + + // Generate Proof + let proofResult = try generateGnarkProof( + r1csPath: r1csPath, pkPath: pkPath, witnessJson: witnessJson) + assert(!proofResult.proof.isEmpty, "Proof should not be empty") + assert(!proofResult.publicInputs.isEmpty, "Public inputs should not be empty") + + // Verify Proof + let isValid = try verifyGnarkProof( + r1csPath: r1csPath, vkPath: vkPath, proofResult: proofResult) + assert(isValid, "Proof verification should succeed") + +} catch let error as MoproError { + print("MoproError: \(error)") + throw error +} catch { + print("Unexpected error: \(error)") + throw error +} diff --git a/cli/src/template/init/tests/gnark.rs b/cli/src/template/init/tests/gnark.rs new file mode 100644 index 000000000..8a8b2485c --- /dev/null +++ b/cli/src/template/init/tests/gnark.rs @@ -0,0 +1,6 @@ +mopro_ffi::uniffi_setup!(); + +#[cfg(target_os = "macos")] +uniffi::build_foreign_language_testcases!("tests/bindings/gnark/test_gnark_cubic.swift",); + +uniffi::build_foreign_language_testcases!("tests/bindings/gnark/test_gnark_cubic.kts",);