owned this note
owned this note
Published
Linked with GitHub
*Big thanks to [Nalin](https://twitter.com/nibnalin) for giving me the opportunity to work on this and on organizing Noveek in August. I learned a lot and it was awesome! Also, thanks to Cathie for her time and the work put into [`keras2circom`](https://github.com/socathie/keras2circom).*
*You can reach me on [twitter/X](https://twitter.com/xyz_pierre) for questions or to take the discussion further.*
# Building Apps with Nova: an example from `zkConnect4`
The [zk-connect4](https://zkconnect4.dev) project aims to further explore what can be achieved with Nova in consumer apps. It serves as a proof of concept pot-pourri of various elements: zkML, folding large r1cs instances, wasm compilation: all executed within the browser. This write-up encompasses different topics, all centered around Nova. We will cover:
1. Developing your own `wasm`.
2. Getting started with zkML using [`keras2circom`](https://github.com/socathie/keras2circom).
3. Leveraging web workers to integrate wasm within a web app.
## I. Wasm Development
WebAssembly, often abbreviated as wasm, is an attractive choice for designing high-performance frontend apps. Rust natively supports targeting wasm as a compilation target, which is advantageous since Nova is written in Rust. This makes compiling your proving stack (witness + proof generation) to wasm relatively straightforward.
### `wasm-pack`
To compile your wasm code, use the [`wasm-pack`](https://rustwasm.github.io/docs/wasm-pack/) library. It manages wasm compilation as well as typings and JS package generation. Getting `wasm-pack` to work on your machine might have some hurdles though.
Firstly, you'll need [`cargo`](https://doc.rust-lang.org/cargo/), Rust's package manager, installed. You can then run `cargo install wasm-pack`. Targeting wasm is quite specific, so you'll need to specify this to Rust when running `wasm-pack build`, the main command to build your JS package along with a usable wasm. For instance, we have a [specific folder](https://github.com/dmpierre/zkconnect4/tree/main/zkconnect4-nova-wasm) in the zkconnect4 project dedicated to the wasm proving logic. The `lib.rs` file indicates that we are indeed targeting wasm, while the `wasm.rs` file contains most of the logic needed to generate Nova proofs within the browser. Each function intended for compilation to wasm is annotated with `#[wasm_bindgen]`.
---
![Annotating](https://hackmd.io/_uploads/B1ZK3Zhgp.png)
*Annotating functions for wasm compilation*
---
Also, we have a corresponding [`Cargo.toml`](https://github.com/dmpierre/zkconnect4/blob/main/zkconnect4-nova-wasm/Cargo.toml) file indicating the wasm-specific dependencies needed.
For a nice `wasm` development experience, you can create a [`.vscode`](https://github.com/dmpierre/zkconnect4/tree/main/zkconnect4-nova-wasm/.vscode) folder to hold some environment-specific configuration variables. One important variable is the path to a working clang compiler installed with llvm, as `wasm-pack` requires it for building your project. You might encounter various errors when running `wasm-pack build --release --out-dir /path/to/directory --target web`, like `blst` not compiling properly. This often boils down to ensuring a correct environment variable setup so that rustc can find your llvm installation. For an example setup, check out [this file](https://github.com/nalinbhardwaj/Nova-Scotia/blob/main/browser-test/env.sh). You might also face issues with cargo complaining about no available target for `wasm32-unknown-unknown`, which also points to an incorrect setup regarding your environment variables.
Despite these setup caveats, `wasm-pack` has worked quite well and there hasn't been any major difficulty in getting it to run properly. There has been previous work on porting circuits written with the Halo2 proving system to wasm. You can find an example [here](https://github.com/nalinbhardwaj/zordle/tree/main). There is [some documentation](https://zcash.github.io/halo2/user/wasm-port.html) on how this was done. The `wasm-pack` documentation is also well-written.
### Testing your wasm
Within the `zk-connect4` project, we set up a few tests to ensure that the wasm was functioning correctly. This has been quite helpful to avoid waiting until the final wasm integration into the web app for testing. It also comes in handy for testing the app on different platforms. We have a dedicated `tests/` folder within `zkconnect4-nova-wasm`, which holds [`lib.rs`](https://github.com/dmpierre/zkconnect4/blob/main/zkconnect4-nova-wasm/tests/lib.rs) containing various tests that will run after executing:
```sh
wasm-pack test --{firefox, chrome, safari, ...}
```
---
![](https://hackmd.io/_uploads/BJqZxFpgT.png)*Tests will run by default on localhost:8000. They can also be done in headless mode.*
---
You can open your browser and check how things are going within the devtools. If you are not interested in doing this, you can pass the `--headless` flag. Be mindful of the somewhat strict timeout that `wasm-bindgen-test-runner`, the lib that `wasm-pack test` wraps behind the scenes, enforces in headless mode. You can also add the `--release` flag to test the release build directly and avoid using the significantly slower debug build.
## II. zkML with Nova and `keras2circom`
Over the last few months, [various zkML projects](https://github.com/worldcoin/awesome-zkml) have popped up. It happens that zk-connect4 runs a fully fledged RL agent against which players can measure against. The performance of the RL agent isn't the point we want to bring forward. Rather, we want to show how easy it is to integrate an ML model within Nova. In our case, we leveraged the [`keras2circom`](https://github.com/socathie/keras2circom) library. It transpiles tensorflow models to circom templates, making it easy to compile ML models into the R1CS format, a nova-compatible constraint system. After saving our tensorflow model in h5 format, cloning and pip installing all libs, you simply have to call
```sh
$ python main.py model.h5 --output model_output
```
Not all tf layers are supported, so you should prioritize architectures that will be adequately handled. You can explore which operations and layers are supported [here](https://github.com/socathie/keras2circom/blob/main/keras2circom/transpiler.py). In our case, our model is a simple fully connected neural net of 4 dense layers, each consisting of 50 neurons and a softmax output layer. Its R1CS representation consists of 115132 non-linear constraints.
I warmly recommend you go check out the [zator](https://github.com/lyronctk/zator) repo which pushed the boundaries in terms of what Nova can do with respect to zkML.
## III. Webapp integration
Our final step will require us to integrate our wasm within a working app. Since proving times can be a bottleneck, we do not want to block our browser's main thread during proof generation. To this end, we will leverage web workers and offload proving to a dedicated worker thread, different from the main one.
### Nextjs and webworkers
Be mindful that web workers are only available client side, while Nextjs also builds server-side code. Hence, you might encounter build-time errors such as `Worker is not defined`. This boils down to the fact that client side APIs are not available in server-side built code. Moreover, working with turborepo, I wanted to use the more modern `app/` directory. However, the Nextjs doc states that "[the worker strategy is not yet stable and does not yet work with the app directory](https://nextjs.org/docs/app/api-reference/components/script#worker)".
If you want to still use the `app/` dir setup, you can use the `@zeit/next-workers` plugin. It will simply require from you to update your `next.config.js`:
```js
// ...
const withWorkers = require('@zeit/next-workers');
// ...
module.exports = withWorkers({
reactStrictMode: true,
transpilePackages: ["ui"],
// ... some other config things
}
});
```
You can check the full `next.config.js` config setup [here](https://github.com/dmpierre/zkconnect4/blob/main/apps/web/next.config.js).
### Chunking public parameters
Webworkers support a limited bandwidth when it comes to exchanging data with your app. The public parameters that Nova generates can be quite large (think GBs). It can quickly become too large for web workers to handle or download in one go, leading them to silently fail. To avoid this issue, you can chunk the public parameters generated by Nova, a very large string, into many small substrings that you can then build back in the browser. This is similar in spirit to [chunking zkeys](https://github.com/nalinbhardwaj/snarkjs/blob/d1c10a6373c02eaa214968da96e2514ddc8c8b92/src/chunk_utils.js), but remains different in that we do not use the chunks directly but build back the whole public parameters to generate a proof beforehand. I'm sure something better can be done!
Eventually, our code for downloading our public parameters looks like this:
```js
for (let i = 0; i < N_CHUNKS; i++) {
downloads[i] = downloadChunk(multiThread, i);
}
const chunks = await Promise.all(downloads);
```
You could also store each of those substrings into the browser's local storage to avoid re-downloading those every time the user interacts with the app. This has also been implemented for zkeys [here](https://github.com/vb7401/ECDSAVerify-starter/blob/master/client/lib/util.ts).
### Webworker API
Finally, we use the [`comlink`](https://www.npmjs.com/package/comlink) library to expose our wasm API to our app's frontend. It has the benefits of *not* having to deal with the somewhat loaded API of web workers - such as using `postMessage`. We can define the behaviour of our worker in a single [`worker.ts`](https://github.com/dmpierre/zkconnect4/blob/main/apps/web/workers/worker.ts) file and expose its API using the `expose()` function from `comlink`.