npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cheny56/zk-voting

v1.0.0

Published

Privacy-preserving voting system using Zero-Knowledge Proofs with multiple tally methods

Readme

@pqc/zk-voting

Privacy-preserving voting system using Zero-Knowledge Proofs with multiple tallying methods.

npm version License: MIT

Features

  • 🔒 Anonymous Voting - ZK proofs hide voter identity
  • 🎯 Double-Vote Prevention - Nullifiers prevent voting twice
  • 📊 Multiple Tally Methods - Choose between reveal-based and homomorphic
  • 🔗 EVM Compatible - Works on Ethereum, Quorum, and other EVM chains
  • 📦 Modular Design - Use components independently

Installation

npm install @pqc/zk-voting

Quick Start

const { 
    RevealTally, 
    HomomorphicTally,
    VotingClient 
} = require('@pqc/zk-voting');

// Simple reveal-based tallying
const tally = new RevealTally(3); // 3 candidates
tally.revealVote(commitment, 1, salt); // Vote for candidate 1
console.log(tally.getResults());

Table of Contents

Overview

This package implements a complete privacy-preserving voting system:

┌─────────────────────────────────────────────────────────────────────────┐
│                           VOTING FLOW                                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  1. REGISTRATION                                                        │
│     ┌───────────┐     ┌───────────────────┐                            │
│     │  Voter    │ --> │ Identity          │ --> Stored in              │
│     │  Secret   │     │ Commitment        │     Merkle Tree            │
│     └───────────┘     └───────────────────┘                            │
│                                                                         │
│  2. VOTING                                                              │
│     ┌───────────┐     ┌───────────────────┐     ┌───────────────────┐  │
│     │  Vote +   │ --> │ ZK Proof          │ --> │ On-chain:         │  │
│     │  Secret   │     │ Generation        │     │ - Nullifier       │  │
│     └───────────┘     └───────────────────┘     │ - Vote Commitment │  │
│                                                  └───────────────────┘  │
│                                                                         │
│  3. TALLYING (choose one)                                              │
│     ┌─────────────────────┐      ┌─────────────────────────────────┐   │
│     │  REVEAL-BASED       │  OR  │  HOMOMORPHIC                    │   │
│     │  - Voters reveal    │      │  - Encrypted aggregation        │   │
│     │  - Simple           │      │  - Max privacy                  │   │
│     │  - Votes public     │      │  - Requires trusted tallier     │   │
│     └─────────────────────┘      └─────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Architecture

Components

| Component | Description | File | |-----------|-------------|------| | RevealTally | Reveal-based vote tallying | lib/tally/reveal-tally.js | | HomomorphicTally | Paillier homomorphic tallying | lib/tally/homomorphic-tally.js | | PaillierCrypto | Paillier encryption primitives | lib/crypto/paillier.js | | VotingClient | High-level client for contracts | client/voting-client.js | | VoterRegistry | Solidity contract for registration | contracts/VoterRegistry.sol | | ZKVoting | Main voting contract | contracts/ZKVoting.sol | | vote.circom | ZK circuit for vote proofs | circuits/vote.circom |

Tallying Methods

Reveal-Based Tallying

In this method, voters reveal their votes after voting ends. Simple and requires no trusted party.

const { RevealTally, prepareRevealData } = require('@pqc/zk-voting');

// Initialize tally for 3 candidates
const tally = new RevealTally(3);

// After voting ends, each voter reveals their vote
// commitment = Poseidon(choice, salt)
const reveal = prepareRevealData(voteChoice, salt);
tally.revealVote(reveal.commitment, reveal.choice, reveal.salt);

// Get results
console.log(tally.getResults());
// [{ candidate: 0, votes: 10, percentage: "33.33" }, ...]

console.log(tally.getWinners());
// [0] or [0, 1] if tie

Privacy Trade-off

| Phase | Voter Identity | Vote Choice | |-------|---------------|-------------| | During voting | 🔒 Hidden (ZK proof) | 🔒 Hidden (commitment) | | After tally | 🔒 Hidden | ⚠️ Revealed |

Homomorphic Tallying

Individual votes are never decrypted. Only the final totals are revealed.

const { HomomorphicTally } = require('@pqc/zk-voting');

// Initialize with candidate count
const tally = new HomomorphicTally(3);

// Generate Paillier keys (admin does this once)
await tally.generateKeys(2048); // Use 2048 bits for production

// Share public key with voters
const pubKey = tally.exportPublicKey();

// Each voter encrypts their vote
const encryptedVote = tally.encryptVote(1); // Vote for candidate 1
tally.addEncryptedVote(encryptedVote);

// After all votes, decrypt only the totals (admin only)
const results = tally.decryptTally();
console.log(results); // [10, 15, 5] - vote counts per candidate

Privacy Trade-off

| Phase | Voter Identity | Vote Choice | |-------|---------------|-------------| | During voting | 🔒 Hidden (ZK proof) | 🔒 Hidden (encrypted) | | After tally | 🔒 Hidden | 🔒 Never revealed |

Comparison

| Aspect | Reveal-Based | Homomorphic | |--------|--------------|-------------| | Privacy Level | Moderate | High | | Complexity | Low | Medium | | Trusted Party | Not required | Required (holds decryption key) | | Voter Action After Voting | Must reveal | None | | Individual Votes | Public after tally | Never revealed | | Use Case | Public elections | Sensitive corporate votes |

Smart Contracts

VoterRegistry.sol

Manages eligible voters using a Merkle tree of identity commitments.

// Register a voter (admin only)
registry.registerVoter(identityCommitment);

// Close registration
registry.closeRegistration();

// Get Merkle root for ZK proofs
bytes32 root = registry.merkleRoot();

ZKVoting.sol

Main voting contract that verifies ZK proofs.

// Cast a vote with ZK proof
ballot.castVote(proof, nullifier, voteCommitment);

// After voting ends, reveal votes (reveal-based)
ballot.revealVote(voteIndex, choice, salt);

// Get results
uint256[] memory results = ballot.getResults();

ZK Circuits

The voting circuit (circuits/vote.circom) proves:

  1. Membership: Voter is in the Merkle tree of eligible voters
  2. Non-double-voting: Nullifier is correctly derived
  3. Valid vote: Choice is within valid range
Public Inputs:
  - merkleRoot: Root of voter registry
  - nullifier: Unique identifier (prevents double voting)
  - voteCommitment: Hash of (choice, salt)
  - candidateCount: Number of candidates

Private Inputs:
  - voterSecret: Voter's secret key
  - merklePath: Proof of membership
  - voteChoice: Actual vote
  - voteSalt: Random salt for commitment

Compiling the Circuit

# Install circom
npm install -g circom

# Compile circuit
npm run compile:circuit

# Generate trusted setup keys
npm run setup:keys

API Reference

RevealTally

const tally = new RevealTally(candidateCount);

// Reveal a vote
tally.revealVote(commitment, choice, salt);

// Batch reveal
tally.revealVotesBatch([{ commitment, choice, salt }, ...]);

// Get results
tally.getTally();           // [count0, count1, ...]
tally.getResults();         // [{ candidate, votes, percentage }, ...]
tally.getWinners();         // [winnerIndex, ...]
tally.getTotalRevealed();   // number
tally.isRevealed(commitment); // boolean

// Serialization
const json = tally.toJSON();
const restored = RevealTally.fromJSON(json);

HomomorphicTally

const tally = new HomomorphicTally(candidateCount, publicKey?, privateKey?);

// Generate keys (admin)
await tally.generateKeys(bitLength);

// Encrypt a vote (voter)
const encryptedVote = tally.encryptVote(choice);

// Add to tally (aggregator)
tally.addEncryptedVote(encryptedVote);

// Cast and add in one step
tally.castVote(choice);

// Decrypt final tally (admin with private key)
const totals = tally.decryptTally();

// Get results
tally.getResults();   // With percentages
tally.getWinners();   // Winner indices
tally.getVoteCount(); // Total votes

// Key management
tally.exportPublicKey();
tally.importPublicKey(exported);

PaillierCrypto

const { PaillierCrypto } = require('@pqc/zk-voting/crypto/paillier');

const paillier = new PaillierCrypto();
await paillier.generateKeys(2048);

// Encrypt
const ciphertext = paillier.encrypt(42);

// Decrypt
const plaintext = paillier.decrypt(ciphertext);

// Homomorphic addition: Enc(a) + Enc(b) = Enc(a + b)
const sum = paillier.add(ciphertext1, ciphertext2);

Examples

Basic Voting Flow

const { buildPoseidon } = require('circomlibjs');
const { RevealTally } = require('@pqc/zk-voting');

async function basicVotingExample() {
    const poseidon = await buildPoseidon();
    const hash = (inputs) => poseidon.F.toString(poseidon(inputs));
    
    const candidates = ['Alice', 'Bob', 'Charlie'];
    const tally = new RevealTally(candidates.length);
    
    // Simulate 10 voters
    const votes = [];
    for (let i = 0; i < 10; i++) {
        const choice = Math.floor(Math.random() * candidates.length);
        const salt = BigInt(Math.floor(Math.random() * 1e18));
        const commitment = hash([BigInt(choice), salt]);
        
        votes.push({ commitment, choice, salt });
    }
    
    // Reveal phase
    for (const vote of votes) {
        tally.revealVote(vote.commitment, vote.choice, vote.salt);
    }
    
    // Results
    console.log('Results:');
    for (const result of tally.getResults()) {
        console.log(`  ${candidates[result.candidate]}: ${result.votes} (${result.percentage}%)`);
    }
    
    const winners = tally.getWinners();
    console.log(`Winner: ${winners.map(w => candidates[w]).join(', ')}`);
}

basicVotingExample();

Homomorphic Voting

const { HomomorphicTally } = require('@pqc/zk-voting');

async function homomorphicVotingExample() {
    const candidates = ['Yes', 'No', 'Abstain'];
    const tally = new HomomorphicTally(candidates.length);
    
    // Admin generates keys
    console.log('Generating encryption keys...');
    await tally.generateKeys(512); // Use 2048 for production
    
    // Public key shared with voters
    const publicKey = tally.exportPublicKey();
    console.log(`Public key (n): ${publicKey.n.slice(0, 30)}...`);
    
    // Voters cast encrypted votes
    const voterChoices = [0, 1, 0, 0, 2, 1, 0]; // Individual choices hidden!
    
    for (let i = 0; i < voterChoices.length; i++) {
        const encryptedVote = tally.encryptVote(voterChoices[i]);
        tally.addEncryptedVote(encryptedVote);
        console.log(`Voter ${i + 1}: Cast encrypted vote`);
    }
    
    // Admin decrypts only the totals
    console.log('\nDecrypting final tally...');
    const results = tally.getResults();
    
    console.log('\nResults (individual votes were NEVER revealed):');
    for (const result of results) {
        console.log(`  ${candidates[result.candidate]}: ${result.votes} (${result.percentage}%)`);
    }
}

homomorphicVotingExample();

Contract Interaction

const { ethers } = require('ethers');
const { VotingClient } = require('@pqc/zk-voting/client');

async function contractExample() {
    const provider = new ethers.JsonRpcProvider('http://localhost:8545');
    const signer = new ethers.Wallet(PRIVATE_KEY, provider);
    
    const client = new VotingClient({
        provider,
        signer,
        registryAddress: '0x...',
        ballotAddress: '0x...'
    });
    
    // Register voters (admin)
    await client.registerVoter(identityCommitment);
    
    // Cast vote with ZK proof
    await client.castVote(proof, nullifier, voteCommitment);
    
    // Get ballot info
    const info = await client.getBallotInfo();
    console.log(`Total votes: ${info.totalVotes}`);
}

Privacy Analysis

See docs/PRIVACY-ANALYSIS.md for a detailed analysis of:

  • What information is hidden at each phase
  • Potential metadata leaks
  • Recommendations for maximum privacy

Summary

| Information | Registration | Voting | Tallying (Reveal) | Tallying (Homomorphic) | |-------------|-------------|--------|-------------------|------------------------| | Voter identity | 🔒 Hidden | 🔒 Hidden | 🔒 Hidden | 🔒 Hidden | | Who voted | ⚠️ Visible | ⚠️ Visible | ⚠️ Visible | ⚠️ Visible | | Vote choice | N/A | 🔒 Hidden | ⚠️ Revealed | 🔒 Hidden | | Vote totals | N/A | N/A | ✅ Public | ✅ Public |

Security Considerations

  1. Trusted Setup: The ZK circuit requires a trusted setup. Use a multi-party computation (MPC) ceremony for production.

  2. Key Security: For homomorphic tallying, the private key must be kept secure. Consider threshold decryption.

  3. Nullifier Uniqueness: Ensure nullifiers are derived correctly to prevent double voting.

  4. Timing Attacks: Use a relayer service to hide which Ethereum address casts which vote.

  5. Front-running: Consider commit-reveal schemes or private mempools to prevent front-running attacks.

Development

# Install dependencies
npm install

# Compile ZK circuit
npm run compile:circuit

# Run tests
npm test

# Run examples
npm run example
npm run example:homomorphic

License

MIT

Related Packages