zkSVM

App

Litepaper

Documentation

Links

DocsOperate

Deployment

From a fresh checkout to a pool on a cluster.

4 min read

From a fresh checkout to a pool on a cluster. Five steps, one of which is a real ceremony.

Prerequisites

ToolVersionNeeded for
Ruststablethe program and its tests
Solana CLI2.x or latercargo-build-sbf, deploys
Anchor0.31.1build, keys, deploy
Node.js22+circuits, SDK
pnpm10+the SDK workspace
circom2.xcompiling 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

Shell
cd circuits
npm install
npm run build          # compile → setup → export verifying keys
OutputWhat it isCommit it?
programs/programs/zksvm-pool/keys/*.vk.binverifying keys the program embedsyes
circuits/build/*/*_final.zkeyproving keys — large, needed by walletsno (publish separately)
circuits/build/*/*_js/*.wasmwitness generators — needed by walletsno (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:

Shell
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

Shell
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:

TestClaim
tree (unit)the program's roots equal the SDK's, constant for constant
snarkjs_crosstesta real snarkjs proof verifies through the program's verifier, against the embedded key; every public input is bound
sdk_compatthe SDK's instruction bytes and account order decode with Anchor's own generated types
onchainthe 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

Shell
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:

TypeScript
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:

Shell
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 test runs 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