From a fresh checkout to a pool on a cluster. Five steps, one of which is a real ceremony.
Prerequisites
| Tool | Version | Needed for |
|---|---|---|
| Rust | stable | the program and its tests |
| Solana CLI | 2.x or later | cargo-build-sbf, deploys |
| Anchor | 0.31.1 | build, keys, deploy |
| Node.js | 22+ | circuits, SDK |
| pnpm | 10+ | the SDK workspace |
| circom | 2.x | compiling the circuits |
0. The order is not negotiable
The verifying keys are compiled into the program
(programs/programs/zksvm-pool/keys/*.vk.bin, via include_bytes!). So:
ceremony -> verifying keys -> program build -> deploy
Keys that change after deployment mean a program upgrade, and every wallet fetching new proving keys in step with it. Existing notes survive that — a note is a commitment in the tree, not tied to any setup — but it is an operation you want to perform zero times on a live pool. Finish the ceremony first.
1. Build the circuits
cd circuits
npm install
npm run build # compile → setup → export verifying keys
| Output | What it is | Commit it? |
|---|---|---|
programs/programs/zksvm-pool/keys/*.vk.bin | verifying keys the program embeds | yes |
circuits/build/*/*_final.zkey | proving keys — large, needed by wallets | no (publish separately) |
circuits/build/*/*_js/*.wasm | witness generators — needed by wallets | no (publish separately) |
Commit the verifying keys. They are what lets anyone rebuild the program and check the deployed binary against the ceremony transcript. Serve the proving keys and wasm from a CDN with published hashes; wallets fetch them once.
npm run build runs the development setup: one contributor, fixed entropy
published in this repository. Anyone can forge proofs against those keys, and
a forged proof drains the vault. Fine for a devnet. For anything holding
value, stop here and read 03-ceremony.md.
The keys checked into this repository are development keys.
2. Regenerate the fixtures
New keys invalidate the recorded proofs the tests replay:
cd circuits && node scripts/gen-fixture.mjs
cd ../sdk && pnpm install && pnpm build
node test/emit-fixture.mjs
node scripts/gen-scenario.mjs
3. Build and test the program
cd programs
anchor keys sync # once per deployment: generates the program keypair and
# rewrites declare_id! and Anchor.toml to match
anchor build
cargo-build-sbf --manifest-path tests/treasury/Cargo.toml # test fixture: a PDA that owns notes
cargo test # the program: tree, verifier, SDK wire format
cargo test -p zksvm-onchain-tests # the compiled .so, end to end in LiteSVM
After anchor keys sync, put the new id in sdk/src/addresses.ts and re-run
step 2's last two commands — the fixtures record the program id, and
sdk_compat fails until the SDK and the program agree on it.
What the tests establish:
| Test | Claim |
|---|---|
tree (unit) | the program's roots equal the SDK's, constant for constant |
snarkjs_crosstest | a real snarkjs proof verifies through the program's verifier, against the embedded key; every public input is bound |
sdk_compat | the SDK's instruction bytes and account order decode with Anchor's own generated types |
onchain | the compiled .so, inside LiteSVM with real syscalls: shield → transfer → claim → unshield with real proofs; replay, a claim by a non-owner and a recipient swap all rejected; everything fits the compute budget |
onchain lives in its own crate, programs/tests/onchain, because LiteSVM's
dependency tree needs OpenSSL and does not build on a stock Windows host — WSL
or Linux is the easy path. It needs target/deploy/zksvm_pool.so and the
treasury fixture, and skips without them.
4. Deploy and initialize
solana config set --url devnet
anchor deploy --provider.cluster devnet
Then send initialize once. It creates the tree account and funds the vault
with its own rent-exempt minimum. It takes no parameters and grants nothing to
the caller, so it does not matter who sends it:
import { initializeInstruction } from "@zksvm/sdk";
await conn.sendTransaction(
new Transaction().add(initializeInstruction(payer.publicKey, PROGRAM_ID)),
[payer],
);
sdk/scripts/e2e-live.mjs does this for you if the pool is missing, then runs
a full shield → transfer → unshield against the cluster:
RPC_URL=https://api.devnet.solana.com PROGRAM_ID=<your id> node sdk/scripts/e2e-live.mjs
It airdrops to a throwaway payer, so it is a devnet / localnet tool.
5. Decide who holds the upgrade authority
This is the trust decision of the deployment. The upgrade authority can replace the program — including the verifying keys, including the rule that says only valid proofs release funds. Whoever holds it can drain the vault.
- Mainnet, value-bearing: a multisig at minimum. The end state for a pool
people rely on is
solana program set-upgrade-authority --final, which makes the program immutable — and makes any bug permanent. Get the audit first. - Devnet: your own key is fine.
Keep programs/target/deploy/zksvm_pool-keypair.json out of version control
(.gitignore already excludes it).
Checklist
- Real ceremony run, transcript published (03-ceremony.md)
- Verifying keys committed; hashes match the transcript
- Fixtures regenerated; both
cargo testruns green - Program id synced into
Anchor.toml,declare_id!, and the SDK - Proving keys + wasm served with published hashes
- Upgrade authority behind a multisig, or burned
- Audit done before real value enters (05-limitations.md)
- Users told what is and is not hidden