Skip to content

Repository files navigation

Vortex

Program

program address Devnet: 6G3qPM3Rf4fmEgWRR8Mhv6822RJvrpuqf2aHB6QEzxe3

Get Pools

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());});

Deposit

  • create a random nullifier and secret and 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);

Indexer

Prove and Withdraw

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

Circuit

Public Inputs

InputDescription
rootThe current Merkle tree root
nullifierHashhash(nullifier)
recipientThe 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.

Private Inputs

InputDescription
nullifierA random value known only to the prover
secretA 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.

pathElements

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 of C2
  • pathElements[1] = H34 — the sibling of H12

pathIndices

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)

levels

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.

Merkle Proof Verification

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]Meaningleftright
0current node is leftnodesibling
1current node is rightsiblingnode

The multiplication terms above compute exactly this selection without any conditional logic.

Compiling the Circuit

1. Compile

circom circuits/vortex.circom --r1cs --wasm --sym -l node_modules/circomlib/circuits

This generates:

  • vortex.r1cs — the constraint system
  • vortex.wasm — used to compute the witness

2. Trusted setup

Perform a trusted setup, or use an existing public Powers of Tau ceremony file (e.g. pot_final_10.ptau).

3. Circuit-specific setup

snarkjs groth16 setup vortex.r1cs pot_final_10.ptau vortex_0000.zkey

This creates vortex_0000.zkey, containing the proving and verification keys.

4. Export the verification key

snarkjs zkey export verificationkey vortex_0000.zkey verification_key.json

5. Prepare the input

Create an input.json with the circuit's public and private inputs:

{
"nullifier": "",
"secret": "",
"pathElements": [],
"pathIndices": [],
"root": "",
"nullifierHash": "",
"recipient": ""
}

6. Generate the witness

node artifacts/vortex_js/generate_witness.js artifacts/vortex_js/vortex.wasm input.json witness.wtns

This creates witness.wtns.

7. Generate the proof

snarkjs groth16 prove vortex_0000.zkey witness.wtns proof.json public.json

This creates proof.json (the proof) and public.json (the public inputs).

8. Verify the proof

snarkjs groth16 verify verification_key.json public.json proof.json

About

Privacy Mixer Pools on Solana

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages