program address Devnet: 6G3qPM3Rf4fmEgWRR8Mhv6822RJvrpuqf2aHB6QEzxe3
constpools=awaitprogram.account.pool.all();pools.map(async(p)=>{constdata=awaitprogram.account.pool.fetch(p.publicKey);consttree=PublicKey.findProgramAddressSync([Buffer.from("tree"),data.depositAmount.toArrayLike(Buffer,"le",8)],program.programId)[0].toString();console.log(tree,"depositAmount: ",data.depositAmount.toString());});- create a random
nullifierandsecretand compute the commitment
import{buildPoseidon}from"circomlibjs";// The more random the betterconstnullifier=BigInt(Math.floor(Math.random()*1_000_000));constsecret=BigInt(Math.floor(Math.random()*1_000_000));constposeidon=awaitbuildPoseidon();// compute the commitmentconstcommitment=poseidon.F.toObject(poseidon([nullifier,secret]));// store the nullifier and secret in a secure place for later withdrawalconsole.log(nullifier,secret);consttx=awaitprogram.methods.deposit(bigintToBytes32(commitment)).accountsPartial({signer: signer.publicKey,feeAccount: newPublicKey("DoQ47WTYzvgCNXwVK1Uf3urpXqa8maE7hJFd1xNLYUp2"),tree: newPublicKey(""),pool: newPublicKey(""),}).signers([signer]).rpc();console.log("signature: ",tx);constposeidon=awaitbuildPoseidon();constcommitment=poseidon.F.toObject(poseidon([nullifier,secret]));constinput={
nullifier,
secret,pathElements: res.proof.pathElements,pathIndices: res.proof.pathIndices,root: res.root,nullifierHash: poseidon.F.toObject(poseidon([nullifier])).toString(),recipient:
"0x"+Buffer.from(newPublicKey("recipient pubkey").toBytes()).toString("hex"),};// TODO: proof generation and transaction submission| Input | Description |
|---|---|
root | The current Merkle tree root |
nullifierHash | hash(nullifier) |
recipient | The withdrawal recipient |
The nullifier is a unique identifier generated for each commitment, used to prevent the same commitment from being spent twice.
In the current Solana program, a PDA account is derived with seeds [b"nullifer", nullifierHash]. If that account already exists, the nullifier has already been used and the transaction is rejected.
| Input | Description |
|---|---|
nullifier | A random value known only to the prover |
secret | A random value known only to the prover |
Both values are random numbers generated by the prover at deposit time.
commitment = Hash(nullifier, secret)This commitment is the value inserted into the Merkle tree at deposit time.
The sibling hashes needed to reconstruct a Merkle proof.
Given the tree:
Root
/ \
H12 H34
/ \ / \
C1 C2 C3 C4
To prove that C2 is in the tree, pathElements is [C1, H34]:
pathElements[0] = C1— the sibling ofC2pathElements[1] = H34— the sibling ofH12
Indicates whether the current node is the left or right child of its sibling at each level.
Parent Parent
/ \ / \
Mine Sib Sib Mine
pathIndices = 0 pathIndices = 1
(Mine is on the left) (Mine is on the right)
The depth of the Merkle tree. A tree with n levels can hold 2^n leaves. This circuit uses 20 levels, supporting up to 2^20 (1,048,576) commitments.
nodeTimesIndex[i] <== nodes[i] * pathIndices[i];
elemTimesIndex[i] <== pathElements[i] * pathIndices[i];Circom has no branching inside arithmetic constraints, so this is a trick to select left/right ordering algebraically instead of with an if statement.
We want:
- If the current node is on the left:
Hash(me, sibling) - If the current node is on the right:
Hash(sibling, me)
Since pathIndices[i] is either 0 or 1:
pathIndices[i] | Meaning | left | right |
|---|---|---|---|
0 | current node is left | node | sibling |
1 | current node is right | sibling | node |
The multiplication terms above compute exactly this selection without any conditional logic.
circom circuits/vortex.circom --r1cs --wasm --sym -l node_modules/circomlib/circuitsThis generates:
vortex.r1cs— the constraint systemvortex.wasm— used to compute the witness
Perform a trusted setup, or use an existing public Powers of Tau ceremony file (e.g. pot_final_10.ptau).
snarkjs groth16 setup vortex.r1cs pot_final_10.ptau vortex_0000.zkeyThis creates vortex_0000.zkey, containing the proving and verification keys.
snarkjs zkey export verificationkey vortex_0000.zkey verification_key.jsonCreate an input.json with the circuit's public and private inputs:
{
"nullifier": "",
"secret": "",
"pathElements": [],
"pathIndices": [],
"root": "",
"nullifierHash": "",
"recipient": ""
}node artifacts/vortex_js/generate_witness.js artifacts/vortex_js/vortex.wasm input.json witness.wtnsThis creates witness.wtns.
snarkjs groth16 prove vortex_0000.zkey witness.wtns proof.json public.jsonThis creates proof.json (the proof) and public.json (the public inputs).
snarkjs groth16 verify verification_key.json public.json proof.json