owned this note
owned this note
Published
Linked with GitHub
# Building a zk/snark-app with Nova
This writeup is a detailed how-to for developers who consider using Nova for their zk/snark-app. We will go over the project's repo regarding circuit development, wasm compilation and react/nextjs setup. For info regarding behind-the-scenes details and benchmarks regarding Nova head over [there](https://hackmd.io/mArMuUx5TC2LEcYecc741Q).
## Preparing your setup
**Ensure the demo app is working fine locally**
Run the following:
```shell
$ git clone git@github.com:dmpierre/nova-browser-ecdsa.git
$ cd nova-browser-ecdsa/
$ pnpm i && pnpm dev
```
Try to generate a proof from your [localhost](http://localhost:3000/).
**Install `wasm-pack`**
Since we will want to use Nova with wasm, you will need [`wasm-pack`](https://github.com/rustwasm/wasm-pack) installed.
In the repo, navigate to `nova-browser-ecdsa/apps/web/` and try to run `pnpm wasm:build`. If this runs fine, you will not have anything to configure. Otherwise, you will need to install `wasm-pack`. Assuming that your have rust installed on your machine, run `cargo install wasm-pack`. When this is done, try to run again `pnpm wasm:build`.
You might have various errors popping up. It might say that `blst` is not compiling properly. In that case, ensure that you have the correct environment variable setup so that rustc can find your llvm setup. See [this file](https://github.com/nalinbhardwaj/Nova-Scotia/blob/main/browser-test/env.sh). You might also end up with cargo complaining that no target is available for`wasm32-unknown-unknown`: this also boils down to setuping correctly your environment variables
There has been previous work done before on porting circuits written with the Halo2 proving system to wasm. You can find an example of it [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. It is fairly similar to what we are doing, so don't hesitate to go have a look.
## Signature aggregation circuit
We will use [Nova-Scotia](https://github.com/nalinbhardwaj/Nova-Scotia/tree/main) for porting our circom circuit to something that Nova can ingest.
First, Nova-scotia expects a `step_in` public signal to be defined in your circuit. This is because Nova *links* each of the witnesses that you will compute with your circuit together using this `step_in` variable. Each of the witness will also output public data which will be redirected to the following `step_in` signal of the next witness. This means that you should ensure that your output signal follows the same format as your `step_in` signal. There is no naming convention for output signals in Nova-Scotia. This is because all output variables of a circuit are public by default. You will sometimes see output signals called `step_out`.
---
![https://eprint.iacr.org/2023/969.pdf](https://hackmd.io/_uploads/H19XORlA2.png)
*A model of how nova works at a basic level. From [this](https://eprint.iacr.org/2023/969.pdf) paper. $F$ is an R1CS instance compiled from circom, $z_i$ are public inputs/outputs, i.e. `step_in`/`step_out` in Nova-scotia, while $aux_i$ are auxiliary private inputs*.
---
We went for a very straightforward solution regarding our aggregation circuit. We define three signals:
```
signal input step_in[7];
signal input signatures[N_SIGS][7];
signal output step_out[7];
```
Efficient ECDSA signatures are represented with arrays of 5 big integers. We append two additional values which consists of a public key in order to add a pubkey check to each verified signature. Hence, each signature consists of 7 values. `step_in` and `step_out` signals are the $z_i$ from the above diagram and all the remaining signatures are fed as private auxiliary inputs. This is not supposed to make sense in terms of an app. A good exercise could be to have as `step_in` an array of public keys and messages and `signatures` be only the signature data. This would make sense for apps where signature data is supposed to remain private.
Our batch size is of `N_SIGS`. This is basically the size of the folding, i.e. how many signatures we aggregate per folding step. You can read on the [benchmarks](https://hackmd.io/mArMuUx5TC2LEcYecc741Q#Benchmark) we ran how the size of a folding step relates to Nova's proving and verifier time.
Our `main.circom` circuit ends up being defined as:
```
component main { public [ step_in ] } = BatchEfficientECDSAPubKey(10);
```
As required, we defined `step_in` as a public variable. We can now compile it with:
```shell
$ circom --prime secq256k1 --r1cs --wasm ./main.circom
```
Note that we are using some different prime compared to the default one used by `circom`. This allows us to have some pretty nice performance gains. You can read [here](https://hackmd.io/mArMuUx5TC2LEcYecc741Q) as to why and how we did this. Be mindful of the prime that you are compiling your circuit with.
## Leveraging Nova-Scotia
Nova scotia is a library that acts as a shim between your circom compiled files and Nova. In our case, it will help us fold together multiple witness instances, each verifying batches of ECDSA signatures. We defined our aggregation with nova [here](https://github.com/dmpierre/nova-browser-ecdsa/blob/main/nova-ecdsa/src/main.rs).
First, we load our previously compiled R1CS file using `load_r1cs`. Then, we read all the signatures which we are going to aggregate and compute witnesses for. They are stored in a `.json` whose structure has been defined with [`struct EffSig`](https://github.com/dmpierre/nova-browser-ecdsa/blob/2308b6eb72de35e599fe1d562a9ff18a8a76225f/nova-ecdsa/src/main.rs#L14).
We then define our starting public input, namely $z_0$ - see above. Since our `step_in` expects one signature along with a public key, we initialize our `start_public_input` with the relevant values. We then set up all the auxiliary/private inputs. We have [`iteration_count`](https://github.com/dmpierre/nova-browser-ecdsa/blob/2308b6eb72de35e599fe1d562a9ff18a8a76225f/nova-ecdsa/src/main.rs#L41) folding steps for which we include [`per_iteration_count`](https://github.com/dmpierre/nova-browser-ecdsa/blob/main/nova-ecdsa/src/main.rs#L41) signatures.
We can now initialize our public parameters out of our R1CS using the `create_public_params` function. It will generate the relevant data needed for proving and verifying our SNARK. We can now run `create_recursive_circuit`. This will generate a `recursive_snark` proving the validity of all the signatures that we aggregated together. This `recursive_snark` has a `verifier()` method which we subsequently call to verify the validity of our proof. We provide to the `verify` method using our initial starting public inputs and also the number of folding steps that this recursive SNARK consists in.
## Writing our `wasm`
Porting our Nova SNARK to the browser is mostly a matter of almost copy-pasting what we have done above. We spinned up a folder called `nova-ecdsa-browser`. Within `wasm.rs`, we define three different functions `generate_params`, `generate_proof` and `verify_compressed_proof`. Each of those correspond to the above described steps. One slight difference is that we also generate spartan compressed proofs in the browser.
Since we will want to debug from within the browser, one of the most useful thing will be to call `init_panic_hook()` at the beginning of each of our function. This will make it possible to console log relevant error messages instead of the `error: unreachable` one. We removed those lines since, but keep in mind that this could be an helpful way of debugging your code.
Once this is ready, we can run the following:
```shell
$ wasm-pack build --target web --out-dir ../packages/nova-ecdsa-browser-pkg
```
This will generate a typescript package ready to be consumed by webapps. Since we are working in the context of a monorepo, we output it in a separate folder inside our `packages` workspace.
## Integrating within a webapp
We now switch to `apps/web` and install our Nova signature aggregation package by adding to our `package.json` the following line:
```
{
...
"nova-ecdsa-browser": "workspace:*",
...
}
```
In order to not block the browser's main thread when making Nova work, we will leverage [web workers](). We define a worker for each of the relevant steps: generating public parameters, computing a proof an verifying it. The code for each of those is stored within the `workers/` folder. Eventually, we also wrote relevant react hooks within the `hooks/` folder. Those hooks will be in charge of calling workers with the relevant data when a user asks for it.
### Testing your `wasm`
TBD
### A tiny glossary of potential errors
**1.`SharedArrayBuffer transfer requires self.crossOriginIsolated`**
When getting the following:
```
workerHelpers.js:98 Uncaught (in promise) DOMException: Failed to execute 'postMessage' on 'Worker': SharedArrayBuffer transfer requires self.crossOriginIsolated.
```
In `next.config.js`, ensure to add:
```js
async headers() {
return [
{
source: '/(.*?)',
headers: [
{
key: 'Cross-Origin-Embedder-Policy',
value: 'require-corp',
},
{
key: 'Cross-Origin-Opener-Policy',
value: 'same-origin',
},
{
key: 'Access-Control-Allow-Origin',
value: '*',
}
],
},
]
}
```
2. "Operation not supported on this platform" when using `wasm`. This could indicate that you are trying to read from a file on the target machine, which is not possible with wasm. There was a [bug](https://github.com/nalinbhardwaj/Nova-Scotia/pull/37) in `nova-scotia` before where the wasm binding called `current_dir()`. Although the PR is merged, a release has not been done yet so this might cause trouble.
3. Size of data that workers can receive and post is limited.