-
Ensure you have Android Studio installed on your computer.
-
Clone the PoC from our GitHub repository.
-
Enable developer mode on your Android device and run the PoC through Android Studio.
-
Upload the witness and zkey files to your phone, for example Downloads directory. You can do this using the Device Explorer in Android Studio. Example files are located in
./examples
directory -
Start our PoC. Use buttons Select Witness File and Select ZKey File to select corresponding files. Press Start Computation to run the Groth16 proof. Upon comletion the result prints to Output textbox, including benchmarking data.
Issue: use of cache in our Groth16 requires to restart the app when changing circuits!
In our PoC we used Aptos keyless circuit for benchmarks. The instructions to derive witness and zkey files are therein.
We will soon add more benchmarks.
For Aptos keyless circuit, on a midrange smartphone (e.g., Samsung A54), the initial proof generation takes approximately 60 seconds. Subsequent proofs are significantly faster, taking around 30 seconds due to caching.
The diagram above illustrates the structure of our application. Details are provided below.
The user interface is implemented in Kotlin, which has been Google's recommended language for new Android projects since 2019. Kotlin's support for coroutines allows for effective asynchronous programming, a crucial feature for managing time-consuming ZK proofs.
The Kotlin file MainActivity.kt
declares and utilizes the function external fun Groth16()
, which is implemented in Rust. We recommend that developers continue using their preferred programming languages for ZK applications, provided they are supported by Android's Native Development Kit (NDK), specifically through Java Native Interface (JNI). JNI officially supports C and C++, and the Rust community has developed excellent support for Android. The Go language is also supported, although we haven't tested it for this PoC.
Our Rust implementation of Groth16 is still under active development in a private repository and will be open-sourced once it stabilizes. In this PoC, we include it as a compiled shared library. Below, we explain how we integrated it with Android:
-
Add the
jni
crate as a dependency inCargo.toml
. This crate provides Rust bindings to JNI, allowing the Rust code to interact with Java or Kotlin in your Android app.[dependencies] jni = "0.20"
-
Define a Rust function that can be called from the Android app using JNI.
#[no_mangle] pub extern "C" fn Java_com_ingonyama_groth16_MainActivity_00024Companion_Groth16( env: jni::JNIEnv, _: jni::objects::JClass, ) -> jni::sys::jstring { ... }
Key points to note:
- Use JNI naming conventions to expose package, class, and method names.
- Ensure proper linking using
#[no_mangle]
andpub extern "C"
for the function. - Correctly interact with the Java environment and handle input/output objects.
You can find more detailed explanations and examples here.
-
Finally, use the
cargo ndk -t arm64-v8a
toolchain to cross-compile the code to a specific Application Binary Interface (ABI). In this example, we target the 64-bit ARM ABI. The resulting.so
shared librarylibgroth16.so
can be found here.
The Groth16 function uses Icicle to execute zero-knowledge primitives such as Number Theoretical Transform (NTT), Multi-Scalar Multiplication (MSM), Merkle trees, etc. Icicle v3 supports multiple hardware accelerators, referred to as backends. For server-side applications, Icicle typically uses a GPU backend (e.g., NVIDIA). The development of mobile GPU backends (e.g., ARM's Mali, Apple's Metal) for client-side applications is ongoing. In this PoC, we use the default CPU backend. For convenience, Icicle is provided as compiled shared libraries, which are located here.