voidcashdocs

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

InputValue
CircuitTransaction(26, 2, 2) with the asset field in every note
Compilercircom 2.2.3, --O2
R1CS SHA-2560ae251fe1cf45316829d446f3e68d3256675a23f6137281cf49865970cf360f9
Phase 1powersOfTau28_hez_final_16.ptau, the Hermez powers of tau for up to 2^16 constraints
Phase 1 BLAKE2b-5126a6277a2f74e1073601b4f9fed6e1e55226917efb0f0db8a07d98ab01df1ccf43eb0e8c3159432acd4960e2f29fe84a4198501fa54c8dad9e43297453efec125, from the table in snarkjs's README
Toolsnarkjs 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

FileWhereSHA-256
Proving key/circuit/transaction.34b15fb1a334f8a0.zkey on the app's origin34b15fb1a334f8a053ebecfd85377eecb85cf891161a4d705ffc6962e384b0ec
Witness generator/circuit/transaction.fbed2c0ce5d0ea4c.wasm on the app's originfbed2c0ce5d0ea4cbaac65289c387ca8240071592a27edf1d22d2f48a3fd4985
Verifying key (JSON)Exported from the proving key5eaf4c06168672ea56f9a70f1a27dd419cc094a61dd84460d86556942e8e1bf6

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.wasm

The 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.json

The 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"))' CrThUQcn1DCQm1RuX3FfzdWcNxZefYaQq5MwscRcWER4

The 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 groth16 key on bn128, with nPublic + 1 IC 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_bn128 syscalls 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.

On this page