Trusted setup
Where the Groth16 keys come from - the public phase 1, one contribution closed with a Solana block hash, the published hashes, and how to check them yourself.
Groth16 needs keys made for its circuit. Whoever knew the secret randomness of that setup could forge proofs, so how the keys were made matters. Voidcash's keys come from a public phase 1, one phase-2 contribution, and a beacon: the hash of a Solana block that did not exist when the contribution was fixed. The result can be checked against the circuit files the app serves, public Solana block data and the program on chain.
Who is trusted
The setup is sound as long as the contributor discarded the secret. The contribution's entropy was drawn once from the system's random source and piped to snarkjs, never written to a command line, a log or a file. There is one contributor, not a multi-party ceremony, in which one honest participant out of many would be enough. The program's upgrade authority can replace the verifying key in any case (see The pool program), so users of the pool already trust the holder of that key.
What the beacon adds: the final key depends on a value nobody could know when the contribution was fixed, and anyone can check that value against the chain.
Inputs
| Input | Value |
|---|---|
| Circuit | Transaction(26, 2, 2) with the asset field in every note |
| Compiler | circom 2.2.3, --O2 |
| R1CS SHA-256 | 0ae251fe1cf45316829d446f3e68d3256675a23f6137281cf49865970cf360f9 |
| Phase 1 | powersOfTau28_hez_final_16.ptau, the Hermez powers of tau for up to 2^16 constraints |
| Phase 1 BLAKE2b-512 | 6a6277a2f74e1073601b4f9fed6e1e55226917efb0f0db8a07d98ab01df1ccf43eb0e8c3159432acd4960e2f29fe84a4198501fa54c8dad9e43297453efec125, from the table in snarkjs's README |
| Tool | snarkjs 0.7.6 |
Steps
Phase 2 start. snarkjs groth16 setup on the R1CS and the phase-1 file.
One contribution, with entropy from /dev/urandom. The contributed key's SHA-256,
b6ce29971bb81fb04c38ec4d847189b675dc741f82e959f942c3207ea10ed94e, was fixed and published at 23:34:27 UTC on
2026-10-16, at Solana slot 457,822,166, together with the beacon slot below.
Beacon. The block hash of the first block produced at or after Solana mainnet slot 457,824,000. That slot was
produced (parent 457,823,999) at 23:41:00 UTC, after the contributed key was published. Its hash,
CrThUQcn1DCQm1RuX3FfzdWcNxZefYaQq5MwscRcWER4, was read at finalized from two RPC providers. Its 32 bytes in hex,
b01c3400b77b0493f1bb4ca443757f68252d197a8335f750babd87564f7b9cbf, went to snarkjs zkey beacon with 2^10
iterations, as the contribution "Solana mainnet block 457824000".
Verify and export. snarkjs zkey verify on the final key, then snarkjs zkey export verificationkey.
Result
| File | Where | SHA-256 |
|---|---|---|
| Proving key | /circuit/transaction.34b15fb1a334f8a0.zkey on the app's origin | 34b15fb1a334f8a053ebecfd85377eecb85cf891161a4d705ffc6962e384b0ec |
| Witness generator | /circuit/transaction.fbed2c0ce5d0ea4c.wasm on the app's origin | fbed2c0ce5d0ea4cbaac65289c387ca8240071592a27edf1d22d2f48a3fd4985 |
| Verifying key (JSON) | Exported from the proving key | 5eaf4c06168672ea56f9a70f1a27dd419cc094a61dd84460d86556942e8e1bf6 |
Each served file's name carries the first 16 hex digits of its SHA-256.
At the setup, snarkjs zkey verify printed a chain of two contributions: #1, the recorded contribution, and #2,
"Solana mainnet block 457824000" with beacon generator b01c3400…9cbf and 2^10 iterations, then "ZKey Ok!".
Verify it yourself
Everything below uses public files and public chain data. APP is the app's origin and RPC_URL any Solana RPC
endpoint.
The circuit files. Download them and check their SHA-256 against the table above.
curl -fsSL "$APP/circuit/transaction.34b15fb1a334f8a0.zkey" -o transaction.zkey
curl -fsSL "$APP/circuit/transaction.fbed2c0ce5d0ea4c.wasm" -o transaction.wasm
shasum -a 256 transaction.zkey transaction.wasmThe verifying key. Export it from the proving key with snarkjs 0.7.6. Its SHA-256 must be
5eaf4c06168672ea56f9a70f1a27dd419cc094a61dd84460d86556942e8e1bf6.
npx snarkjs@0.7.6 zkey export verificationkey transaction.zkey vk.json
shasum -a 256 vk.jsonThe beacon. Read the block hash of slot 457,824,000 and base58-decode it. It must be
CrThUQcn1DCQm1RuX3FfzdWcNxZefYaQq5MwscRcWER4, which decodes to the beacon value
b01c3400b77b0493f1bb4ca443757f68252d197a8335f750babd87564f7b9cbf.
curl -s "$RPC_URL" -H 'content-type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"getBlock",
"params":[457824000,{"transactionDetails":"none","rewards":false,"maxSupportedTransactionVersion":0}]}'
node -e 'const A="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";let n=0n;
for (const c of process.argv[1]) n = n * 58n + BigInt(A.indexOf(c));
console.log(n.toString(16).padStart(64, "0"))' CrThUQcn1DCQm1RuX3FfzdWcNxZefYaQq5MwscRcWER4The key the program verifies against. The verifying key's points are compiled into the program, so they appear in
its bytecode, which anyone can read from the program's ProgramData account. Dump the program, then look for every
point of vk.json in it: alpha, beta, gamma, delta and the 8 IC points (one more than the 7 public inputs), each
coordinate as 32 big-endian bytes, G2 coordinates in the order x.c1, x.c0, y.c1, y.c0.
solana program dump -u "$RPC_URL" HjQyQZ3xewjsBb3w8JFGJSh2ztauV4f6X4o5CrZTobZm pool.so// check-vk.mjs: node check-vk.mjs vk.json pool.so
import { readFileSync } from 'node:fs';
const vk = JSON.parse(readFileSync(process.argv[2], 'utf8'));
const bin = readFileSync(process.argv[3]);
const be = (d) => Buffer.from(BigInt(d).toString(16).padStart(64, '0'), 'hex');
const g1 = (p) => Buffer.concat([be(p[0]), be(p[1])]);
const g2 = (p) => Buffer.concat([be(p[0][1]), be(p[0][0]), be(p[1][1]), be(p[1][0])]);
const points = { alpha: g1(vk.vk_alpha_1), beta: g2(vk.vk_beta_2), gamma: g2(vk.vk_gamma_2), delta: g2(vk.vk_delta_2) };
vk.IC.forEach((p, i) => (points[`IC[${i}]`] = g1(p)));
for (const [name, bytes] of Object.entries(points)) console.log(name, bin.indexOf(bytes) >= 0 ? 'found' : 'MISSING');How the program embeds the key
The verifying key is compiled into the program as constants when the program is built:
- the key must be a
groth16key onbn128, withnPublic + 1IC points (7 public inputs, so 8 points); - each field element is written as 32 big-endian bytes, and each G2 point as x.c1, x.c0, y.c1, y.c0, the order the
alt_bn128syscalls take.
Proofs made with this key verify on chain against the embedded one, so a key that does not match the published one fails every proof check. The app serves the same proving key and witness generator, pinned by SHA-256 (see The circuit).
When the setup is redone
Any change to the circuit gives a new R1CS and needs a new setup, a new verifying key, and so a program upgrade. An
earlier setup, for the circuit before the asset field was added (R1CS b5e5780618e0ad2a…, beacon Solana block
457,748,000), was superseded by this one.
The transact circuit
The one zero-knowledge circuit behind every shield, private transfer and unshield - its inputs, constraints, public inputs, the ext-data binding and the proof system.
The pool program
The Anchor program that holds the pool's SOL - its accounts and seeds, every instruction and who may call it, fees, pause flags, events, limits and upgrades.