Skip to content

Circuits

SDK reference · Circom / Groth16

The vote circuit proves four things in zero knowledge: the voter is in the snapshot, the nullifier is correctly derived (so they can vote at most once), the direction is a valid bit, and the sealed ciphertext is well-formed — all without revealing identity, weight, or direction.

circuits/vote/vote.circom
pragma circom 2.2.1;
include "poseidon.circom"; // circomlib Poseidon
include "merkle.circom"; // MerkleTreeChecker(TREE_DEPTH)
template Vote(TREE_DEPTH) {
// ---- PUBLIC SIGNALS (order BINDING) ----
signal input merkleRoot; // [0] snapshot root
signal input proposalId; // [2] binds proof to a proposal (anti-replay)
signal input sealedCommitmentHash; // [3] commits to the sealed ciphertext
signal output nullifier; // [1] = Poseidon(secret, proposalId)
// ---- PRIVATE INPUTS ----
signal input secret, weight, direction, sealKey;
signal input pathElements[TREE_DEPTH];
signal input pathIndices[TREE_DEPTH];
// 1) leaf = Poseidon(Poseidon(secret), weight)
component secretCommit = Poseidon(1); secretCommit.inputs[0] <== secret;
component leaf = Poseidon(2);
leaf.inputs[0] <== secretCommit.out; leaf.inputs[1] <== weight;
// 2) Merkle membership
component mt = MerkleTreeChecker(TREE_DEPTH);
mt.leaf <== leaf.out; mt.root <== merkleRoot;
for (var i = 0; i < TREE_DEPTH; i++) {
mt.pathElements[i] <== pathElements[i];
mt.pathIndices[i] <== pathIndices[i];
}
// 3) nullifier = Poseidon(secret, proposalId)
component nf = Poseidon(2);
nf.inputs[0] <== secret; nf.inputs[1] <== proposalId; nullifier <== nf.out;
// 4) direction is a bit
direction * (direction - 1) === 0;
// 5) sealed-vote well-formedness
component sc = Poseidon(3);
sc.inputs[0] <== direction; sc.inputs[1] <== weight; sc.inputs[2] <== sealKey;
sealedCommitmentHash === sc.out;
}
component main {public [merkleRoot, proposalId, sealedCommitmentHash]} = Vote(20);
SignalKindMeaning
[0] merkleRootinputThe snapshot root the voter proves membership in.
[1] nullifieroutputPoseidon(secret, proposalId) — deterministic per voter+proposal; prevents double-voting.
[2] proposalIdinputBinds the proof to one proposal (anti-replay across proposals).
[3] sealedCommitmentHashinputPoseidon(direction, weight, sealKey) — commits to the timelock-sealed ciphertext.
InputTypeMeaning
secretfieldVoter’s private scalar (the leaf preimage).
weightfieldToken weight — hidden; revealed only at close.
directionbitVote choice {0,1} — sealed off-circuit, never public.
pathElements[20]field[]Merkle sibling hashes, root → leaf.
pathIndices[20]bit[]Left/right selector per tree level.
sealKeyfieldRandomness binding the ciphertext commitment.
#KindStatement
1. leafconstraintleaf = Poseidon(Poseidon(secret), weight).
2. membershipconstraintMerkleTreeChecker(20): leaf is in the tree under merkleRoot.
3. nullifierconstraintnullifier = Poseidon(secret, proposalId).
4. direction bitconstraintdirection · (direction − 1) === 0 (binary).
5. seal well-formednessconstraintsealedCommitmentHash === Poseidon(direction, weight, sealKey).

The proving artifacts are served as static assets so the browser can prove client-side:

ArtifactPathUse
vote.wasm/zk/vote.wasmWitness generator (snarkjs.groth16.fullProve).
vote_final.zkey/zk/vote_final.zkeyGroth16 proving key.
verification_key.json/zk/verification_key.jsonsnarkjs VK; source for the on-chain embedded VK.
const { proof, publicSignals } = await generateVoteProof(
input,
{ wasmPath: "/zk/vote.wasm", zkeyPath: "/zk/vote_final.zkey" },
deadlineUnixSeconds,
);