Skip to content

Imdavyking/cryptvote

Repository files navigation

CryptVote

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.


The Problem

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.


What CryptVote Is

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

How It Works

1. Admin Creates a Poll

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.

2. Voter Proves Eligibility

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.

3. Vote is Encrypted and Submitted

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.

4. Homomorphic Aggregation On-Chain

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.

5. Admin Closes the Poll

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.


Privacy Guarantees

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

Tech Stack

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

ZK Circuits

Vote Circuit (vote_circuit)

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:

  1. commitment = Poseidon2(secret, 0) is in the Merkle tree
  2. nullifier = Poseidon2(secret, poll_id)
  3. vote[0] and vote[1] are binary and sum to 1
  4. Paillier ciphertexts match the plaintext vote under the poll's public key

Decrypt Circuit (decrypt_circuit)

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


Project Structure

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

Installation & Setup

Prerequisites

make install-bun install-noir install-barretenberg install-starknet install-devnet install-garaga install-app-deps

Development Environment

# Terminal 1: Local StarkNet node
make devnet

# Terminal 2: Export account details
make accounts-file

Build ZK Circuits

# build circuit
make build-circuit gen-vk

Deploy Contracts

# Generate verifier contracts from VKs
make gen-verifier

# Build verifiers
make build-verifier

# Deploy (update class hashes in Makefile after declare)
make deploy

Run App

make artifacts
make run-app

Voter Flow

  1. Register — submit your identity commitment (Poseidon2(secret, 0)) before the poll starts
  2. Download your poll-{id}.json commitment file — contains your secret, keep it safe
  3. Wait for the admin to set the Merkle root and activate the poll
  4. Vote — upload your commitment file, select YES or NO, ZK proof is generated in-browser (~30-60s) and submitted on-chain

Admin Flow

  1. Create poll — set question, voting window, generate and download Paillier keypair
  2. Wait for voter registrations
  3. Set Merkle root — builds tree from all commitments and activates the poll
  4. After poll ends — upload private key file, decrypt tally, submit YES and NO proofs separately

Testing

scarb test

License

MIT

About

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors