Skip to content

thrishank/vortex

Repository files navigation

Vortex

Program

program address Devnet: 6G3qPM3Rf4fmEgWRR8Mhv6822RJvrpuqf2aHB6QEzxe3

Get Pools

const pools = await program.account.pool.all();

pools.map(async (p) => {
  const data = await program.account.pool.fetch(p.publicKey);

  const tree = 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 better
const nullifier = BigInt(Math.floor(Math.random() * 1_000_000));
const secret = BigInt(Math.floor(Math.random() * 1_000_000));

const poseidon = await buildPoseidon();

// compute the commitment
const commitment = poseidon.F.toObject(poseidon([nullifier, secret]));

// store the nullifier and secret in a secure place for later withdrawal
console.log(nullifier, secret);

const tx = await program.methods
  .deposit(bigintToBytes32(commitment))
  .accountsPartial({
    signer: signer.publicKey,
    feeAccount: new PublicKey("DoQ47WTYzvgCNXwVK1Uf3urpXqa8maE7hJFd1xNLYUp2"),
    tree: new PublicKey(""),
    pool: new PublicKey(""),
  })
  .signers([signer])
  .rpc();

console.log("signature: ", tx);

Indexer

Prove and Withdraw

const poseidon = await buildPoseidon();

const commitment = poseidon.F.toObject(poseidon([nullifier, secret]));

const input = {
  nullifier,
  secret,
  pathElements: res.proof.pathElements,
  pathIndices: res.proof.pathIndices,
  root: res.root,
  nullifierHash: poseidon.F.toObject(poseidon([nullifier])).toString(),
  recipient:
    "0x" +
    Buffer.from(new PublicKey("recipient pubkey").toBytes()).toString("hex"),
};

// TODO: proof generation and transaction submission

Circuit

Public Inputs

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.

Private Inputs

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.

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

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

No releases published

Packages

 
 
 

Contributors