Privacy-preserving voting infrastructure for StarkNet.
CryptVote is a private ballot system built on StarkNet. Individual votes are encrypted and hidden until the tally is complete, eligibility is proven without revealing identity, and every step is verifiable on-chain. No vote manipulation. No whale signaling. No trusted intermediary during the voting period.
On-chain governance is broken by transparency. When a whale votes YES publicly, smaller holders see it and follow. The outcome is influenced before the poll even closes. Private ballots exist in the physical world for this exact reason — they should exist on-chain too.
Existing solutions either sacrifice privacy (on-chain voting) or sacrifice verifiability (off-chain voting like Snapshot). CryptVote does both.
CryptVote is a private voting module — a piece of infrastructure that a DAO can plug into its governance process. It handles:
- Proving who is eligible to vote (without revealing which voter you are)
- Encrypting votes so they remain hidden during the voting period
- Aggregating votes homomorphically on-chain without decrypting them
- Revealing and verifying the final tally only after the poll closes
The admin submits a proposal on-chain with a question, Paillier public key, and voting window. Eligible voters register their identity commitments (Poseidon hashes of their secret keys) on-chain. The admin then builds a Merkle tree from all commitments and sets the root — at which point the poll becomes active and registration closes.
Each voter generates a ZK proof off-chain using their secret key. The proof attests:
- Their commitment exists in the Merkle tree (they are eligible)
- A nullifier derived from
hash(secret, poll_id)— preventing double voting without linking the vote to any identity - Their encrypted ballot is a valid binary vote —
[1,0]for YES or[0,1]for NO — not any other value
The proof is generated in-browser using Noir + Barretenberg (UltraKeccakZKHonk) and verified on-chain via a Garaga-generated Cairo verifier.
The voter encrypts their ballot using Paillier encryption under the poll's public key. The ciphertext is submitted alongside the ZK proof. The contract verifies the proof, checks the nullifier is unused, records the nullifier, and aggregates the encrypted vote into the running ciphertext.
Paillier encryption is additively homomorphic. The contract multiplies incoming ciphertexts directly:
Enc(agg) = Enc(vote_1) * Enc(vote_2) * ... mod n²
No decryption happens at any point during the voting period. Individual votes remain hidden.
After the voting window ends, the admin decrypts only the final aggregated ciphertext using the private key (stored locally — never submitted on-chain). A separate ZK proof is generated proving the decryption is correct. The result is published on-chain with tally_proven = true.
| Property | Mechanism |
|---|---|
| Votes hidden until tally | Paillier homomorphic encryption |
| Eligible voters only | Merkle membership ZK proof (Noir + Garaga) |
| No double voting | Nullifier: hash(secret, poll_id) |
| Valid ballots only | Binary constraint proven in ZK circuit |
| Correct tally | Admin decryption ZK proof verified on-chain |
| Voter identity hidden | Secret key never leaves the browser |
| Layer | Technology |
|---|---|
| Smart contracts | Cairo 2 on StarkNet |
| ZK circuits | Noir (UltraKeccakZKHonk, trustless — no trusted setup) |
| Proving backend | Barretenberg (bb.js, in-browser WASM) |
| On-chain verification | Garaga v1.0.1 |
| Homomorphic encryption | Paillier (paillier-bigint) |
| Merkle tree | Poseidon2 hashing |
| Frontend | React + starknet-react + TailwindCSS |
Private inputs: secret, merkle_proof, is_even, vote, randomness r
Public inputs: poll_id, merkle_root, nullifier, enc_vote (yes + no), pk_g, pk_n, pk_n2
Proves:
commitment = Poseidon2(secret, 0)is in the Merkle treenullifier = Poseidon2(secret, poll_id)vote[0]andvote[1]are binary and sum to 1- Paillier ciphertexts match the plaintext vote under the poll's public key
Private inputs: lambda, mu (Paillier private key)
Public inputs: c (ciphertext), n, n2, total (decrypted result)
Proves: the announced tally correctly decrypts the aggregated ciphertext using the private key
cryptvote/
├── contracts/ # Cairo smart contracts
│ └── src/
│ └── lib.cairo # CryptVote contract + verifier interfaces
├── merkle_verifier/ # Garaga-generated vote proof verifier
├── decrypt_verifier/ # Garaga-generated decrypt proof verifier
├── vote_circuit/ # Noir circuit for voter eligibility + encryption
│ └── src/main.nr
├── decrypt_circuit/ # Noir circuit for admin tally decryption
│ └── src/main.nr
├── app/ # React frontend
│ └── src/
│ ├── pages/
│ │ ├── create-poll/ # Admin poll creation + Paillier keygen
│ │ ├── view-polls/ # Browse and vote on active polls
│ │ ├── close-poll/ # Admin tally decryption + proof submission
│ │ └── merkle-polls/ # Admin commitment registration + Merkle root
│ └── helpers/
│ ├── gen_proof.ts # Unified ZK proof generation hook
│ └── merkle_tree.ts # Poseidon2 Merkle tree
└── Makefile # Full build pipeline
make install-bun install-noir install-barretenberg install-starknet install-devnet install-garaga install-app-deps# Terminal 1: Local StarkNet node
make devnet
# Terminal 2: Export account details
make accounts-file# build circuit
make build-circuit gen-vk# Generate verifier contracts from VKs
make gen-verifier
# Build verifiers
make build-verifier
# Deploy (update class hashes in Makefile after declare)
make deploymake artifacts
make run-app- Register — submit your identity commitment (
Poseidon2(secret, 0)) before the poll starts - Download your
poll-{id}.jsoncommitment file — contains your secret, keep it safe - Wait for the admin to set the Merkle root and activate the poll
- Vote — upload your commitment file, select YES or NO, ZK proof is generated in-browser (~30-60s) and submitted on-chain
- Create poll — set question, voting window, generate and download Paillier keypair
- Wait for voter registrations
- Set Merkle root — builds tree from all commitments and activates the poll
- After poll ends — upload private key file, decrypt tally, submit YES and NO proofs separately
scarb testMIT