Sailos
Expo - React Native

Circom Guide

In this guide we'll talk about how to run Circom (Groth16) Circuits on your iOS/Android devices. We won't go in details about how to setup and build your Circom Circuits or how to make an Expo App.

Quickstart

Prepping the Project

Let's get started with a blank expo project, you can also use any existing expo app, adding zk-proofs requires only a couple of lines really.

bun create expo --template blank-typescript

and then add our expo module and relevant libs.

bunx expo install zk-expo expo-asset

We already have some sample files ready for you that you'll need to get your circuits running, create a directory /public in the project root and put these files in it.

Expo throws some errors if the files have the same name regardless of extensions so make sure they have different names.

Next, create a file in the root called metro.config.js so that expo can pick up the wasm, zkey & graph extensions.

metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const path = require("path");
 
const config = getDefaultConfig(__dirname);
 
config.resolver.assetExts = ["wasm", "zkey", "graph"]; 
 
module.exports = config;

Running the circuits

Load the Assets

App.tsx
import { StyleSheet, Button, View } from 'react-native';
import { useAssets } from "expo-asset"; 
import * as ZkExpo from "zk-expo"; 
 
export default function App() {
 
  const [assets, error] = useAssets([ 
    require("./public/loginProof_wasm.wasm"), 
    require("./public/loginProof_zkey.zkey"), 
  ]); 
 
  return (
    <View style={styles.container}>
      <Button title="Make Groth16 Proof" />
    </View>
  );
 
}
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});

Make the Proof

App.tsx
import { useAssets } from "expo-asset";
import * as ZkExpo from "zk-expo";
 
export default function App() {
 
  const [assets, error] = useAssets([
    require("./public/circuit_wasm.wasm"),
    require("./public/circuit_zkey.zkey")
  ]);
 
  async function make() { 
    let inputs = { 
      secret: "1362766633228928585266883205500641602962979188179006392651332184307221268928",  
      message: "246923712567881323126559150739123310486883838966133236273155052809857112023",  
      scope: "187035976211163640032000461805755068405187575174480755232212391996596767257",  
    }; 

    if (assets) {  
      let proof = await ZkExpo.groth16Prove(  
        assets[0].localUri as string,  
        assets[1].localUri as string, 
        JSON.stringify(inputs), 
      ); 
      console.log({proof}) 
    } else { 
      alert("Assets not loaded"); 
    } 
  } 
 
  return (
    <View style={styles.container}>
      <Button title="Make Groth16 Proof" onPress={make} />  // [!code highlight]
    </View>
  );
 
}
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});

Voila 🥳 You just made a proof.

Make the Proof (Advanced)

This method is experimental but gives a 2.5 - 3X Speedup. It does so by precompting the static execution graph required for the witness generation and getting rid of the wasm runtime requirement. Not all Circom operations are supported but should work for most circuits, you can read more about this method at @iden3/circom-witness

App.tsx
import { useAssets } from "expo-asset";
import * as ZkExpo from "zk-expo";
 
export default function App() {
 
  const [assets, error] = useAssets([
    require("./public/circuit_graph.graph"),
    require("./public/circuit_zkey.zkey")
  ]);
 
  async function make() {
    let inputs = {
      secret:
        "1362766633228928585266883205500641602962979188179006392651332184307221268928",
      message:
        "246923712567881323126559150739123310486883838966133236273155052809857112023",
      scope:
        "187035976211163640032000461805755068405187575174480755232212391996596767257",
    };
 
    if (assets) {
      let proof = await ZkExpo.groth16ProveV2(  
        assets[0].localUri as string,
        assets[1].localUri as string,
        JSON.stringify(inputs),
      );
      console.log({proof})
    } else {
      alert("Assets not loaded");
    }
  }
 
  return (
    <View style={styles.container}>
      <Button title="Make Groth16 Proof" onPress={make} />  // [!code highlight]
    </View>
  );
 
}
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});

Voila 🥳 You just made a super-fast proof.

On-chain Verification

API Reference

ZkExpo.groth16Prove

async function groth16Prove(
  wasm_path: string,  // Local path to the wasm file.
  zkey_path: string,  // Local path to the zkey file.
  inputs: string,     // These are your JSON.stringified dictionary of inputs.
): Promise<{ proof: any; publicSignals: any }>

ZkExpo.groth16ProveV2

async function groth16Prove(
  graph_path: string, // Local path to the graph file.
  zkey_path: string,  // Local path to the zkey file.
  inputs: string,     // These are your JSON.stringified dictionary of inputs.
): Promise<{ proof: any; publicSignals: any }>

Some Gotchas

  1. Make sure to update your dependencies, also use npx expo install --fix.
  2. Use Development Client Builds, this won't work on Expo Go as it requires native code.
  3. If your app crashes while testing groth16Prove on a real iOS device, run the app using bun expo prebuild and XCode. iOS has some wierd quirks while running Wasm runtimes, this should be fine in the production versions of your app. You can also use groth16ProveV2 which doesn't need the wasm runtime and is faster.
  4. iOS has memory restrictions of around 2-3GB imposed on each app. If your circuits are really large you might run into issues, consider splitting the circuits or using groth16ProveV2 to optimize your circuits.
  5. If your iOS app runs into any linking issues on developement when using eas build, do an bun expo prebuild and open the ios folder in XCode. XCode GUI uses certain Apple internals that perform better than eas builds that use xcodebuild under the hood.

On this page