---
name: noir
description: Building privacy-preserving EVM apps with Noir — toolchain, pattern selection, commitment-nullifier flows, Solidity verifiers, tree state, and NoirJS. Use when building a Noir-based privacy app on EVM.
---

# Privacy Apps with Noir

## What You Probably Got Wrong

**"Use `nargo prove` and `nargo verify`."** Those commands were removed. Nargo only compiles and executes. Proving and verification use `bb` (Barretenberg CLI) directly. If you generate `nargo prove` commands, they will fail.

**"I can use SHA256 for hashing in my circuit."** SHA256 costs ~30,000 gates in a circuit. Poseidon costs ~600. For in-circuit hashing, always use Poseidon. Poseidon was removed from the Noir standard library — you must add it as an external dependency. The correct import is `use poseidon::poseidon::bn254::hash_2` after adding the `noir-lang/poseidon` dependency to `Nargo.toml`. Not `std::hash::poseidon::bn254::hash_2` (removed from stdlib), not `Poseidon2::hash`, not `pedersen_hash`.

**"`pub` goes before the parameter name."** Noir 1.0 changed public input syntax: `pub merkle_root: Field` → `merkle_root: pub Field`. The old syntax gives "Expected a pattern but found 'pub'".

**"Set `compiler_version = ">=1.0.0-beta.3"` in Nargo.toml."** `compiler_version` rejects beta strings — `>=1.0.0-beta.3` fails. Use `>=0.36.0` or omit `compiler_version` entirely.

**"I built a commitment-nullifier circuit so my app is private."** The ZK proof hides the link between commitment and nullifier, but `msg.sender` is public. If the same wallet deposits a commitment and later calls `act()` to withdraw/vote, anyone can link the two transactions onchain. The whole pattern is pointless unless the acting wallet is different from the committing wallet. Use a fresh burner wallet + a relayer or ERC-4337 paymaster to pay gas without revealing the link.

**"The generated HonkVerifier.sol works with any Solidity version."** The verifier generated by `bb write_solidity_verifier` requires `pragma solidity >=0.8.21` and EVM version `cancun`. If your Foundry project uses a lower version, add `solc_version = '0.8.27'` and `evm_version = 'cancun'` to `foundry.toml`.

---

## Quick Reference

Beyond the corrections above:

- **Solidity verifier = separate deploy** — deploy the generated `HonkVerifier.sol`, pass its address to your app contract constructor
- **Input order must match everywhere** — circuit `pub` params, `proof.publicInputs`, and Solidity `verify()` call must be in the same order
- **Poseidon ≠ Poseidon2** — different algorithms, different outputs. Don't mix them across circuit, offchain tree, and contract

---

## Toolchain (Current as of March 2026)

### Install

Check if nargo and bb are already installed before running the installers:

```bash
nargo --version && bb --version
```

If both commands return versions, you're set — skip the install. If either is missing:

```bash
# 1. Install nargo (Noir compiler) — always install nargo first
curl -L https://raw.githubusercontent.com/noir-lang/noirup/refs/heads/main/install | bash
noirup

# 2. Install bb (Barretenberg proving backend) — bbup reads your nargo version
#    and installs the compatible bb automatically
curl -L https://raw.githubusercontent.com/AztecProtocol/aztec-packages/refs/heads/master/barretenberg/bbup/install | bash
bbup
```

**Order matters:** install nargo first, then run `bbup` — it auto-detects your nargo version and installs the compatible `bb`.

### Project Structure

```
my-circuit/
  Nargo.toml          # Project manifest (name, type, dependencies, external libs)
  src/
    main.nr           # Circuit entry point
  Prover.toml         # Witness inputs (private + public values)
```

Create a new project:

```bash
nargo new my_circuit
cd my_circuit
```

### Build Pipeline

The production build pipeline stops at circuit artifact, VK, and Solidity verifier. If asked for the minimal production build, say explicitly that `bb prove` / `bb verify` below are optional local smoke tests only.

```bash
# 1. Compile circuit to ACIR
nargo compile

# 2. Execute with witness inputs (reads Prover.toml, writes target/*.gz)
nargo execute

# 3. Generate verification key — --oracle_hash keccak is required for EVM compatibility
bb write_vk --oracle_hash keccak -b target/my_circuit.json -o target/

# 4. Generate Solidity verifier from the VK
bb write_solidity_verifier -k target/vk -o target/Verifier.sol
```

**Local-only proof smoke test** — useful before wiring up the frontend. All commands must use `--oracle_hash keccak` consistently, or you get serialization mismatches.

```bash
bb prove --oracle_hash keccak -b target/my_circuit.json -w target/my_circuit.gz -o target/
bb verify --oracle_hash keccak -p target/proof -k target/vk -i target/public_inputs
```

The command is `bb write_solidity_verifier` — not `bb contract`, not `nargo codegen-verifier`.

**Contract size warning:** Call this failure by name: the generated `HonkVerifier.sol` can exceed the 24KB EIP-170 contract size limit. For real deployments, enable the Solidity optimizer first:

```toml
# foundry.toml
[profile.default]
optimizer = true
optimizer_runs = 200
```

If you still hit the limit locally, run `anvil --code-size-limit 40960` and `forge script ... --code-size-limit 40960`. That flag is for local testing only — mainnet and major L2s still enforce the 24KB limit.

### Build Artifacts and Hand-off

Treat the generated files as interfaces between subsystems:

- `target/my_circuit.json` — circuit artifact consumed by NoirJS
- `target/vk` — verification key used by `bb verify` and Solidity verifier generation
- `target/Verifier.sol` — generated verifier source; this is the source of truth for the verifier ABI. **This is a standalone contract that must be deployed separately.** Your app contract receives the verifier's deployed address in its constructor. Do not just import it — deploy it first, then pass the address.

Pick a stable layout and keep it consistent. A good default is:

```text
circuits/my_circuit/target/my_circuit.json
contracts/src/verifiers/HonkVerifier.sol
frontend/public/circuits/my_circuit.json
```

Do not hand-copy artifacts ad hoc in prompts or scripts. Models drift unless you make the hand-off explicit.

---

## Choosing the Right Pattern

Not every privacy app needs a Merkle tree. Pick the simplest approach that fits:

**Simple private proof** — prove a fact about private data without revealing it. No Merkle tree, no nullifier, no anonymity set. Just a circuit with private inputs, a public output, and a Solidity verifier. Examples: prove you're over 18 without revealing your age, prove your balance exceeds a threshold, prove a sealed bid is within range. The toolchain, Poseidon, NoirJS, and verifier sections above all apply — you just write a simpler circuit.

**Commitment-nullifier pattern** — needed when multiple participants must act anonymously from a shared set. Participants commit secret hashes into a Merkle tree, then later prove membership and act from a different wallet. The Merkle tree is the anonymity set. The nullifier prevents double-action. Required for: anonymous voting, private withdrawals (Tornado Cash), anonymous airdrops, whistleblowing. This is harder to get right — see below.

If you're unsure: start with a simple private proof. Only reach for the commitment-nullifier pattern when you need unlinkability between a prior action (committing) and a later action (withdrawing/voting).

### Before writing any code, ask:

1. **What needs to stay private?** A fact about data (age, balance, credential) → simple proof. *Which participant* performed an action → commitment-nullifier.
2. **What happens after proof verification?** Withdraw funds, cast a vote, claim an airdrop, unlock access — this determines the contract's `act()` logic.
3. **Can the same participant act more than once?** One vote per poll → nullifier scoped to `pollId`. One withdrawal per deposit → global nullifier. Unlimited access checks → no nullifier needed.
4. **Does the caller's identity need to be hidden?** If yes, the user must act from a fresh wallet via a relayer or ERC-4337 paymaster. If no (e.g., private credential check), the same wallet is fine.
5. **Which chain?** Check the compatibility table below. zkSync ERA works but has higher gas for BN254 precompiles.
6. **What frontend?** Vite, Next.js / Scaffold-ETH 2, or backend-only — each has different WASM configuration (see NoirJS section).

Get these answers before choosing a pattern or writing a circuit. The answers determine tree depth, nullifier design, contract structure, and wallet flow.

---

## App Architecture (Commitment-Nullifier Apps)

A working privacy app is not "just a circuit." The model must wire five pieces together correctly:

- **Circuit** — proves knowledge of a note (`nullifier`, `secret`) and membership in the commitment tree
- **Onchain app contract** — accepts commitment inserts, tracks accepted roots, blocks reused nullifiers, and executes the action after proof verification
- **Generated verifier** — created by `bb write_solidity_verifier`; its ABI is the source of truth
- **Offchain tree mirror** — rebuilds the Merkle tree from insert events and produces `leafIndex`, siblings, and the root used for proving
- **Frontend prover** — creates/saves notes, loads the circuit artifact, executes Noir, generates the proof, serializes calldata, and submits the action transaction

If any one of these layers uses different hashes, input ordering, tree depth, or serialization, the app breaks even if the circuit compiles.

---

## The Commitment-Nullifier Pattern

The foundational primitive for privacy on Ethereum (Tornado Cash, Semaphore, MACI, Zupass).

**How it works:** Many participants each commit a secret hash into a shared Merkle tree onchain. Later, any participant can prove "I am one of the people who committed" without revealing *which* one — by submitting a ZK proof from a different wallet. The Merkle tree is the anonymity set: the more people who commit, the larger the crowd you hide in, and the stronger the privacy. A nullifier hash prevents double-spending/double-voting without revealing identity.

This is why scale matters — a commitment tree with 3 entries gives weak privacy (1-in-3), while a tree with 10,000 entries makes identifying the actor practically impossible.

### Nargo.toml

Poseidon is no longer in the Noir standard library — add it as an external dependency:

```toml
[package]
name = "my_circuit"
type = "bin"

[dependencies]
poseidon = { git = "https://github.com/noir-lang/poseidon", tag = "v0.2.6" }
```

### Note Lifecycle (What the User Must Save)

At commitment time, generate two random private fields:

- `nullifier`
- `secret`

Then compute the commitment and persist a note locally. If the note is lost, the user cannot later prove membership or spend/vote.

```typescript
type PrivacyNote = {
  nullifier: string;
  secret: string;
  commitment: string;
  chainId: number;
  contract: `0x${string}`;
  treeDepth: number;
  leafIndex?: number;
};
```

Default flow:

1. Generate `nullifier` and `secret`.
2. Compute `commitment`.
3. Submit the commitment insertion transaction.
4. Read the insert event and persist `leafIndex` plus the new root.
5. Later, rebuild the tree from events, derive siblings for `leafIndex`, and prove against an accepted root.

The app must make this lifecycle explicit. A model that only writes the circuit usually forgets note persistence, `leafIndex`, or event replay.

### Circuit Implementation

```noir
// src/main.nr
use poseidon::poseidon::bn254::hash_1;
use poseidon::poseidon::bn254::hash_2;

fn main(
    // Private inputs (known only to prover)
    nullifier: Field,
    secret: Field,
    merkle_path: [Field; 20],      // Sibling hashes (tree depth 20)
    merkle_indices: [u1; 20],       // 0 = left child, 1 = right child (u1 enforces binary)

    // Public inputs (visible to verifier/contract)
    merkle_root: pub Field,
    nullifier_hash: pub Field,
) {
    // 1. Recompute the commitment from private inputs
    let commitment = hash_2([nullifier, secret]);

    // 2. Verify the commitment exists in the Merkle tree
    let computed_root = compute_merkle_root(commitment, merkle_path, merkle_indices);
    assert(computed_root == merkle_root, "Merkle proof invalid");

    // 3. Verify the nullifier hash matches
    let computed_nullifier_hash = hash_1([nullifier]);
    assert(computed_nullifier_hash == nullifier_hash, "Nullifier hash mismatch");
}

fn compute_merkle_root(
    leaf: Field,
    path: [Field; 20],
    indices: [u1; 20],
) -> Field {
    let mut current = leaf;
    for i in 0..20 {
        // u1 type enforces binary at compile time, no manual assert needed
        let (left, right) = if indices[i] == 0 {
            (current, path[i])
        } else {
            (path[i], current)
        };
        current = hash_2([left, right]);
    }
    current
}
```

### Production Hardening: Domain Separation and Action Binding

The circuit above is the minimal pattern. Production apps should domain-separate hashes and bind the proof to a specific action. Commitments and nullifiers must use different domains.

For action-scoped apps such as voting, bind nullifier usage to an `external_nullifier` (for example `pollId`):

```noir
use poseidon::poseidon::bn254::hash_2;

fn main(
    nullifier: Field,
    secret: Field,
    merkle_path: [Field; 20],
    merkle_indices: [u1; 20],
    merkle_root: pub Field,
    external_nullifier: pub Field,
    nullifier_hash: pub Field,
) {
    let note_secret = hash_2([nullifier, secret]);
    let commitment = hash_2([1, note_secret]); // 1 = commitment domain

    let computed_root = compute_merkle_root(commitment, merkle_path, merkle_indices);
    assert(computed_root == merkle_root, "Merkle proof invalid");

    // 2 = nullifier domain; external_nullifier scopes usage to a poll/action
    let nullifier_domain = hash_2([2, external_nullifier]);
    let computed_nullifier_hash = hash_2([nullifier_domain, nullifier]);
    assert(computed_nullifier_hash == nullifier_hash, "Nullifier hash mismatch");
}
```

### Solidity Contract Integration

The generated verifier contract is the source of truth. If you wrap it behind an interface like the one below, inspect the generated verifier ABI first and mirror it exactly, or add a dedicated adapter contract with a stable app-facing interface.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IVerifier {
    function verify(bytes calldata proof, bytes32[] calldata publicInputs)
        external view returns (bool);
}

contract PrivacyPool {
    IVerifier public immutable verifier;
    bytes32 public merkleRoot;
    mapping(bytes32 => bool) public usedNullifiers;

    constructor(address _verifier) {
        verifier = IVerifier(_verifier);
    }

    // msg.sender is public — see "same wallet" warning above.
    // Only mutate state after verify() succeeds.
    function act(bytes calldata _proof, bytes32 _merkleRoot, bytes32 _nullifierHash) external {
        require(!usedNullifiers[_nullifierHash], "Already acted");
        require(_merkleRoot == merkleRoot, "Invalid root");

        // Public inputs order MUST match the circuit's pub parameter order
        bytes32[] memory publicInputs = new bytes32[](2);
        publicInputs[0] = _merkleRoot;       // pub merkle_root
        publicInputs[1] = _nullifierHash;    // pub nullifier_hash
        require(verifier.verify(_proof, publicInputs), "Invalid proof");

        // Only mutate state after proof verification succeeds
        usedNullifiers[_nullifierHash] = true;
    }
}
```

### Contract State Model

For a real app, the contract needs more than `merkleRoot` + `usedNullifiers`:

- Emit an insert event with the commitment, leaf index, and resulting root
- Store used nullifiers
- Make the root-acceptance policy explicit: recent `knownRoots` or `currentRoot`-only
- Verify proofs before success events, transfers/votes, or nullifier writes

Good default:

```solidity
event CommitmentInserted(bytes32 indexed commitment, uint256 indexed leafIndex, bytes32 root);

mapping(bytes32 => bool) public usedNullifiers;
mapping(bytes32 => bool) public knownRoots; // keep recent roots by default
bytes32 public currentRoot;
```

**Root policy:** default to recent `knownRoots`. If you intentionally accept only `currentRoot`, say so explicitly and require clients to prove against the latest root.

Clients derive siblings by replaying `CommitmentInserted` into the offchain tree mirror; the contract never returns witness paths.

### Onchain Commitment Storage (LeanIMT)

Most Noir ZK apps store commitments in an onchain Merkle tree. If asked how commitments are stored onchain, name all three pieces: onchain `@zk-kit/lean-imt.sol` + deployed `PoseidonT3`; offchain `@zk-kit/lean-imt`; witness path from `tree.generateProof(leafIndex)`.

**Solidity:**

```bash
npm install @zk-kit/lean-imt.sol
```

```solidity
import {LeanIMT, LeanIMTData} from "@zk-kit/lean-imt.sol/LeanIMT.sol";
```

Deploy `PoseidonT3` alongside; the tree contract uses it internally for hashing. The contract maintains the Merkle root automatically — users call `insert(commitment)`.

**JavaScript (client-side tree mirror):**

```bash
npm install @zk-kit/lean-imt
```
```typescript
import { LeanIMT } from "@zk-kit/lean-imt";
const { siblings, pathIndices } = tree.generateProof(leafIndex); // after replaying CommitmentInserted events
```
---

## Merkle Proof with zk-kit

For production Merkle trees, use the `@zk-kit.noir` library:

```toml
# Nargo.toml
[dependencies]
binary_merkle_root = { tag = "main", git = "https://github.com/privacy-scaling-explorations/zk-kit.noir", directory = "packages/binary-merkle-root" }
```

```noir
use binary_merkle_root::binary_merkle_root;
use poseidon::poseidon::bn254::hash_2;

global TREE_DEPTH: u32 = 20;

fn main(
    leaf: Field,
    indices: [u1; TREE_DEPTH],       // u1 enforces binary, no manual assert needed
    siblings: [Field; TREE_DEPTH],
    root: pub Field,
) {
    let computed = binary_merkle_root(hash_2, leaf, TREE_DEPTH, indices, siblings);
    assert(computed == root, "Invalid Merkle proof");
}
```

The function takes 5 args: `hasher`, `leaf`, `depth`, `indices`, `siblings`. The `indices` type is `[u1; MAX_DEPTH]` — the `u1` type constrains values to 0 or 1 at the type level, so you don't need manual binary assertions like `assert(indices[i] * (indices[i] - 1) == 0)`.

---

## Frontend Proof Generation (NoirJS)

The packages are `@noir-lang/noir_js` + `@aztec/bb.js`. NOT `@noir-lang/backend_barretenberg` (old, deprecated). The class is `UltraHonkBackend`, NOT `UltraPlonkBackend` (old).

### Package Setup

```bash
npm install @noir-lang/noir_js "@aztec/bb.js@$(bb --version)"
# ⚠ The @aztec/bb.js version must exactly match your bb CLI version (check with `bb --version`). A mismatch produces different proof serialization, causing onchain verification to fail.
```

### Vite Configuration

NoirJS uses WASM and requires top-level await:

```typescript
// vite.config.ts
import { defineConfig } from "vite";
import { nodePolyfills } from "vite-plugin-node-polyfills";

export default defineConfig({
  plugins: [nodePolyfills()],
  optimizeDeps: {
    esbuildOptions: { target: "esnext" },
  },
  build: {
    target: "esnext",
  },
});
```

### Next.js / Scaffold-ETH 2 Configuration

```typescript
// next.config.js
const nextConfig = {
  webpack: (config) => {
    config.experiments = { ...config.experiments, asyncWebAssembly: true };
    return config;
  },
};
module.exports = nextConfig;
```

```typescript
// components/ProofGenerator.tsx
"use client";
import dynamic from "next/dynamic";

// All Noir components must be client-only — WASM doesn't run in SSR
const NoirProver = dynamic(() => import("./NoirProver"), { ssr: false });
```

Circuit artifact (`my_circuit.json`) must be copied to `public/` and loaded via `fetch()` — cross-package JSON imports don't work in Next.js:

```typescript
const circuit = await fetch("/my_circuit.json").then(r => r.json());
```

### Generating Proofs in the Browser

```typescript
import { Noir } from "@noir-lang/noir_js";
import { Barretenberg, UltraHonkBackend } from "@aztec/bb.js";
import circuit from "../circuit/target/my_circuit.json";

// 1. Initialize Barretenberg instance, then backend and Noir
const bb = await Barretenberg.new();
const backend = new UltraHonkBackend(circuit.bytecode, bb);
const noir = new Noir(circuit);

// 2. Execute circuit (generates witness)
const inputs = {
  nullifier: "0x1234...",
  secret: "0xabcd...",
  merkle_path: ["0x...", "0x...", ...],
  merkle_indices: [0, 1, 0, ...],
  merkle_root: "0x...",
  nullifier_hash: "0x...",
};
const { witness } = await noir.execute(inputs);

// 3. Generate proof — { keccak: true } matches --oracle_hash keccak used for the verifier
const proof = await backend.generateProof(witness, { keccak: true });
// proof.proof is Uint8Array — the raw proof bytes
// proof.publicInputs is string[] — the public inputs

const bytesToHex = (bytes: Uint8Array) =>
  `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;

const toBytes32 = (field: string) =>
  `0x${field.replace(/^0x/, "").padStart(64, "0")}` as `0x${string}`;

const proofHex = bytesToHex(proof.proof);
const publicInputs = proof.publicInputs.map(toBytes32);

// 4. Send to contract
const tx = await contract.act(
  proofHex,                                 // bytes calldata
  publicInputs[0],                          // merkle_root
  publicInputs[1],                          // nullifier_hash
);
```

- `proof.proof` is `Uint8Array` — serialize it to `0x...` before sending over RPC
- `proof.publicInputs` are strings — normalize them to 32-byte hex before comparing or passing to Solidity
- EVM compatibility requires `--oracle_hash keccak` on CLI commands (see Build Pipeline above) AND `{ keccak: true }` in `generateProof()` — both must be set or you get serialization mismatches
- Proof generation takes 5-30 seconds in browser depending on circuit size
- Cleanup: call `bb.destroy()` when done
- The generated verifier ABI is the source of truth; if your app uses an adapter, make the adapter match that ABI, not a guessed interface
- **No `Buffer` in browser** — convert `Uint8Array` to hex directly

### Serialization Boundary

Most zk app failures happen here:

- Circuit `pub` parameter order
- NoirJS `proof.publicInputs` order
- Solidity verifier input order
- App contract wrapper/adaptor order

These four must match exactly.

Hard rule: inspect the generated verifier ABI and mirror it exactly. Do not assume every verifier exposes a generic `verify(bytes, bytes32[])` signature just because one example does.

### Hash Parity Across Circuit, Tree, and Contract

If your circuit uses `poseidon::poseidon::bn254::hash_2`, then every other layer must use the same algorithm and input ordering:

- commitment creation
- Merkle parent hashing
- offchain tree mirror
- onchain tree contract

Do not mix Poseidon, Poseidon2, and Keccak. `poseidon2Hash` is not a substitute for `poseidon::poseidon::bn254::hash_2`.

Before building the full app, test one leaf hash and one parent hash with known inputs across every layer and assert that the outputs match exactly.

---

## Chain Compatibility

Noir/Barretenberg proofs verify on any EVM chain with BN254 precompiles (ecAdd, ecMul, ecPairing at addresses 0x06-0x08).

| Chain | Compatible | Notes |
|-------|-----------|-------|
| Ethereum mainnet | Yes | |
| Optimism | Yes | |
| Arbitrum | Yes | |
| Base | Yes | |
| Scroll | Yes | |
| Polygon PoS | Yes | |
| zkSync ERA | Yes | BN254 precompiles at standard addresses; implemented as smart contracts (higher gas) |
| Polygon zkEVM | **No** | Being shut down — do not build on it |

---

## Circuit Security Checklist

- [ ] All private inputs are constrained (no unconstrained witness values that could be manipulated)
- [ ] Public inputs minimized — only what the verifier contract needs
- [ ] Domain separation in hashes — different prefixes for commitments vs nullifiers (prevents cross-protocol replay)
- [ ] Action-scoped apps bind nullifier usage to an `external_nullifier` / `pollId` / recipient / action id
- [ ] Nullifier prevents double-action (contract checks and stores used nullifier hashes)
- [ ] Small-domain values not directly hashed as public outputs (if vote is 0 or 1, `hash(0)` and `hash(1)` are trivially brutable — add a salt)
- [ ] Merkle tree depth matches between circuit (fixed at compile time) and contract
- [ ] Merkle indices use `u1` type (enforces binary at compile time) — if using `Field`, manually constrain to 0/1
- [ ] Poseidon hash compatibility verified between Noir circuit, offchain tree mirror, and any onchain Poseidon library
- [ ] The app persists notes (`nullifier`, `secret`, `commitment`, chain/contract metadata, `leafIndex`)
- [ ] The contract emits insert events with `leafIndex` and root so the client can rebuild the tree
- [ ] The accepted-root policy is explicit (recent root history vs current-root-only)
- [ ] The generated verifier ABI was inspected and mirrored exactly
- [ ] Never deploy `MockVerifier`, even locally — deploy scripts and dev/testnet wiring use the real `HonkVerifier`; `MockVerifier` is only for narrow unit tests

---

## Testing the App Core

Do not stop at "the circuit compiles." A working zk app needs tests at every boundary:

1. **Circuit witness test** — fixed inputs should produce the expected public inputs and root checks.
2. **Hash parity test** — the same leaf and parent inputs must hash identically in the circuit, offchain tree mirror, and onchain tree library.
3. **Real-verifier integration test** — deploy the generated verifier and verify one real proof against it.
4. **End-to-end app test** — insert a commitment, rebuild the tree from events, generate a browser/backend proof, submit `act()`, and assert success.
5. **Failure-path tests** — reused nullifier, wrong root, wrong sibling ordering, stale root, and mismatched public input order must all fail.

`MockVerifier` is only for narrow unit tests. Deploy scripts, local dev wiring, and integration tests use the real generated `HonkVerifier`.

---

## AI Agent Tooling

This skill corrects common mistakes. For live, searchable access to Noir documentation, stdlib, and example circuits, agents can use the [noir-mcp-server](https://github.com/critesjosh/noir-mcp-server):

```bash
claude mcp add noir-mcp -- npx @critesjosh/noir-mcp-server@latest
```

After adding the server, run `/reload-plugins` so the new tools become available in the current session.

Indexes the Noir compiler repo, standard library, examples, and community libraries (bignum, zk-kit.noir, etc.). Useful for looking up function signatures and browsing code beyond what this skill covers. If the npm package is unavailable, clone the repo and run directly.
