program address Devnet: 6G3qPM3Rf4fmEgWRR8Mhv6822RJvrpuqf2aHB6QEzxe3
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());
});- create a random
nullifierandsecretand 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);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| 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