@cheny56/zk-voting-native
v1.0.0
Published
ZK Voting with Native PQC-Quorum Integration - Optimized for Groth16 precompile at 0x16
Downloads
107
Maintainers
Readme
@pqc-quorum/zk-voting-native
Zero-Knowledge Voting with Native PQC-Quorum Integration. Optimized for the Groth16 precompile at address 0x16.
Features
- 🚀 Native Precompile Integration - Uses Groth16 verifier at
0x16 - ⚡ Gas-Efficient - Precompile verification costs ~200k gas vs ~5M for Solidity
- 🔧 Go Prover -
gnark-based proof generation - 🔒 MiMC Hash - Compatible with gnark circuits
- 📦 Modular Design - Separate lib for circuits, tallying, and client
Installation
npm install @pqc-quorum/zk-voting-nativeGo Prover (Required for proof generation)
# Build the Go prover
cd go && go build -o ../bin/zk-prover .Quick Start
const { VotingClient, ProofGenerator } = require('@pqc-quorum/zk-voting-native');
// Connect to PQC-Quorum node
const client = new VotingClient('http://localhost:8545', privateKey);
// Cast a vote with native ZK proof
await client.castVote(voterSecret, leafIndex, voteChoice);Table of Contents
- Overview
- Architecture
- Prerequisites
- Smart Contracts
- Go Prover
- JavaScript Client
- API Reference
- Examples
- Benchmarks
- Comparison with @pqc/zk-voting
Overview
This package provides a ZK voting system optimized for PQC-Quorum nodes with native Groth16 verification:
┌─────────────────────────────────────────────────────────────────────────────┐
│ NATIVE ZK VOTING FLOW │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────────────┐ │
│ │ JavaScript │ │ Go Prover │ │ PQC-Quorum Node │ │
│ │ Client │ │ (gnark) │ │ │ │
│ └───────┬────────┘ └───────┬────────┘ └───────────┬────────────┘ │
│ │ │ │ │
│ │ 1. Prepare inputs │ │ │
│ ├─────────────────────>│ │ │
│ │ │ 2. Generate proof │ │
│ │ │ (gnark Groth16) │ │
│ │<─────────────────────┤ │ │
│ │ 3. Proof bytes │ │ │
│ │ │ │ │
│ │ 4. Submit tx ──────────────────────────────────>│ │
│ │ │ │ │
│ │ │ 5. Verify via 0x16 ──>│ │
│ │ │ │ (Precompile) │
│ │<─────────────────────────────────────────────────│ │
│ │ 6. Vote recorded │ │ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘Why Native Integration?
| Feature | snarkjs (Solidity) | Native (Precompile) | |---------|-------------------|---------------------| | Verification Gas | ~5,000,000 | ~200,000 | | Proof Gen Speed | ~2-5 seconds | ~200ms | | Trusted Setup | Required | Pre-deployed | | Hash Function | Poseidon | MiMC | | Best For | Any EVM chain | PQC-Quorum |
Prerequisites
- Node.js >= 18.0.0
- Go >= 1.21 (for proof generation)
- PQC-Quorum node with ZK precompiles enabled
Smart Contracts
Contract Overview
| Contract | Address | Description |
|----------|---------|-------------|
| VoterRegistry | Deployed | Merkle tree of eligible voters |
| ZKBallot | Deployed | Main voting with precompile verification |
| TallyManager | Deployed | Tallying orchestration |
| IZKPrecompile | 0x16 | Native Groth16 verifier |
VoterRegistry.sol
// Register voters (admin only)
function registerVoter(bytes32 commitment) external;
function registerVotersBatch(bytes32[] calldata commitments) external;
function closeRegistration() external;
// Query state
function merkleRoot() external view returns (bytes32);
function getVoterCount() external view returns (uint256);ZKBallot.sol
// Cast vote with native ZK verification
function castVote(
bytes calldata proof, // Groth16 proof bytes
bytes calldata publicInputs, // [merkleRoot, nullifier, commitment, count]
bytes[] calldata encryptedVote // For homomorphic tally (optional)
) external;
// End voting
function endVoting() external;
// Query
function getTotalVotes() external view returns (uint256);
function nullifierUsed(bytes32) external view returns (bool);Native ZK Precompile
The precompile at 0x16 provides efficient Groth16 verification:
interface IZKPrecompile {
function verifyGroth16(
bytes32 vkHash, // Hash of verification key
bytes calldata proof, // 8 G1/G2 elements (256 bytes)
bytes calldata inputs // Public inputs (32 bytes each)
) external view returns (bool);
}Go Prover
The Go prover uses gnark for Groth16 proof generation.
Circuit Definition
// go/circuit/vote_circuit.go
type VoteCircuit struct {
// Private inputs
VoterSecret frontend.Variable
VoterLeafIndex frontend.Variable
MerklePath [20]frontend.Variable
PathIndices [20]frontend.Variable
VoteChoice frontend.Variable
VoteSalt frontend.Variable
// Public inputs
MerkleRoot frontend.Variable `gnark:",public"`
Nullifier frontend.Variable `gnark:",public"`
VoteCommitment frontend.Variable `gnark:",public"`
CandidateCount frontend.Variable `gnark:",public"`
}Building the Prover
cd go
go mod tidy
go build -o ../bin/zk-prover .Using the Prover
# Generate proof
./bin/zk-prover prove \
--secret 0x1234... \
--leaf-index 5 \
--merkle-path 0xabc...,0xdef...,... \
--path-indices 0,1,0,... \
--vote-choice 2 \
--vote-salt 0x5678... \
--merkle-root 0x9abc... \
--candidate-count 4
# Outputs: proof.bin, public_inputs.jsonJavaScript Client
VotingClient
High-level client for the complete voting flow:
const { VotingClient } = require('@pqc-quorum/zk-voting-native');
const client = new VotingClient('http://localhost:8545', privateKey);
// Deploy contracts
await client.deployVoterRegistry();
await client.deployZKBallot(candidates, duration, vkHash);
await client.deployTallyManager();
// Admin: Register voters
await client.registerVoters([commitment1, commitment2, ...]);
await client.closeRegistration();
// Voter: Cast vote
await client.castVote({
voterSecret: BigInt('0x...'),
leafIndex: 5,
voteChoice: 2,
candidateCount: 4,
});
// Query state
const totalVotes = await client.getTotalVotes();
const candidates = await client.getCandidates();ProofGenerator
JavaScript wrapper for the Go prover:
const { ProofGenerator } = require('@pqc-quorum/zk-voting-native');
const prover = new ProofGenerator({
proverPath: './bin/zk-prover',
circuitPath: './circuits/vote.r1cs',
pkPath: './keys/proving.key',
});
// Generate proof
const { proof, publicInputs } = await prover.generateProof({
voterSecret,
leafIndex,
merklePath,
pathIndices,
voteChoice,
voteSalt,
merkleRoot,
candidateCount,
});API Reference
VotingClient
class VotingClient {
constructor(rpcUrl: string, privateKey: string);
// Deployment
deployVoterRegistry(): Promise<Contract>;
deployZKBallot(
candidates: string[],
durationSeconds: number,
vkHash: string
): Promise<Contract>;
deployTallyManager(): Promise<Contract>;
// Connect to existing
connectVoterRegistry(address: string): void;
connectZKBallot(address: string): void;
connectTallyManager(address: string): void;
// Admin functions
registerVoter(commitment: string): Promise<TransactionReceipt>;
registerVoters(commitments: string[]): Promise<TransactionReceipt>;
closeRegistration(): Promise<TransactionReceipt>;
// Voting
castVote(params: CastVoteParams): Promise<{
nullifier: string;
voteCommitment: string;
txHash: string;
}>;
// Queries
getMerkleRoot(): Promise<string>;
getTotalVotes(): Promise<bigint>;
getCandidates(): Promise<Candidate[]>;
isNullifierUsed(nullifier: string): Promise<boolean>;
getVoteCommitments(): Promise<string[]>;
// Tallying
endVoting(): Promise<TransactionReceipt>;
submitHomomorphicTally(tally: number[]): Promise<TransactionReceipt>;
}
interface CastVoteParams {
voterSecret: bigint;
leafIndex: number;
voteChoice: number;
candidateCount: number;
}Lib Exports
const {
// Crypto
mimcHash,
bigIntToHex32,
hex32ToBigInt,
// Merkle Tree
MerkleTree,
// Vote Proof
generateVoteProofInputs,
TREE_DEPTH,
// Tally
RevealTally,
HomomorphicTally,
PaillierCrypto,
} = require('@pqc-quorum/zk-voting-native/lib');MerkleTree
const { MerkleTree, mimcHash } = require('@pqc-quorum/zk-voting-native/lib');
// Create tree
const tree = new MerkleTree(20); // depth 20
// Add leaves
await tree.addLeaf(commitment1);
await tree.addLeaf(commitment2);
// Get root
const root = tree.getRoot();
// Generate proof
const { pathElements, pathIndices } = await tree.generateProof(commitment, index);Examples
Complete Voting Flow
const {
VotingClient,
ProofGenerator,
MerkleTree,
mimcHash,
bigIntToHex32,
} = require('@pqc-quorum/zk-voting-native');
async function completeVotingExample() {
// Setup
const rpcUrl = 'http://localhost:8545';
const adminKey = process.env.ADMIN_PRIVATE_KEY;
const candidates = ['Alice', 'Bob', 'Charlie'];
// Initialize client
const client = new VotingClient(rpcUrl, adminKey);
// Deploy contracts
console.log('Deploying contracts...');
await client.deployVoterRegistry();
await client.deployZKBallot(candidates, 3600, VK_HASH);
await client.deployTallyManager();
// Register voters
const voters = [];
const tree = new MerkleTree(20);
for (let i = 0; i < 10; i++) {
const secret = BigInt(crypto.randomBytes(32).toString('hex'), 16);
const commitment = await mimcHash(secret);
voters.push({ secret, commitment, index: i });
await tree.addLeaf(commitment);
}
const commitments = voters.map(v => bigIntToHex32(v.commitment));
await client.registerVoters(commitments);
await client.closeRegistration();
console.log(`Registered ${voters.length} voters`);
console.log(`Merkle root: ${bigIntToHex32(tree.getRoot())}`);
// Cast votes
for (const voter of voters) {
const voteChoice = Math.floor(Math.random() * candidates.length);
const result = await client.castVote({
voterSecret: voter.secret,
leafIndex: voter.index,
voteChoice,
candidateCount: candidates.length,
});
console.log(`Vote cast: ${result.txHash}`);
}
// End voting and get results
await client.endVoting();
const totalVotes = await client.getTotalVotes();
console.log(`Total votes: ${totalVotes}`);
}Local Testing with Mock Prover
const { VotingClient, MockProofGenerator } = require('@pqc-quorum/zk-voting-native');
// Use mock prover for testing (no Go required)
const client = new VotingClient(rpcUrl, privateKey, {
proofGenerator: new MockProofGenerator(),
});Benchmarks
Run benchmarks with:
npm run benchmarkResults (PQC-Quorum Testnet)
| Operation | Time | Gas | |-----------|------|-----| | Proof Generation | ~200ms | - | | On-chain Verification | - | ~200,000 | | Vote Registration | ~50ms | ~45,000 | | Full Vote Cast | ~300ms | ~250,000 |
Comparison
| Metric | Native (gnark) | snarkjs | |--------|---------------|---------| | Proof Gen | 200ms | 2-5s | | Verify Gas | 200k | 5M+ | | Setup | Pre-deployed | Custom |
Comparison
When to use which package?
| Use Case | Recommended Package |
|----------|---------------------|
| PQC-Quorum node | @pqc-quorum/zk-voting-native |
| Any EVM chain | @pqc/zk-voting |
| Maximum portability | @pqc/zk-voting |
| Maximum performance | @pqc-quorum/zk-voting-native |
| No Go environment | @pqc/zk-voting |
Feature Matrix
| Feature | @pqc/zk-voting | @pqc-quorum/zk-voting-native | |---------|---------------|-------------------------------| | ZK System | Circom/snarkjs | gnark/Groth16 | | Hash Function | Poseidon | MiMC | | Verification | Solidity | Precompile (0x16) | | Proof Generation | JavaScript | Go | | EVM Compatibility | Any | PQC-Quorum | | Gas Cost | ~5M | ~200k | | Trusted Setup | Custom | Pre-deployed | | Homomorphic Tally | ✓ | ✓ | | Reveal-Based Tally | ✓ | ✓ |
Development
# Install dependencies
npm install
# Build Go prover
npm run compile-go
# Run example
npm run example
# Run benchmarks
npm run benchmarkLicense
MIT
Related Packages
- @pqc/zk-voting - Portable ZK voting with snarkjs
- @pqc-quorum/zk-client - ZK RPC client for PQC-Quorum nodes
