Skip to main content

2 posts tagged with "noir"

View All Tags

Mopro x Noir: Powering Mobile Zero-Knowledge Proofs

· 11 min read
Vivian Jeng
Developer on the Mopro Team

Introduction

Noir has been gaining significant traction recently, and for good reason. It's renowned for its elegant circuit frontend design and high-performance backend. With robust SDKs that make building web-based zero-knowledge apps seamless—and the ability to verify proofs on-chain—Noir stands out as a powerful and promising framework for developing modern ZKP applications.

However, while Noir offers strong support for web-based applications, native mobile support is still limited within the ecosystem. Most existing resources for mobile development have been contributed by the zkPassport team, including key projects like

Our work is deeply inspired by the zkPassport team’s contributions, but builds upon them with significant improvements and optimizations.

In this article, we’ll walk through how we integrated Noir into the Mopro project, highlighting the key challenges we faced and the opportunities we uncovered along the way. We'll also introduce Stealthnote Mobile, a fully native mobile application built with Noir, to demonstrate the potential and performance of running zero-knowledge proofs natively on mobile devices.

Build noir-rs

Like many apps with Mopro, our journey began with building a Rust crate. We started by adopting noir_rs from the zkPassport team. However, we quickly ran into some major challenges: compiling the Barretenberg backend — the proving system used by most Noir projects — could take up to 20 minutes. On top of that, the build process required a very specific developer environment, and lacked caching, meaning the entire binary had to be rebuilt from scratch even after a successful build.

To address these issues, we introduced a solution in

by prebuilding the backend binaries and hosting them on a server. During the build process, these binaries are downloaded automatically, eliminating the need for local compilation. This approach drastically reduces build time—from 20 minutes to just 8 seconds—and removes the need for any special environment setup. In fact, you can see from our GitHub Actions YAML file that no additional dependencies are required.

Suggestions

Currently, the build process for the backend happens locally, which makes it non-reproducible and difficult to upgrade in CI environments like GitHub Actions. To improve this, we believe the build logic should be moved into CI. However, this process is quite complex and largely duplicated across repositories like the Noir and Aztec packages (See: publish-bb.yml).

Our proposal is that the Noir and Aztec teams consolidate this effort by building libbarretenberg.a within the same CI pipeline and releasing them. Since bb depends on these static libraries, they would naturally be compiled as part of that process. These prebuilt artifacts could then be published alongside the bb binary. This would allow downstream consumers like noir-rs to directly use the published binaries, eliminating the need to maintain custom build scripts or host binaries separately.

note

bb is a CLI tool that generates proofs directly in the user's terminal. Underneath, it relies on libbarretenberg.a—a static library that exposes low-level function APIs for proof generation, e.g.

void acir_prove_ultra_honk(uint8_t const* acir_vec,
bool const* recursive,
uint8_t const* witness_vec,
uint8_t** out)

When properly linked, this library can be used directly within a Rust program, enabling seamless integration of the proving system into native applications.

Go Mobile-Native

Once we have the Rust crate ready, integrating it into a Mopro project allows us to easily generate bindings for both iOS and Android. Mopro significantly improves the mobile integration experience in the following ways:

  1. No additional Swift or Kotlin bindings required

    Unlike the zkPassport team's approach—where they built separate Swoir and noir_android libraries with custom domain-specific interfaces—Mopro leverages uniffi to automatically generate language bindings. This means developers don’t need to manually maintain Swift 1 or Kotlin wrappers 2; they can directly import and use the generated bindings in their mobile codebases. e.g.

    For swift:

    import moproFFI

    let proofData = try! generateNoirProof(circuitPath: zkemailCircuitPath, srsPath: zkemailSrsPath, inputs: inputs)

    For kotlin:

    import uniffi.mopro.generateNoirProof

    let proofData = generateNoirProof(circuitFile, srsPath, inputs)
  2. Framework-Agnostic Design

    Mopro is not tied to any specific mobile framework. By defining reusable templates for each target framework, developers are free to choose the environment they’re most comfortable with. Currently, Mopro supports native iOS (Xcode + Swift), native Android (Android Studio + Kotlin), as well as cross-platform frameworks like React Native and Flutter—offering maximum flexibility and accessibility for diverse developer needs.

Mopro Support

We’ve successfully integrated Noir proving into both Mopro-FFI and the Mopro CLI. You can now install the Mopro CLI and follow the steps in the Getting Started guide to create a Noir project. Simply replace the SRS and circuit files with your own, and provide your custom circuit input — that’s it!

We’ve also provided an example zkEmail repository featuring a Noir circuit

along with a NoirHack workshop video.

Feel free to explore the repo, clone it, and follow along to learn how to use Noir with Mopro in a real-world application.

Challenge: Cross-platform support is limited

Our current implementation draws heavily from the zkPassport team’s work, which currently supports iOS devices and ARM64 Android devices/emulators. However, there are limitations: iOS simulators—essential for efficient development and testing—are not supported, and many Android developers (especially those using Windows with WSL) rely on x86_64 emulators. Even CI environments like GitHub Actions commonly use x86_64 Android emulators.

PlatformsCurrent support targetSupport
iOSaarch64-apple-ios
iOSaarch64-apple-ios-sim
iOSx86_64-apple-ios
Androidx86_64-linux-android
Androidi686-linux-android
Androidarmv7-linux-androideabi
Androidaarch64-linux-androids
MacOSstable-aarch64-apple-darwin
Linuxx86_64-unknown-linux-gnu
Windowsx86_64-pc-windows-msvc

Expanding support to these platforms is a significant challenge. The barretenberg backend, written in C++ and built with CMake, is large and complex, making cross-compilation non-trivial. While we’re evaluating the effort required to support additional architectures, we’re also hopeful that the Noir or zkPassport teams may address this gap in the future.

Case Study: Stealthnote Mobile App

During the NoirHack 2025 (April 14th to May 10th), the Mopro team participated by building a mobile-native Noir application. Our project was inspired by Stealthnote, originally created by Saleel. Stealthnote is a web-based app that allows users to sign in with Google OAuth and prove ownership of their organizational email address. It leverages a Noir circuit to generate a zero-knowledge proof from the JWT issued by Google OAuth. You can read more about the original project in Saleel’s blog post.

We aim to enhance performance and user experience by building a fully native mobile app. At the same time, we want to demonstrate that the current Mopro + Noir stack is fully capable of supporting mobile-native development. Therefore, we decided to build a mobile version of Stealthnote.

What We Built

During the hackathon, we developed

A mobile-native frontend for the original Stealthnote project. To maintain compatibility, we kept the original Noir circuits and backend logic intact, focusing entirely on building the mobile frontend. Even so, creating a mobile frontend involved tackling many challenges across platforms.

We chose Flutter as our cross-platform framework because of its fast build times compared to React Native and its rich ecosystem of packages for mobile functionalities—such as Google authentication, biometric login, camera access, gallery browsing, and file storage. In our view, Flutter is currently the best choice for building high-performance, cross-platform apps.

Our architecture separates responsibilities clearly:

  • Flutter handles:

    1. UI/UX design
    2. Google OAuth authentication
    3. Backend API communication
  • Rust handles:

    1. Parsing the JWT and converting it into the structured inputs required by the Noir circuit
    2. Generating and verifying Noir proofs
    3. Ephemeral key generation using Ed25519 and Poseidon2
    info

    To learn how to integrate Mopro Rust bindings into a Flutter project, please refer to the

This clear separation lets us leverage the strengths of both technologies—Flutter’s frontend speed and flexibility, and Rust’s performance and security for core cryptographic logic.

Benchmark

While the Mopro team anticipated the outcome, the benchmark results are still notable: a mobile-native prover delivers performance up to 10 times faster than running the same proof in a browser environment.

JWT OperationProveVerify
Browser37.292 s0.286 s
Desktop (Mac M1 Pro)2.02 s0.007 s
Android emulator (Pixel 8)4.786 s3.013 s
iPhone 16 Pro2.626 s1.727 s

Challenge: Insufficient Rust tooling and SDKs.

One of the main challenges we faced was converting all the TypeScript functions used in StealthNote into their Rust equivalents. For standard cryptographic operations like Ed25519 signatures, this was relatively straightforward — Rust has mature libraries we could rely on.

However, Poseidon2 presented a bigger challenge. Unlike the original Poseidon hash, Poseidon2 is a newer variant used in the Noir circuits. Unfortunately, there was no direct Rust implementation available. The Noir team primarily supports a TypeScript version via bb.js, which is a wrapper around Barretenberg's C++ implementation compiled to WASM. This made it hard to fully understand the logic and port it to Rust.

Eventually, we located the Poseidon2 Noir circuit and its permutation logic in the noir-lang repository. Using that as a reference, we implemented our own version of

aligning it with the Noir circuit to ensure compatibility.

Suggestions

Since Barretenberg already provides a C++ implementation for most Noir circuit functions, it would actually be more efficient and sustainable for the Noir or Aztec team to maintain a native C++ or Rust SDK for Rust developers — rather than focusing primarily on the JavaScript (bb.js) interface.

As we suggested earlier, an ideal solution would be for the team to publish precompiled static binaries (e.g. libbarretenberg.a) alongside versioned releases. This way, other developers — like us — could easily integrate these binaries into their Rust projects using proper FFI bindings.

This approach would remove the need for every developer to rebuild Barretenberg from source or maintain their own custom builds. For example, our bb.rs project demonstrates this idea, but it still lacks many of the standard library features available in bb.js, such as: poseidon2, blake2, AES encryption/decryption. Providing official support for these as native libraries would greatly improve the developer experience and adoption for building performant, mobile-native or server-side ZK applications.

Conclusion

We began integrating Noir with Mopro in early April, and the process progressed smoothly — thanks in large part to the foundational work done by the zkPassport team. In contrast to our experience with the Circom prover (which took several months to make mobile-compatible), Noir integration was faster and more straightforward.

However, we also discovered significant gaps in Noir’s current support for mobile-native development. While the prover itself is crucial, a complete SDK ecosystem is equally important. At the moment, teams aiming to build mobile-native apps — like zkPassport — must develop many of the components from scratch, rather than being able to rely on official tooling or prebuilt libraries from the Noir team.

This raises a challenge: what if a project doesn’t have the expertise, time, or resources to build and maintain a mobile-native infrastructure? These barriers can prevent great ideas from becoming usable apps.

There’s still a long road ahead — for both the Mopro team and the Noir ecosystem — to provide a full suite of mobile-native infrastructure that can empower ZK developers, whether they're building the next zkPassport or an entirely new kind of app.

Finally, we want to encourage more developers to explore building mobile-native apps. These platforms offer better performance, deeper device integration, and more seamless user experiences — which are essential if you want your ZK project to truly reach users.

Contribution

All kinds of contributions are welcome! 🎉

Feel free to check out the current issues on the Mopro GitHub repository, or open a new issue if you notice something missing or have ideas to improve the project.

Here are some current Noir-related issues worth exploring:

Feel free to reach out to the Mopro team on Telegram @zkmopro for questions or support, and follow us on X (formerly Twitter) @zkmopro to stay up to date with our latest progress!

Footnotes

  1. Noir Swift wrapper: https://github.com/Swoir/Swoir/blob/main/Sources/Swoir/Circuit.swift

  2. Noir Kotlin wrapper: https://github.com/madztheo/noir_android/blob/main/lib/src/main/java/com/noirandroid/lib/Circuit.kt

Integrating Mopro Native Packages Across Mobile Platforms

· 8 min read
Moven Tsai
Developer on the Mopro Team

TL; DR Mopro now ships pre-built native packages for Swift (iOS), Kotlin (Android), Flutter, and React Native.
Just one import and one build. Proving made simple!

Announcing Mopro Native Packages

We're excited to launch Mopro native packages, enabling developers to effortlessly generate and verify zero-knowledge proofs (ZKPs) directly on mobile devices. These native packages leverage Rust's performance and seamlessly integrate with popular mobile frameworks. Built using the Mopro CLI, they're available for direct import via each platform's package manager.

You can also easily create your own customized native packages by following zkmopro-docs.

FrameworkPackage ManagerDefault PackageszkEmail Packages via Mopro
Swift (iOS)Xcode / SwiftPM / CocoaPodsmopro-swift-packagezkemail-swift-package
Kotlin (Android)JitPackmopro-kotlin-packagezkemail-kotlin-package
Flutterpub.devmopro_flutter_packagezkemail_flutter_package
React Nativenpm / yarnmopro-react-native-packagezkemail-react-native-package

This blog provides a quick guide on integrating these packages into your projects, outlines how we built them (so you can customize your own), addresses challenges we overcame, and highlights future developments. Let's get started!

Import, Build, Prove - That's It

Mopro's native packages simplify the integration process dramatically. Unlike the traditional approach that requires crafting APIs, generating bindings, and manually building app templates, these pre-built packages allow developers to import them directly via package managers and immediately begin developing application logic.

For ZK projects, converting your Rust-based solutions into mobile-native packages is straightforward with Mopro. Our guide on "How to Build the Package" explains the process clearly.

For instance, our zkEmail native packages were created by first defining ZK proving and verification APIs in Rust, generating bindings with mopro build, and embedding these into native packages. The circuit is the header-only proof from zkemail.nr_header_demo.

Here's how zkEmail performs on Apple M3 chips:

zkEmail OperationiOS, Time (ms)Android, Time (ms)
Proof Generation1,3093,826
Verification9622,857

iOS zkEmail App Example
iOS
Android zkEmail App Example
Android

Flutter App for iOS & Android zkEmail Example

Notice that, with Mopro and the use of Noir-rs, we port zkEmail into native packages while keeping the proof size align with Noir's Barretenberg backend CLI. It transfers the API logic directly to mobile platforms with no extra work or glue code needed!

How it worked before Mopro

Previously, integrating ZKPs into mobile applications involved more manual work and platform-specific implementations. For example, developers might have used solutions like:

These approaches often required developers to handle bridging code and manage dependencies separately for each platform, unlike the streamlined process Mopro now offers.

With Mopro, developers can leverage pre-built native packages and import them directly via package managers. This, combined with automated binding generation, significantly reduces the need for manual API crafting and platform-specific glue code.

While developers still write their application logic using platform-specific languages, Mopro simplifies the integration of core ZK functionalities, especially when leveraging Rust's extensive cryptography ecosystem.

Under The Hood

Developing native packages involved tackling several technical challenges to ensure smooth and efficient operation across different platforms.

This section dives into two key challenges we addressed:

  1. Optimizing static library sizes for iOS to manage package distribution and download speeds.
  2. Ensuring compatibility with Android's release mode to prevent runtime errors due to code shrinking.

Optimizing Static Library Sizes for iOS

Why Static Linking?

UniFFI exports Swift bindings as a static archive (libmopro_bindings.a). Static linking ensures all Rust symbols are available at link-time, simplifying Xcode integration. However, it bundles all Rust dependencies (Barretenberg Backend, rayon, big-integer math), resulting in larger archive sizes.

Baseline Size

The full build creates an archive around ≈ 153 MB in size. When uploading files over 100 MB to GitHub, Git LFS takes over by replacing the file with a text pointer in the repository while storing the actual content on a remote server like GitHub.com. This setup can cause issues for package managers that try to fetch the package directly from a GitHub URL for a release publish.

While uploading large files may be acceptable for some package management platforms or remote servers like Cloudflare R2, the large file size slows down:

  • CocoaPods or SwiftPM downloads
  • CI cache recovery
  • Cloning the repository, especially on slower connections

Our Solution: Zip & Unzip Strategy

To keep development fast and responsive, we compress the entire MoproBindings.xcframework before uploading it to GitHub and publishing it to CocoaPods, reducing its size to about ≈ 41 MB.

We also found that by customizing script_phase in the .podspec (check our implementation in ZKEmailSwift.podspec), we can unzip the bindings during pod install. This gives us the best of both worlds: (1) smaller packages for distribution and (2) full compatibility with Xcode linking. The added CPU cost is minor compared to the time saved on downloads.

Comparison With Android

On Android, dynamic .so libraries (around 22 MB in total) are used, with symbols loaded lazily at runtime to keep the package size small. In contrast, because iOS's constraint on third-party Rust dynamic libraries in App Store builds, static linking with compression is currently the most viable option, to the best of our knowledge.

Ensuring Android Release Mode Compatibility

Another challenge we tackled was ensuring compatibility with Android's release mode. By default, Android's release build process applies code shrinking and obfuscation to optimize app size. While beneficial for optimization, this process caused a java.lang.UnsatisfiedLinkError for Mopro apps.

The root cause was that code shrinking interfered with JNA (Java Native Access), a crucial dependency for UniFFI, which we use for Rust-to-Kotlin bindings. The shrinking process was removing or altering parts of JNA that were necessary for the bindings to function correctly, leading to the UnsatisfiedLinkError when the app tried to call the native Rust code.

The Fix: Adjusting Gradle Build Configurations

Our solution, as detailed in GitHub Issue #416, involves a configuration adjustment in the consuming application's android/build.gradle.kts file (or android/app/build.gradle for older Android projects). Developers using Mopro need to explicitly disable code and resource shrinking for their release builds:

android {
// ...
buildTypes {
getByName("release") {
// Disables code shrinking, obfuscation, and optimization for
// your project's release build type.
minifyEnabled = false
// Disables resource shrinking, which is performed by the
// Android Gradle plugin.
shrinkResources = false
}
}
}

Impact and Future Considerations

This configuration ensures that JNA and, consequently, the UniFFI bindings remain intact, allowing Mopro-powered Android apps to build and run successfully in release mode. This approach aligns with recommendations found in the official Flutter documentation for handling similar issues. While this increases the final app size slightly, it guarantees the stability and functionality of the native ZK operations. We are also actively exploring ways to refine this in the future to allow for optimized builds without compromising JNA's functionality.

The Road Ahead

a. Manual Tweaks for Cross-Platform Frameworks

Cross-platform frameworks like React Native and Flutter require additional glue code to define modules, as they straddle multiple runtimes. Each layer needs its own integration.

For example, in our zkEmail React Native package, we use three separate wrappers.

Similarly, for our zkEmail Flutter package, a comparable set of wrappers is employed:

b. Support for Custom Package Names

Initially, we encountered naming conflicts when the same XCFramework was used in multiple Xcode projects. Addressing this to allow fully customizable package names is an ongoing effort.

Initial progress was made with updates in issue#387 and a partial fix in PR#404. Further work to complete this feature is being tracked in issue#413.

What's Next: Shaping Mopro's Future Together

Currently, the Mopro CLI helps you create app templates via the mopro create command, once bindings are generated with mopro build.

Our vision is to enhance this by enabling the automatic generation of fully customized native packages. This would include managing all necessary glue code for cross-platform frameworks, potentially through a new command (maybe like mopro pack) or by extending existing commands. We believe this will significantly streamline the developer workflow. If you're interested in shaping this feature, we invite you to check out the discussion and contribute your ideas in issue #419.

By achieving this, we aim to unlock seamless mobile proving capabilities, simplifying adoption for developers leveraging existing ZK solutions or porting Rust-based ZK projects. Your contributions can help us make mobile ZK development more accessible for everyone!

If you find it interesting, feel free to reach out to the Mopro team on Telegram: @zkmopro, or better yet, dive into the codebase and open a PR! We're excited to see what the community builds with Mopro.

Happy proving!