Rust Setup for Android/iOS Bindings
This tutorial guides you through building iOS and Android bindings from a Rust project. Itβs divided into three sections:
- π General setup β Use
mopro-ffi
to generate native bindings from your Rust project. - π Integrating a ZK prover β Add support for Circom, Halo2, and/or Noir.
- π¦ customize the bindings using any Rust crate, making it easy to expose your logic to mobile platforms.
Make sure you've installed the prerequisites.
π General setupβ
0. Initialize a new projectβ
If you already have a Rust project, skip ahead to 1. Add dependencies.
If you donβt have an existing Rust project, start by initializing a new Rust crate with creating a new directory (e.g., mopro-rust-project
) and change into it:
mkdir mopro-rust-project
cd mopro-rust-project
Initialize a new Rust crate by running:
cargo init --lib
1. Add dependenciesβ
Include the crate in your Cargo.toml
[dependencies]
mopro-ffi = { git = "https://github.com/zkmopro/mopro" }
uniffi = "0.29"
thiserror = "2.0.12"
[build-dependencies]
mopro-ffi = { git = "https://github.com/zkmopro/mopro" }
uniffi = { version = "0.29", features = ["build"] }
2. Setup the libβ
Define the crate-type
for the UniFFI build process configuration.
[lib]
crate-type = ["lib", "cdylib", "staticlib"]
3. Use mopro-ffi
macroβ
The mopro-ffi
macro exports the default Circom prover interfaces. To enable it, activate the mopro-ffi
macro in src/lib.rs
.
mopro_ffi::app!();
4. Define the binariesβ
The binaries are used to generate bindings for both iOS and Android platforms.
We'll add a new file at src/bin/ios.rs
:
fn main() {
mopro_ffi::app_config::ios::build();
}
and another at src/bin/android.rs
:
fn main() {
mopro_ffi::app_config::android::build();
}
5. What's nextβ
You have two options moving forward:
- Integrate a supported prover β Choose one of the built-in options: Circom, Halo2, or Noir (see the next section).
- Customize your bindings β If you're using a different prover or want to expose your own business logic, skip ahead to the Customize Bindings section.
π Integrating a ZK proverβ
Be sure to complete the general setup before continuing.
This tutorial will cover:
If youβre starting from scratch, the setup process is very similar to using the mopro CLI. Please refer to the Getting Started guide for detailed instructions.
Setup Circom-Based rust projectβ
π’ Ensure you've completed the General Setup before proceeding.
1. Add witness generatorβ
Each witness generator must be built within a project. You need to supply the required data for it to generate a circuit-specific execution function.
Here, we used rust-witness as an example.
To learn more about witnesscalc
and circom-witnesscalc
for Mopro, please check out circom-prover.
Include the rust-witness
in your Cargo.toml and enable circom
feature in mopro-ffi
:
[dependencies]
mopro-ffi = { git = "https://github.com/zkmopro/mopro", features = ["circom"] } # enable circom feature
rust-witness = "0.1"
num-bigint = "0.4"
[build-dependencies]
rust-witness = "0.1"
In build.rs
, add the following code to compile the witness generator wasm sources (.wasm
) into a native library and link to it:
fn main() {
rust_witness::transpile::transpile_wasm("../path to directory containing your wasm sources");
// e.g. rust_witness::transpile::transpile_wasm("./test-vectors".to_string());
// The directory should contain the following files:
// - <circuit name>.wasm
}
Learn more about .wasm
files in Circom documentation.
Here are the example WASM and Zkey files to be downloaded.
2. Use mopro-ffi
macroβ
Bind the corresponding WASM and Zkey files together using mopro-ffi
.
mopro_ffi::app!(); // Enable the mopro-ffi macro to generate UniFFI scaffolding.
// Activate rust-witness function
rust_witness::witness!(multiplier2);
// Set the witness functions to a zkey
mopro_ffi::set_circom_circuits! {
("multiplier2_final.zkey", mopro_ffi::witness::WitnessFn::RustWitness(multiplier2_witness)),
}
To ensure the circuit is correctly integrated with Rust, consider writing unit tests to verify the bindings.
#[cfg(test)]
mod circom_tests {
use super::*;
#[test]
fn test_multiplier2() {
let zkey_path = "./test-vectors/circom/multiplier2_final.zkey".to_string();
let circuit_inputs = "{\"a\": 2, \"b\": 3}".to_string();
let result = generate_circom_proof(zkey_path.clone(), circuit_inputs, ProofLib::Arkworks);
assert!(result.is_ok());
let proof = result.unwrap();
let valid = verify_circom_proof(zkey_path, proof, ProofLib::Arkworks);
assert!(valid.is_ok());
assert!(valid.unwrap());
}
}
3. Generate bindings for iOS and Androidβ
Now you're ready to build your static library! You should be able to run either the Mopro CLI or the binaries.
1. Execute the process through Mopro CLI (recommended ππ»)
To install the Mopro CLI, please refer to the Getting Started guide.
Execute the building through
mopro build
Then, you can select the target mode and architectures with greater flexibility.
2. Execute the process using the defined binaries.
For example:
cargo run --bin ios # Debug mode for iOS
cargo run --bin android # Debug mode for Android
CONFIGURATION=release cargo run --bin ios # Release mode for iOS
CONFIGURATION=release cargo run --bin android # Release mode for Android
ANDROID_ARCHS=x86_64-linux-android cargo run --bin android # Build for Android x86_64-linux-android architecture
IOS_ARCHS=aarch64-apple-ios,aarch64-apple-ios-sim cargo run --bin ios # Build for iOS aarch64-apple-ios and aarch64-apple-ios-sim architecture
to build the corresponding static library.
Running your project in release
mode significantly enhances performance compared to debug
mode. This is because the Rust compiler applies optimizations that improve runtime speed and reduce binary size, making your application more efficient.
4. What's nextβ
Once the bindings are successfully built, you will see the MoproiOSBindings
and/or MoproAndroidBindings
folders.
Next, you have two options:
- Use the
mopro create
command from the mopro CLI to generate ready-to-use templates for your desired framework (e.g., Swift, Kotlin, React Native, or Flutter). - If you already have a mobile app or prefer to manually integrate the bindings, follow the iOS Setup and/or Android Setup sections.
- If you find that some functionality is still missing on mobile, you can refer to the Customize the Bindings section to learn how to expose additional functions using any Rust crate.
Setup Halo2-Based rust projectβ
π’ Ensure you've completed the General Setup before proceeding.
1. Add Halo2 circuitsβ
Enable halo2
feature in mopro-ffi
and import the Halo2 prover as a Rust crate using:
[dependencies]
mopro-ffi = { git = "https://github.com/zkmopro/mopro", features = ["halo2"] } # enable halo2 feature
plonk-fibonacci = { package = "plonk-fibonacci", git = "https://github.com/sifnoc/plonkish-fibonacci-sample.git" }
See how to define a Halo2 prover crate and generate SRS here: plonkish-fibonacci-sample
Next, copy your SRS and key files into the project folder. For this tutorial, we'll assume you place them in test-vectors/halo2
.
Download example SRS and key files :
2. Use mopro-ffi
macroβ
Now, add three rust files, for example, in test-vectors/halo2
folder.
Update the src/lib.rs
file to look like the following:
mopro_ffi::app!(); // Enable the mopro-ffi macro to generate UniFFI scaffolding.
mopro_ffi::set_halo2_circuits! {
("plonk_fibonacci_pk.bin", plonk_fibonacci::prove, "plonk_fibonacci_vk.bin", plonk_fibonacci::verify),
}
To ensure the circuit is correctly integrated with Rust, consider writing unit tests to verify the bindings.
#[cfg(test)]
mod halo2_tests {
use std::collections::HashMap;
use super::*;
#[test]
fn test_plonk_fibonacci() {
let srs_path = "./test-vectors/halo2/plonk_fibonacci_srs.bin".to_string();
let pk_path = "./test-vectors/halo2/plonk_fibonacci_pk.bin".to_string();
let vk_path = "./test-vectors/halo2/plonk_fibonacci_vk.bin".to_string();
let mut circuit_inputs = HashMap::new();
circuit_inputs.insert("out".to_string(), vec!["55".to_string()]);
let result = generate_halo2_proof(srs_path.clone(), pk_path.clone(), circuit_inputs);
assert!(result.is_ok());
let halo2_proof_result = result.unwrap();
let valid = verify_halo2_proof(
srs_path,
vk_path,
halo2_proof_result.proof,
halo2_proof_result.inputs,
);
assert!(valid.is_ok());
assert!(valid.unwrap());
}
}
3. Generate bindings for iOS and Androidβ
Similar to Circom-based Rust project setup 3. Generate bindings for iOS and Android
4. What's nextβ
Similar to Circom-based Rust project setup 4. What's next
Setup Noir-Based rust projectβ
π’ Ensure you've completed the General Setup before proceeding.
0. Prepare Noir circuitsβ
Follow the Noir documentation to build your circuit.
Youβll need to generate a .json
file from the compiled circuit for use in this project.
Downloading the SRS (Structured Reference String) is optional but recommended, as it can significantly improve proving performance. See Downloading SRS for more details.
1. Enable noir
featureβ
[dependencies]
mopro-ffi = { git = "https://github.com/zkmopro/mopro", features = ["noir"] } # enable noir feature
Noir and its dependencies are not yet published as crates on crates.io. Therefore, we can only import them directly from GitHub.
Youβre now ready to generate a Noir proof using mopro-ffi.
Download example SRS and circuit files :
To verify everything is working correctly, you can write a Rust unit test like the one below to ensure the proof is computed successfully.
#[cfg(test)]
mod noir_tests {
use super::*;
#[test]
fn test_noir_multiplier2() {
let srs_path = "./test-vectors/noir/noir_multiplier2.srs".to_string();
let circuit_path = "./test-vectors/noir/noir_multiplier2.json".to_string();
let circuit_inputs = vec!["3".to_string(), "5".to_string()];
let result = generate_noir_proof(
circuit_path.clone(),
Some(srs_path.clone()),
circuit_inputs.clone(),
);
assert!(result.is_ok());
let proof = result.unwrap();
let valid = verify_noir_proof(circuit_path.clone(), proof);
assert!(valid.is_ok());
assert!(valid.unwrap());
}
}
2. Generate bindings for iOS and Androidβ
Similar to Circom-based Rust project setup 3. Generate bindings for iOS and Android
3. What's nextβ
Similar to Circom-based Rust project setup 4. What's next
π¦ Customize the bindingsβ
π’ Ensure you've completed the General Setup before proceeding.
If your ZK prover isnβt included, or you already have your own Rust crate,
you can use the #[uniffi::export]
procedural macro to define your own functions and generate mobile-native bindings from them.
1. Define exported functionsβ
Export Rust functions using a procedural macro, as shown below:
mopro_ffi::app!(); // Enable the mopro-ffi macro to generate UniFFI scaffolding.
#[uniffi::export]
pub fn hello_world() -> String {
"Hello, World!".to_string()
}
For more examples and detailed references, check the UniFFI documentation.
You may also bring in other Rust crates to extend functionality.
For instance, to use semaphore-rs
, you can add it like this:
[dependencies]
semaphore-rs = { git = "https://github.com/semaphore-protocol/semaphore-rs", features = ["serde"] }
mopro_ffi::app!(); // Enable the mopro-ffi macro to generate UniFFI scaffolding.
use semaphore_rs::group::Group;
use semaphore_rs::identity::Identity;
use semaphore_rs::proof::GroupOrMerkleProof;
use semaphore_rs::proof::Proof;
use semaphore_rs::proof::SemaphoreProof;
#[uniffi::export]
fn semaphore_prove(
id_secret: String,
leaves: Vec<Vec<u8>>,
message: String,
scope: String,
tree_depth: u16,
) -> String {
let identity = Identity::new(id_secret.as_bytes());
let group_members: Vec<[u8; 32]> = leaves
.iter()
.map(|leaf| {
leaf.as_slice()
.try_into()
.expect("Leaf must be exactly 32 bytes")
})
.collect::<Vec<[u8; 32]>>();
let group = Group::new(&group_members).unwrap();
let proof = Proof::generate_proof(
identity,
GroupOrMerkleProof::Group(group),
message.to_string(),
scope.to_string(),
tree_depth,
)
.unwrap();
let proof_json = proof.export().unwrap();
return proof_json;
}
#[uniffi::export]
fn semaphore_verify(proof: String) -> bool {
let proof = SemaphoreProof::import(&proof).unwrap();
let valid = Proof::verify_proof(proof);
valid
}
Example usage
#[cfg(test)]
mod uniffi_tests {
use super::*;
use semaphore_rs::utils::to_element;
#[test]
fn test_mopro_uniffi_hello_world() {
let secret1 = "secret1";
let secret2 = "secret2";
let identity1 = Identity::new(secret1.as_bytes());
let identity2 = Identity::new(secret2.as_bytes());
let leaves = vec![
to_element(*identity1.commitment()).to_vec(),
to_element(*identity2.commitment()).to_vec(),
];
let message = "message";
let scope = "scope";
let tree_depth = 10;
let proof = semaphore_prove(
secret1.to_string(),
leaves,
message.to_string(),
scope.to_string(),
tree_depth,
);
assert!(semaphore_verify(proof));
}
}
2. Generate bindings for iOS and Androidβ
Similar to Circom-based Rust project setup 3. Generate bindings for iOS and Android
Once the bindings are generated, you'll see your exported functions (e.g., semaphoreProve
, semaphoreVerify
) included in the generated codeβfor example, in MoproiOSBindings/mopro.swift
for iOS and MoproAndroidBindings/uniffi/mopro/mopro.kt
for Android.
You can then use these functions directly within your iOS and/or Android applications as part of the generated bindings.
3. What's nextβ
Similar to Circom-based Rust project setup 4. What's next