owned this note
owned this note
Published
Linked with GitHub
# Tutorial: Use batching for Semaphore Noir proof
Batching is a powerful technique to reduce on-chain verification costs by aggregating multiple Semaphore Noir proofs into a single Batch proof.
This tutorial walks you through how to the individual Semaphore Noir proofs, batch them together into a single Batch proof and finally perform verification on-chain.
Note that this tutorial currently relies on local dependencies. In the future we will replace this with deployed npm packages.
**What you'll do:**
**1. Build local packages**
Setup the `semaphore-noir` and `snark-artifacts` repositories locally.
**2. Generate Semaphore proofs**
Use the `noir-proof-batch` package in the SDK to create multiple Semaphore Noir proofs.
**3. Create a batch proof**
Obtain a single batch proof of your Semaphore proofs.
**4. Verify on-chain**
Send the final batch proof to a deployed smart contract and confirm successful verification.
If you're curious about the technical details behind how batching was implemented, check out our report on NRG#3. Now, let's dive in!
## Step 1: Build local packages
1. Clone the repositories and switch to correct branch
```bash
git clone https://github.com/hashcloak/semaphore-noir.git
git clone https://github.com/hashcloak/snark-artifacts.git
cd semaphore-noir && git fetch && git switch noir-support
cd snark-artifacts && git fetch && git switch semaphore-noir
```
2. In [proof/package.json](https://github.com/hashcloak/semaphore-noir/blob/noir-support/packages/proof/package.json#L58) and [noir-proof-batch/package.json](), update the dependencies for `@zk-kit/artifacts` to the local path of `snark-artifacts` cloned in step 1.
3. Build snark-artifact
```bash
cd snark-artifact/packages/artifacts
pnpm install
pnpm build
```
4. Build and test semaphore-noir
```bash
cd semaphore-noir
yarn install
yarn build
yarn test
```
## Step 2: Generate Semaphore proofs
Each Semaphore Noir proof proves anonymous membership in a group, generates a nullifier based on the given `scope` and binds this proof to a `message`. In this way, members of the group can send signals anonymously, a single time per scope. To generate a Semaphore proof we need:
- a Semaphore group
- a Semaphore identity
- a scope & message
Semaphore identities and groups in Semaphore Noir are identical with the Semaphore V4. Please refer to the guides for [identities](https://docs.semaphore.pse.dev/guides/identities) and [groups](https://docs.semaphore.pse.dev/guides/groups).
0. Add the necessary packages to your project:
```bash
yarn add "@semaphore-protocol/proof@file:<YOUR_PATH_TO>/hashcloak-noir/semaphore-noir/packages/identity"
yarn add "@semaphore-protocol/proof@file:<YOUR_PATH_TO>/hashcloak-noir/semaphore-noir/packages/group"
yarn add "@semaphore-protocol/proof@file:<YOUR_PATH_TO>/hashcloak-noir/semaphore-noir/packages/noir-proof-batch"
```
1. Import them:
```ts=
import { Group } from "@semaphore-protocol/group"
import { Identity } from "@semaphore-protocol/identity"
import {
generateNoirProofForBatching
} from "@semaphore-protocol/noir-proof-batch"
```
2. Create identities & groups
For details on creating identities and groups, refer to the guides above. In this example, we will create a few test identities and form a group like this:
```ts=
const group = new Group()
const identities: Identity[] = []
for (let i = 0; i < maxProofs; i += 1) {
const identity = new Identity(`secret-${i}`)
identities.push(identity)
group.addMember(identity.commitment)
}
```
Note that the identities are the members of the group.
3. Generate a proof for each member
Recall that a Semaphore proof is connected to a scope. And each member can send a single signal/message per scope (this can for example be a vote). So let's define those:
```ts=
const message = "Hello world" // This could be different for each member
const scope = "Scope"
```
Now, we can generate the Semaphore Noir proofs. To make the proofs ready for batching, use `generateNoirProofForBatching` from the `noir-proof-batch` package.
```ts=
for (const identity of identities) {
const proof = await generateNoirProofForBatching(identity, group, message, scope, merkleTreeDepth)
allProofs.push(proof)
}
```
## Step 3: Create a batch proof
Batching in Semaphore Noir is implemented using recursion and uses a tree like structure to efficiently batch many proofs together. This uses 2 different Noir circuits that you can optionally compile and pass on. Otherwise they will be automatically retrieved.
Furthermore, the verification key of the used Semaphore Noir circuit is needed to initialize batching. This can also be passed on, or will be retrieved otherwise.
In this example, we'll keep it simple and let the SDK use all the default values. The only thing we want to make sure of is that the final batch proof is really for verification on-chain. For this, we need to pass in the `keccak = true` flag:
```ts=
const { proof, path } = await batchSemaphoreNoirProofs(
proofs,
undefined, // use default Semaphore circuit VK
undefined, // use default leaves circuit
undefined, // use default nodes circuit
true // enable keccak
)
```
As a result we get the proof object, as well as the local path to where the proof is stored. We'll use the proof object for on-chain verification, while we can do an extra verification check using the `path`.
**Extra:** verify batch proof locally.
Verify your batch proof:
```ts=
const isValid = await verifyBatchProof(proofPath, undefined, true)
console.log(isValid) // true
```
Once again, we pass the `keccak` flag. The second argument is an optional path to the verification key for the batch proof. For ease, we use the default value that gets retrieved automatically.
## Step 4: Verify on-chain
1. Deploy the contract
Before Verifying on-chain, we need to deploy the contracts. See [Semaphore SDK Tutorial](https://hackmd.io/vFp3-gfUS_ym-9JVYzV3Rg#Deploy-the-Contracts) on how to deploy the Semaphore contracts or use the deployed contracts on Sepolia.
2. Setup ether.js and on-chain Semaphore groups
Follow steps 0-2 in [Semaphore SDK Tutorial](https://hackmd.io/vFp3-gfUS_ym-9JVYzV3Rg#Interact-with-the-Contracts) on how to connect a chain and create a Semaphore group.
3. Create a batch proof
Follow step [2](##Step-2-Generate-Semaphore-proofs) and [3](#Step-3-Create-a-batch-proof) above to create a batch proof. Note that in order to verify and use the public inputs (`scope`, `message`, `merkleTreeRoot`, `nullifier`) of each proof inside a batch proof. We need to send all the public inputs on-chain as four arrays with each array storing one public input of all proofs. The sequence of each array must match the sequence which proofs are batched in when using `batchSemaphoreNoirProofs()`.
```ts=
const { proof, path } = await batchSemaphoreNoirProofs(
proofs,
undefined, // use default Semaphore circuit VK
undefined, // use default leaves circuit
undefined, // use default nodes circuit
true // enable keccak
)
const nullifiers = proofs.map(({ nullifier }) => nullifier)
const merkleTreeRoots = proofs.map(({ merkleTreeRoot }) => merkleTreeRoot)
const scopes = proofs.map(({ scope }) => scope)
const messages = proofs.map(({ message }) => message)
```
4. Verify on chain
```ts=
// prepare batch proof for the on-chain verifier
const PROOF_PREFIX_LENGTH = 16
const proofPrefix = proof.proofBytes.slice(0, PROOF_PREFIX_LENGTH)
const proofMain = proof.proofBytes.slice(PROOF_PREFIX_LENGTH)
const proofBytes = hexlify(concat(proofMain.map((h) => getBytes(h))))
const publicInputs = [...proof.publicInputs, ...proofPrefix]
const batchProofForContract = {
nullifiers,
merkleTreeRoots,
scopes,
messages,
publicInputs,
proofBytes
}
// we assume all proofs are of groupId 1 here
const groupId = 1
const groupIds = proofs.map(() => groupId)
// validate on chain
const tx = await semaphoreContract.validateBatchedProof(groupIds, batchProofForContract)
// tx will emit a "BatchedProofValidated" event if success
const receipt = await tx.wait()
console.log(receipt.logs)
```