# Privacy Pools V2 — Trusted Setup Ceremony
A browser-based **Groth16 Phase-2 trusted setup ceremony** for the Privacy Pools V2
zero-knowledge circuits. Anyone can contribute fresh randomness from their browser;
the app coordinates the queue, stores the proving keys, verifies every contribution,
and — once finished — publishes everything you need to check the result yourself.
> **The one thing to remember about a trusted setup:** it is secure as long as **at
> least one** participant was honest and destroyed their secret randomness. You do not
> have to trust the organizers, the other contributors, or this website — you only
> have to trust *yourself*, or anyone else who contributed honestly. This document
> explains how to contribute, and how to verify the whole thing independently.
---
## Contents
- [What this is](#what-this-is)
- [How it works](#how-it-works)
- [The circuits](#the-circuits)
- [Contributing](#contributing)
- [Verifying the ceremony yourself](#verifying-the-ceremony-yourself) ← the important part
- [Published artifacts reference](#published-artifacts-reference)
- [Running your own deployment (operators)](#running-your-own-deployment-operators)
- [Configuration](#configuration)
- [Trust model & guarantees](#trust-model--guarantees)
---
## What this is
Privacy Pools V2 uses Groth16 zk-SNARKs. Groth16 needs a **circuit-specific
proving key** (a `.zkey`), and generating that key involves secret randomness (often
called *toxic waste*). If a single party generated the key alone and kept that
randomness, they could forge proofs — e.g. withdraw funds that were never deposited.
A **Phase-2 trusted setup ceremony** removes that risk by having many independent
people each fold their own secret randomness into the key, one after another, and then
destroy it. The final key is safe unless **every single** contributor colluded or was
compromised. One honest contributor is enough.
This app runs that ceremony for all **27 Privacy Pools V2 circuits** entirely in the
browser:
- **Your randomness never leaves your device.** The contribution is computed in a
WebAssembly worker in your browser. Only the transformed proving key is uploaded —
the secret is zeroed from memory immediately and never written to disk or sent to the
coordinator.
- **Every contribution is verified** on the server before it is accepted, and the
entire chain is re-verified from the original parameters at finalization.
- **The result is publicly checkable.** Finalization applies a public, unpredictable
beacon (Ethereum beacon-chain randomness) and publishes a transcript, the
verification keys, and the final proving keys so anyone can reproduce the checks.
---
## How it works
```
┌─────────────────────────────────────────────┐
Genesis zkey ───▶│ contribution 1 ▶ contribution 2 ▶ … ▶ N │──▶ + Beacon ──▶ Final zkey
(pinned, hash │ (each contributor folds in secret entropy, │ (public (+ vkey,
published) │ server verifies, hash recorded in a chain) │ Ethereum transcript)
└─────────────────────────────────────────────┘ RANDAO)
```
1. **Genesis.** The operator runs the Phase-1 → Phase-2 transition once, producing a
*genesis* `.zkey` per circuit from the circuit's `.r1cs` and a Perpetual Powers of
Tau (`.ptau`) file. The genesis is hashed and that hash is **published externally**
so it can never be swapped later.
2. **Contributions.** Each contributor downloads the current `.zkey`, verifies its
hash, folds in randomness derived from their own device, and uploads the result. The
server verifies the new key genuinely extends the chain and records the
contribution's hash in a running **hash chain**.
3. **Finalization.** When the target is reached (or the deadline passes), the operator
applies a **beacon** — by default the RANDAO reveal from a finalized Ethereum
beacon-chain slot, which is public and was unpredictable in advance. Before doing so
the server re-verifies the whole chain from the pinned genesis and re-walks it
against the recorded hashes. It then exports each circuit's **verification key** and
**final proving key**, plus a **transcript** of the entire ceremony.
---
## The circuits
The ceremony covers 27 circuits. Contributors contribute to every circuit in sequence
(largest circuits take the most time).
| Circuit | What it proves |
| --- | --- |
| `deposit` | Deposit funds into the privacy pool. |
| `ragequit` | Exit the pool and withdraw your full deposit. |
| `transact_NxM` (N,M ∈ 1..5) | A private transfer with **N** input notes and **M** output notes — 25 variants, from `transact_1x1` to `transact_5x5`. |
Larger `N×M` circuits have more constraints and take longer to contribute to. Exact
constraint counts are listed in [`ceremony.config.ts`](./ceremony.config.ts).
---
## Contributing
**Live ceremony:** https://ceremony.privacypools.com
### Requirements
- A **desktop browser** (the contribution runs a heavy WASM computation; mobile is not
supported).
- A **GitHub account** — sign-in is required purely to prevent spam and to give each
participant a place in the queue. No other data is collected.
- **Time and a device that stays awake.** Contributing to all 27 circuits runs entirely
in your browser and takes anywhere from ~15 minutes to well over an hour depending on
your hardware and connection (see [How long it takes](#how-long-it-takes)). Keep the
tab open and foregrounded until it finishes.
### What happens, step by step
1. **Sign in with GitHub** and join the queue.
2. **Collect entropy.** Move your mouse / tap around. Your input is mixed into
cryptographic randomness that seeds your contribution.
3. **Contribute.** For each circuit the app downloads the current key, verifies its
hash, computes your contribution in a background worker, uploads it, and waits for
the server to verify it.
4. **Get your receipt.** When you finish, you receive a receipt for each circuit and
can download it as JSON. Optionally, publish a one-click **attestation** (see below).
### How long it takes
Plan for it to take a while — **this is normal and expected.** For each of the 27
circuits your browser (1) downloads the current proving key, (2) computes your
contribution, and (3) uploads the result. The larger circuits (the `transact_5x5` end)
have bigger keys, so both the transfer and the computation grow. Total time is dominated
by your **CPU** (the compute) and your **connection speed** (the up/downloads).
| Machine | Connection | Approx. total time |
| --- | --- | --- |
| Apple M4 Max | ~1 Gbps | ~15 min |
| Recent laptop (e.g. M2/M3 Pro) | ~300 Mbps | ~35 min* |
| Apple M1 Max | ~100 Mbps | ~1 h 15 min |
*Interpolated. These are rough figures from limited early runs — treat them as a
guide, not a guarantee. A slower CPU or connection simply takes longer; nothing is
wrong.
There is nothing to babysit: the app shows per-circuit progress and an updating time
estimate, and it automatically retries transient network hiccups. Just leave the tab
open (and your machine awake) and let it finish.
### What happens to your secret randomness
Your secret entropy is generated in memory, used to transform the keys, and then
**immediately zeroed**. It is never written to disk and never transmitted to the
coordinator — only the resulting proving key (which reveals nothing about your secret)
is uploaded. This is the "toxic waste destroyed" step shown on the completion screen.
### Your receipt
Each receipt records, for one circuit:
| Field | Meaning |
| --- | --- |
| `circuitId`, `contributionIndex` | Which circuit, and your position in its chain. |
| `serverContributionHash` | **Your contribution hash `h_k`** — the Blake2b hash that the ceremony chain folds over and that appears inside the final proving key. This is the value you look for when verifying that your contribution was included. |
| `contributionHash` | SHA-256 of the exact `.zkey` bytes you produced (a file integrity hash — distinct from `h_k`). |
| `previousContributionHash` | The head hash `h_{k-1}` your contribution extended. |
| `chainHash` | The running chain hash after your contribution: `SHA-256(previousChainHash ‖ h_k)`. |
### Attestation (optional)
From the completion screen you can **Publish as Gist**, which posts a small public
record to a GitHub Gist on your own account, timestamped with your login. It contains
your circuit, index, and your own `h_k`.
An attestation is **not a signature and not a vote.** It only lets *you* later point to
a public, timestamped record that your contribution happened — useful for detecting if
your contribution were ever censored (your `h_k` would be absent from the final key).
Because every `h_k` is public, anyone could publish a valid-looking attestation for
someone else's contribution, so **a count of "N attestations" is not evidence of N
independent honest participants.** Security comes from the verifiable hash chain and
the open verifier below, not from attestations.
---
## Verifying the ceremony yourself
**You do not have to trust this website.** Everything needed to independently verify the
final parameters is published. This section is the runbook. All checks use
[`snarkjs`](https://github.com/iden3/snarkjs) — the standard, widely-audited tool —
so you never have to run code from this project to trust its output.
### What you need
1. **`snarkjs`**, pinned to the version this ceremony used:
```bash
npm install -g snarkjs@0.7.5
```
2. **The published ceremony artifacts** (served from the ceremony site's web root):
- `https://ceremony.privacypools.com/finalize/transcript.json`
- `https://ceremony.privacypools.com/finalize/.final.zkey`
- `https://ceremony.privacypools.com/finalize/.vkey.json`
- `https://ceremony.privacypools.com/genesis/.genesis.zkey`
- `https://ceremony.privacypools.com/genesis/init-transcript.json`
3. **The circuit `.r1cs` files**, from the *audited* Privacy Pools V2 circuits release
(compile them yourself from the audited sources for maximum assurance — do not take
the r1cs from this app).
4. **The Powers of Tau (`.ptau`) file** the ceremony was built on. It uses the Privacy &
Scaling Explorations [Perpetual Powers of Tau](https://github.com/privacy-scaling-explorations/perpetualpowersoftau)
`pot28_0080` file, sized per circuit (`ppot_0080_.ptau`, where `` is the
smallest power of two that fits the circuit's constraints). `setup:ptau` downloads it
from the public PSE bucket; you can fetch the same file independently — its provenance
comes from the public PPoT ceremony, not from this deployment.
> The `.r1cs` and `.ptau` are the trust roots that come from *outside* this deployment.
> Getting them from the audited circuit release and the canonical PPoT — not from this
> app — is what makes the verification independent.
### Step 1 — The genesis is the one that was announced
The whole chain is only meaningful if it started from the genuine genesis. Confirm the
genesis key matches the hash **published externally at the start of the ceremony**
(e.g. in a public Git commit or announcement), and that the same hash appears in
`init-transcript.json`:
```bash
sha256sum deposit.genesis.zkey
# 0x-strip and compare against:
# - the externally announced genesis hash for `deposit`
# - init-transcript.json → circuits[].genesisZkeyHash
```
If these disagree, stop — the chain was not rooted in the announced parameters.
### Step 2 — Each final key is a valid Phase-2 key for its circuit
This is the core check. `snarkjs zkey verify` recomputes the entire Phase-2 chain from
the Powers of Tau through every contribution and the final beacon, and confirms the
final key is internally consistent:
```bash
snarkjs zkey verify deposit.r1cs powersOfTau_final.ptau deposit.final.zkey
# Expect: "ZKey Ok!"
```
It also prints the Blake2b hash of **every** contribution in the chain and the beacon —
keep this output for Step 4. Repeat for all 27 circuits.
### Step 3 — The beacon is public, unpredictable randomness
Finalization mixes in a beacon so that even if *every* contributor were malicious, the
final key still depends on a value nobody could control. By default the beacon is the
RANDAO reveal from a finalized Ethereum beacon-chain slot. Read it from the transcript:
```bash
jq '.ceremony | {beaconHash, beaconSource, beaconSlot}' transcript.json
```
Then fetch that slot's RANDAO reveal from any Ethereum beacon node and confirm it
matches `beaconHash`:
```bash
curl -s https://ethereum-beacon-api.publicnode.com/eth/v2/beacon/blocks/ \
| jq -r '.data.message.body.randao_reveal'
# strip the leading 0x and compare to transcript.json → ceremony.beaconHash
```
The same beacon value is embedded in each `*.final.zkey` and printed by the Step 2
`zkey verify` output, so you can confirm the published beacon is the one actually
applied. For the strongest guarantee, the operator announces the target slot number
*before* it is produced — check that announcement against `beaconSlot`.
### Step 4 — Your contribution is included
Find your own `h_k` — the `serverContributionHash` from your receipt (or the `h_k` in
your attestation Gist) — in the list of contribution hashes printed by `zkey verify` in
Step 2, or in the transcript:
```bash
jq '.receipts[] | select(.circuitId=="deposit") | {contributionIndex, serverContributionHash}' transcript.json
```
Because Step 2 proved the chain is valid and Step 3 proved the final beacon is public
randomness, your `h_k` appearing in the verified chain means your randomness is
permanently part of the final key.
### Step 5 — The verification key matches the final key (optional)
dApps embed the small `*.vkey.json`, not the large `.final.zkey`. Confirm the published
verification key is the one derived from the verified proving key:
```bash
snarkjs zkey export verificationkey deposit.final.zkey vkey_check.json
diff <(jq -S . vkey_check.json) <(jq -S . deposit.vkey.json) # expect no differences
```
### Step 6 — The recorded chain is self-consistent (optional)
You can independently recompute the chain hash from the receipts. Starting from
`chainHash₀ = 0x000…0` (32 zero bytes), for each contribution in order:
```
chainHashₖ = SHA-256( chainHashₖ₋₁ ‖ h_k ) # raw bytes, not text
```
The result after the last contribution must equal the circuit's `finalChainHash` in
`transcript.json`. This ties the published transcript to the same `h_k` values you
verified inside the final key.
### Quick check: verify a single receipt in the app
If you just want to confirm a receipt is recorded by the coordinator (a convenience
check, **not** a substitute for the independent verification above), use the **Verify**
screen on the ceremony site and paste the receipt JSON. It confirms the receipt's hash
matches the coordinator's record — it does not re-derive anything cryptographically.
---
## Published artifacts reference
Finalization writes to `public/finalize/` (served at the site's web root):
| File | Contents |
| --- | --- |
| `transcript.json` | Full ceremony record: beacon (hash, source, slot), and per circuit the `totalContributions`, `finalChainHash`, `finalContributionHash` (the beacon's hash), `finalZkeyHash`, and `verificationKey`; plus every contribution `receipt`. |
| `.vkey.json` | Groth16 verification key — embed this in verifiers/dApps. |
| `.final.zkey` | Finalized proving key. |
Initialization writes to `public/genesis/`:
| File | Contents |
| --- | --- |
| `init-transcript.json` | Genesis record: per circuit the `genesisZkeyHash`, `csHash` (circuit identity), sizes, and storage paths. |
| `.genesis.zkey` | The pinned genesis proving key each chain starts from. |
---
## Running your own deployment (operators)
Everything below is for whoever **operates** a ceremony (deploys the app, initializes
and finalizes it). Contributors and verifiers do not need this section.
### Prerequisites
- Node.js and `pnpm` (see `packageManager` in `package.json`).
- A Vercel account (Blob + KV storage) and a GitHub OAuth App.
### 1. Install
```bash
pnpm install
```
### 2. Add circuit files and download the ptau
Place your compiled `.r1cs` files in the `circuits/` folder, then run:
```bash
pnpm setup:ptau
```
This reads each circuit's constraint count, downloads the correct
[PPoT](https://github.com/privacy-scaling-explorations/perpetualpowersoftau) `.ptau`
file, and updates `ceremony.config.ts` with the actual constraint values.
> **Note:** if the circuit artifacts path was skipped during scaffolding,
> `ceremony.config.ts` may have empty `circuits`/`tiers` arrays. Populate them manually
> — see [Configuration](#configuration).
### 3. Configure environment variables
```bash
cp .env.example .env
```
| Variable | Source | Purpose |
| --- | --- | --- |
| `BLOB_READ_WRITE_TOKEN` | Vercel Blob | Read/write zkey files |
| `KV_REST_API_URL` | Vercel KV (Upstash) | Redis endpoint for ceremony state |
| `KV_REST_API_TOKEN` | Vercel KV (Upstash) | Redis auth token |
| `GITHUB_CLIENT_ID` | GitHub OAuth App | OAuth client ID |
| `GITHUB_CLIENT_SECRET` | GitHub OAuth App | OAuth client secret |
| `NEXTAUTH_SECRET` | Generated locally | JWT session encryption secret |
| `NEXTAUTH_URL` | Deployment URL | Canonical app URL |
| `BEACON_API_URL` | *(optional)* | Ethereum Beacon API endpoint for finalization (defaults to a public node) |
### 4. Provision Vercel storage
1. Link to a Vercel project: `vercel link`
2. Create a **Blob** store in the Vercel dashboard (Storage tab).
3. Create a **KV (Upstash)** store in the same tab.
4. Pull the generated env vars: `vercel env pull`
### 5. GitHub OAuth
1. Create an OAuth App at [github.com/settings/developers](https://github.com/settings/developers).
2. Set the callback URL to `/api/auth/callback/github`.
3. Check **Enable Device Flow** to support CLI contributions (`@wonderland/cabure-cli`).
4. Copy the Client ID and Client Secret into your `.env`.
5. Generate `NEXTAUTH_SECRET`:
```bash
openssl rand -base64 32
```
### 6. Initialize and run
```bash
pnpm init:ceremony
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000). `init:ceremony` only needs to run
once; after that the API routes manage ceremony state automatically.
### Deploy
```bash
vercel --prod
```
Add all environment variables in the Vercel dashboard under **Settings → Environment
Variables**, and set `NEXTAUTH_URL` to your production domain.
### Keep Vercel Fluid Compute OFF
The contribution route downloads the Powers of Tau file (often hundreds of MB) and runs
`verifyChain` inside the request, writing temp files to `/tmp` (512 MB hard cap on
Vercel).
**Turn Fluid Compute off** (Project → Settings → Functions). Fluid reuses instances and
shares `/tmp` and memory across concurrent invocations, so two contributions verifying
at once overflow `/tmp` (two copies of the ptau exceed 512 MB) and fail with
*"Verification failed. If your contribution is valid, please retry."* even though the
contribution is valid. With Fluid off, each verify runs in its own isolated instance
and fits.
Trade-off: classic functions cap `maxDuration` at 300 s (the contribute route sets
exactly that). A circuit whose verify cannot finish under 300 s must be verified on an
external worker rather than in the route.
### Scripts
| Script | Description |
| --- | --- |
| `pnpm setup:ptau` | Detect circuit constraints, download the correct PPoT ptau, and update config. |
| `pnpm init:ceremony` | Generate genesis zkeys, upload to Blob, write the manifest to KV. Outputs to `public/genesis/`. |
| `pnpm reset:ceremony` | Back up state, then wipe all KV keys and Blob objects. Refuses a finalized ceremony unless `-- --force`; asks for typed confirmation unless `-- --yes`. |
| `pnpm finalize:ceremony` | Apply the beacon (Ethereum RANDAO by default), verify the chain, export vkeys and final zkeys. Outputs to `public/finalize/`. |
#### setup:ptau
```bash
pnpm setup:ptau # download and update constraints
pnpm setup:ptau -- --force # re-download even if the ptau exists
pnpm setup:ptau -- --verify # also run snarkjs ptau verification
```
#### Finalization
By default, finalization uses the RANDAO reveal from the latest finalized Ethereum
beacon-chain slot as the beacon source, which makes the beacon publicly verifiable.
```bash
pnpm finalize:ceremony # latest finalized slot RANDAO (default)
pnpm finalize:ceremony -- --beacon-slot 7325000 # specific pre-announced slot
pnpm finalize:ceremony -- --beacon 0xabc123 # explicit hex beacon value
pnpm finalize:ceremony -- --random-beacon # random beacon (local testing only)
pnpm finalize:ceremony -- --force # finalize before the target is reached
```
For maximum verifiability, **announce a future beacon-chain slot number publicly before
running** with `--beacon-slot`. The RANDAO reveal is fetched from the Ethereum Beacon
API (`BEACON_API_URL` overrides the default public endpoint).
Finalization **seals** the ceremony the moment it starts and commits the beacon in the
same step. If a run is interrupted, the ceremony stays sealed: resume with
`pnpm finalize:ceremony -- --force` (which reuses the committed beacon, so the result is
reproducible), or run `pnpm reset:ceremony` to start over. The beacon cannot be silently
re-rolled by re-running.
#### Pin the genesis hash externally
`init:ceremony` records each circuit's genesis hash — as `genesisZkeyHash` in
`init-transcript.json` and as `initialZkeyHash` in KV. `finalize:ceremony` checks the
genesis blob against that hash before verifying the chain, which catches a swapped or
corrupted genesis blob while KV is intact.
It does **not** defend against an attacker who can write both the blob and KV: they
would rewrite the pinned hash to match a swapped genesis. To close that gap, **publish
each `genesisZkeyHash` somewhere outside this deployment's control at the start of the
ceremony** — commit it to a public Git repo, post it where contributors can read it.
Contributors and auditors can then confirm (Step 1 above) that the finalized parameters
were built on the genesis announced at the start, not one substituted later.
---
## Agent / headless contributions
An autonomous agent (e.g. `codex`) can contribute **headlessly** (no browser UI),
using the OS CSPRNG for randomness (`node:crypto.randomBytes` — the
`/dev/urandom` / `getentropy` / `BCryptGenRandom` equivalent, never mouse). The
landing page has a **FOR AGENTS** button linking to [`/llms.txt`](public/llms.txt)
— a machine-readable runbook the agent follows.
The agent gets a Caburé Bearer JWT one of two ways, then runs `npx
@wonderland/cabure-cli contribute --token `:
- **GitHub (recommended, default)** — the existing CLI device flow
(`POST/GET /api/ceremony/auth/cli`). Keeps a real GitHub identity on the
contribution; a human authorizes a code once. Works out of the box (requires
"Enable Device Flow" on the GitHub OAuth App).
- **Generated keypair (opt-in, fully autonomous)** — `POST /api/ceremony/auth/wallet`:
the agent signs a challenge with an Ed25519 key it generates and gets a JWT
under an anonymous `agent:` identity, no human. **Enable with
`ALLOW_AGENT_AUTH=1`** (404 otherwise). Deliberately not sybil-resistant —
acceptable for a Phase-2 setup where extra participants only add entropy.
Wallet self-test: `pnpm exec tsx scripts/agent-auth-selftest.ts`.
## Configuration
Edit [`ceremony.config.ts`](./ceremony.config.ts) to customize the ceremony name,
circuits, tiers, contribution targets, and UI copy. The full shape is defined by
`CeremonyConfig` in [`src/types/ceremony.ts`](./src/types/ceremony.ts); all user-facing
strings live in [`src/copy.ts`](./src/copy.ts).
### Circuits
Each entry in `circuits` describes one zkey chain that contributors extend.
`setup:ptau` fills in `constraints` automatically once the `.r1cs` files are in place.
```ts
circuits: [
{
id: "deposit", // unique, stable ID used in receipts and storage paths
label: "Deposit", // display name in the UI
description: "Deposit funds into the privacy pool.",
constraints: "2,061", // filled in by `pnpm setup:ptau`
targetContributions: 5, // per-circuit target
artifacts: {
r1csPath: "circuits/deposit.r1cs", // path relative to the project root
ptauPath: PTAU_PATH, // circuits/pot_final.ptau, downloaded by setup:ptau
},
},
// …
],
```
### Tiers
Tiers let contributors pick a smaller commitment on the tier-selection screen. Set
`tiersEnabled: false` and omit `tiers` to skip that screen and contribute to every
circuit. Each `circuitIds` must reference IDs defined in `circuits`.
```ts
tiersEnabled: true,
tiers: [
{
id: "core", // one of: "core" | "popular" | "all"
label: "Core circuits",
description: "Fast contribution — essential circuits only",
estimatedMinutes: 5,
circuitIds: ["deposit", "ragequit"],
},
{
id: "all",
label: "Full ceremony",
description: "Contribute to every circuit",
estimatedMinutes: 30,
circuitIds: ["deposit", "ragequit", "transact_1x1" /* … */],
},
],
```
---
## Trust model & guarantees
- **1-of-N honesty.** The final keys are secure if at least one contributor was honest
and destroyed their randomness. Contributions are computed client-side and the secret
is never uploaded.
- **Rooted in a pinned, externally-announced genesis.** The chain can be proven to
extend the genesis parameters that were published at the start (Step 1).
- **Server-side verification of every contribution**, and a full chain re-verification
from the genesis at finalization — including a re-walk that matches the final key's
embedded contribution hashes against the recorded receipts, so a key swapped via a
leaked storage token (but no KV access) is rejected.
- **A public, unpredictable beacon** (Ethereum RANDAO) is applied last, so the result
does not depend solely on the contributors.
- **Fully reproducible verification.** The final keys, verification keys, and a complete
transcript are published; anyone can reproduce every check with standard tooling
([Verifying the ceremony yourself](#verifying-the-ceremony-yourself)).
- **Attestations prove inclusion, not honesty.** They are an optional, voluntary record;
soundness comes from the verifiable chain and the open verifier, not from attestation
counts.