A zero-knowledge proof system for private voting using Circom. This project implements privacy-preserving voting using zero-knowledge proofs.
Quick setup and test:
npm install # Install dependencies
npm run generate-verifiers # Generate Solidity verifiers (for real ZK verification)
npm run generate-inputs # Generate vote commit and reveal inputs
ls -la contracts/verifiers # Verify verifier contracts were generated (VoteCommitVerifier.sol and VoteRevealVerifier.sol)
npx hardhat compile --force # Compile the generated verifier contracts with force flag
npm run test-all # Run all tests (both circuits and contracts)
Important: Make sure verifier contracts are properly generated in the contracts/verifiers directory before running tests. If any are missing, run
npm run generate-verifiers
again or check for errors during the generation process.
MultiVote allows users to:
- Commit to votes privately
- Reveal votes with zero-knowledge proofs
Privacy is preserved using Pedersen commitments and zero-knowledge proofs to ensure that voting details remain confidential while still being verifiable.
MultiVote offers several significant advantages over traditional voting systems:
-
No Trusted Party Required: Unlike many voting systems that require a trusted third party to ensure fairness, MultiVote operates in a fully decentralized manner. The system mathematically guarantees privacy and correctness through zero-knowledge proofs rather than relying on trusted authorities. Only the ceremony of circuit generation needs a trusted party, no active trusted party during operation.
-
No Merkle Trees Needed: While many privacy-focused systems rely on Merkle trees for efficient verification, MultiVote uses direct commitment verification, eliminating the complexity and potential vulnerabilities associated with Merkle tree implementations. This results in a simpler, more auditable system with reduced attack surface.
-
Full Privacy Preservation: The combination of Pedersen commitments and zero-knowledge proofs ensures that voting choices remain completely private while still being verifiable.
-
On-chain Verification: All verifications happen trustlessly on-chain, ensuring that the integrity of the voting process is maintained in a fully transparent manner.
-
Unlinkable Vote Revelation: A key strength of this approach is that voters only need a second fresh address to reveal their vote. This address must be unlinkable to their public commit address, providing an additional layer of privacy. This separation of commit and reveal identities makes it virtually impossible to correlate a user's voting patterns or preferences with their public identity, even with sophisticated chain analysis.
The project includes zero-knowledge circuits for voting:
-
vote_commit.circom: Allows users to commit to a vote privately.
-
vote_reveal.circom: Enables users to reveal their vote with a proof.
-
Install dependencies:
npm install
-
Clean temporary files (if needed):
npm run clean
-
Generate Solidity verifiers (optional):
npm run generate-verifiers
This will create Solidity verifier contracts for all circuits in
contracts/verifiers/
.
To test the entire system at once:
npm run test-all
This will run both the circuit tests and smart contract tests consecutively.
The project includes a comprehensive testing workflow with organized npm commands.
Command | Description |
---|---|
npm run generate-inputs |
Generate vote commit and reveal inputs |
npm run test-circuits |
Test all ZK circuits and debugging scripts |
npm run test-contracts |
Run Hardhat smart contract tests |
npm run test-all |
Run all tests (circuits and contracts) |
npm run clean |
Clean up all build files and generated content |
npm run clean:full |
Clean everything including node_modules |
npm run clean:frontend |
Clean frontend build files |
npm run clean:frontend:full |
Clean frontend build files and node_modules |
npm run generate-verifiers |
Generate Solidity verifier contracts for all circuits |
To clean all generated files and build artifacts:
npm run clean
This command:
- Removes the build directory
- Removes the temp directory
- Cleans the inputs directory (preserves test input files)
- Removes the generated verifier contracts
- Removes Hardhat artifacts and caches
- Removes Powers of Tau files and other ZK-related artifacts
For a full clean (including node_modules):
npm run clean:full
To clean only the frontend:
npm run clean:frontend
For a full frontend clean (including node_modules):
npm run clean:frontend:full
npm run test-circuits
This command runs the input generation script first, then executes the ./scripts/test_all_circuits.sh
script which:
- Generates and tests commit and reveal circuits
- Verifies witness generation for all circuits
- Runs debugging scripts to validate bit encoding and hashing
- Cleans up temporary files when done
npm run test-contracts
This runs the Hardhat test suite which verifies the voting functionality and integration between smart contracts and ZK circuits.
Note: If you want to use real verifiers instead of mocks in the tests, first run
npm run generate-verifiers
before runningnpm run test-contracts
. This will generate the necessary Solidity verifier contracts that the tests will use instead of falling back to mock verifiers.
Troubleshooting: If you encounter an error during testing:
- Make sure verifier contracts (VoteCommitVerifier.sol and VoteRevealVerifier.sol) exist in
contracts/verifiers/
- Run
npx hardhat compile --force
to ensure all contracts are properly compiled- Check that the Solidity version in the generated verifier contracts matches your project's requirements (should be ^0.8.0)
All temporary files created during testing are stored in the ./temp
directory and cleaned up afterward to keep the project organized.
If you need more granular control over circuit testing, you can follow these steps:
-
Generate inputs:
npm run generate-inputs
This script generates cryptographic Pedersen commitments for vote commits and reveals.
-
Compile specific circuits:
circom circuits/vote_commit.circom --r1cs --wasm -o temp circom circuits/vote_reveal.circom --r1cs --wasm -o temp
-
Generate witnesses:
node temp/vote_commit_js/generate_witness.js temp/vote_commit_js/vote_commit.wasm inputs/vote_commit_input.json temp/vote_commit_witness.wtns node temp/vote_reveal_js/generate_witness.js temp/vote_reveal_js/vote_reveal.wasm inputs/vote_reveal_input.json temp/vote_reveal_witness.wtns
-
Debug specific components:
node scripts/debugVoteCommit.js node scripts/debugVoteReveal.js node scripts/debugHash.js
To generate production-ready Solidity verifiers for all circuits:
npm run generate-verifiers
This command:
- Creates a trusted setup for zero-knowledge proofs (Powers of Tau)
- Compiles all circuits to R1CS format
- Generates proving and verification keys
- Creates ready-to-use Solidity verifier contracts (VoteCommitVerifier.sol and VoteRevealVerifier.sol)
- Places the verifiers in
contracts/verifiers/
directory
The generated verifiers can be used directly in your smart contracts by importing them.
The project uses Pedersen hash commitments to hide vote data while still allowing proofs about that data. A commitment acts like a sealed envelope:
- You can put values inside (vote choice, voter ID, random salt)
- Once sealed, no one can see what's inside
- You can't change the contents without breaking the seal
- Later, you can open it to prove what was inside
These commitments are crucial to the privacy model:
- Public data: only the commitments are stored on-chain
- Private data: vote choice, voter ID, and salt values remain known only to the owner
- Verifiable: despite being private, users can prove statements about the hidden values
A key aspect of the implementation:
-
There is a significant difference between the JavaScript and Circom implementations of Pedersen hashing. To ensure the commitments generated in JavaScript match what the circuit expects, we need to:
- Convert numbers to bits correctly (using Num2Bits in circom)
- Ensure bit order is consistent (LSB first in both implementations)
- Use the output directly from the circuit to validate the approach
The circuit-generated commitments (rather than JavaScript approximations) are used in the input files.
Unlike systems that rely on trusted operators or complex cryptographic structures like Merkle trees, MultiVote's approach has several benefits:
-
Reduced Complexity: By avoiding Merkle trees, the system has fewer components that could potentially contain bugs or vulnerabilities.
-
Direct Verification: Each vote is individually verified through zero-knowledge proofs, without needing to traverse a tree structure or rely on intermediary data.
-
No Coordinator Required: The system operates without any central coordinator, making it resistant to censorship and single points of failure.
-
Simpler Auditing: The straightforward commitment-verification process makes it easier to audit and reason about the system's security properties.
The circuits improve privacy by:
- Breaking the direct link between identities and votes
- Preventing graph analysis that could de-anonymize users
- Making it impossible to determine voting patterns
- Eliminating reliance on trusted third parties who could compromise privacy
- Address Separation: A key privacy strength is that users can commit votes with one address and reveal with a completely different, unlinkable address. This separation prevents correlation between a user's public identity and their voting activity, even in the face of advanced blockchain analysis.
One of the most powerful privacy features of MultiVote is the ability to use different addresses for committing and revealing votes. Here's how it works:
- A user commits their vote from their primary address (Address A)
- The same user can later reveal their vote from a completely different address (Address B)
- No on-chain connection exists between Addresses A and B
- Even if Address A is known publicly, the vote remains private as long as Address B cannot be linked to it
This approach provides significant privacy benefits compared to traditional voting systems, or even other blockchain voting systems that use the same address for both actions. By requiring a fresh address for revelation that's unlinkable to the commit address, MultiVote makes it virtually impossible to track voting patterns or preferences of specific users.
To maintain maximum privacy, users should follow these best practices:
- Use a completely fresh address for vote revelation
- Never transfer funds directly between commit and reveal addresses
- Ensure metadata (like transaction timing, gas settings) doesn't create patterns that could link the addresses
Apache License 2.0
See the LICENSE file for details.