Cloak-minster Ballot is a privacy-preserving voting system built on Solana that uses zero-knowledge proofs to ensure voter anonymity while maintaining election integrity. The system combines circom circuits for proof generation, Anchor smart contracts for on-chain logic, and IPFS for decentralized storage.
- System Architecture
- Circom Circuits
- Anchor Instructions
- Off-Chain Components
- On-Chain Verification
- Election Lifecycle
- Privacy Guarantees
- Setup and Usage
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Client App │ │ IPFS Storage │ │ Solana Blockchain│
│ │ │ │ │ │
│ • Proof Gen │◄──►│ • Nullifier │◄──►│ • Smart Contract│
│ • Vote Creation │ │ Trees │ │ • ZK Verification│
│ • Registration │ │ • Spent Trees │ │ • State Storage │
└─────────────────┘ └─────────────────┘ └─────────────────┘
The system operates through three main components:
- Circom Circuits: Generate zero-knowledge proofs for voter registration and voting
- Solana Smart Contract: Verify proofs and manage election state on-chain
- IPFS Storage: Store Merkle trees off-chain for scalability
This circuit generates a unique nullifier for each voter without revealing their identity.
template IdentityNullifier() {
signal input identity_secret; // Private voter secret
signal input election_id; // Public election identifier
signal output nullifier; // Unique voter nullifier
component poseidon = Poseidon(2);
poseidon.inputs[0] <== identity_secret;
poseidon.inputs[1] <== election_id;
nullifier <== poseidon.out;
}
Purpose: Creates a deterministic, unique identifier for each voter per election while keeping their identity private.
Privacy Properties:
- The nullifier links a vote to a registered voter without revealing voter identity
- Different elections produce different nullifiers for the same voter
- Impossible to reverse-engineer the voter's identity from the nullifier
This circuit proves voter membership in the registered voter set without revealing which specific voter is voting.
template Vote(depthD) {
signal input identity_nullifier; // Voter's nullifier
signal input membership_merke_tree_siblings[depthD]; // Merkle proof siblings
signal input membership_merke_tree_path_indices[depthD]; // Merkle proof path
signal output membership_merkle_root; // Computed root
}
Components:
- Membership Proof: Verifies the voter's nullifier exists in the registered voters Merkle tree
- Sparse Non-Membership (commented out): Originally designed to prevent double voting using a spent nullifier tree
Privacy Properties:
- Proves voter eligibility without revealing which registered voter is casting the vote
- Membership proof uses a Merkle tree of all registered voter nullifiers
- The circuit computes the Merkle root from the provided nullifier and proof path
The Solana smart contract implements several key instructions for managing elections:
pub struct Election {
pub admin: Pubkey, // Election administrator
pub name: String, // Election identifier
pub is_registration_open: bool, // Registration phase status
pub is_voting_open: bool, // Voting phase status
pub is_voting_concluded: bool, // Election completion status
pub merkle_root: [u8; 32], // Root of registered voters tree
pub nullifiers_ipfs_cid: String, // IPFS hash of nullifier tree data
pub spent_tree: [u8; 32], // Root of spent nullifiers tree
pub spent_nullifiers_ipfs_cid: String, // IPFS hash of spent tree data
pub options: Vec<String>, // Voting options
pub tallies: Vec<u64>, // Vote counts per option
}
Initializes a new election with specified options and administrator.
Handles voter registration with zero-knowledge proof verification.
pub fn register_voter_handler(
ctx: Context<RegisterVoter>,
name: String,
nullifier: [u8; 32],
proof_a: [u8; 64],
proof_b: [u8; 128],
proof_c: [u8; 64]
) -> Result<()>
Process:
- Verifies the election is in registration phase
- Validates the ZK proof using the register voter verifying key
- Emits a
NullifierAdded
event with the voter's nullifier
Processes votes with membership proof verification.
pub fn vote_handler(
ctx: Context<Vote>,
name: String,
proof_a: [u8; 64],
proof_b: [u8; 128],
proof_c: [u8; 64],
membership_merkle_root: [u8; 32],
new_spent_root: [u8; 32],
spent_nullifiers_ipfs_cid: String,
option: String
) -> Result<()>
Process:
- Verifies the election is in voting phase
- Validates the ZK proof using the vote verifying key
- Checks the voting option exists
- Updates vote tallies and spent nullifier tree
- Emits a
VoteAdded
event
Updates the Merkle root after new voter registrations.
Transitions from registration to voting phase.
Finalizes the election and closes voting.
The system uses IPFS to store Merkle tree data off-chain for scalability:
// Store registered voter nullifiers
const file = JSON.stringify({
depth: 20,
leaves: leaves_g.map(l => "0x" + l.toString('hex'))
});
const { cid } = await ipfs.add({ content: file });
// Store spent voter nullifiers (double-vote prevention)
const file = JSON.stringify({
depth: TREE_DEPTH,
spentLeaves: spent_leaves_hex.map(l => "0x" + l.toString('hex'))
});
const { cid } = await ipfs.add({ content: file });
export async function registerVoter(
secret: Uint8Array,
election_name_str: string,
program: Program<ZkVotingSystem>,
signer: anchor.web3.Keypair
) {
// 1. Generate nullifier proof
const { proof, publicSignals } = await snarkjs.groth16.fullProve({
identity_secret: secretKeyBigInt,
election_id: electionIdBigInt,
}, wasmPath, zkeyPath);
// 2. Submit to smart contract
await program.methods.registerVoter(
election_name,
nullifier,
proofA,
proofB,
proofC
);
// 3. Update IPFS tree data
const tree = new MerkleTree(leaves_g, poseidon);
await ipfs.add({ content: JSON.stringify(treeData) });
}
export async function performVote(
voucher: any,
election_name_str: string,
option: string
) {
// 1. Generate membership proof
const { proof, publicSignals } = await snarkjs.groth16.fullProve({
identity_nullifier: nullifier,
membership_merke_tree_siblings: siblings,
membership_merke_tree_path_indices: pathIndices,
}, wasmPath, zkeyPath);
// 2. Update spent nullifier tree
tree.add(nullifier_bigint, 1n);
// 3. Submit vote to smart contract
await program.methods.vote(
election_name,
proofA, proofB, proofC,
membership_merkle_root,
new_spent_root,
ipfs_cid,
option
);
}
Voters download cryptographic vouchers containing their membership proofs:
export async function downloadVoucher(
secret: Uint8Array,
election_name_str: string
) {
// 1. Regenerate nullifier
const identity_nullifier = generateNullifier(secret, election_id);
// 2. Fetch registered voters from IPFS
const leaves = await getLeavesFromIpfs(ipfs, election.nullifiersIpfsCid);
// 3. Generate Merkle proof
const tree = new MerkleTree(leaves, poseidon);
const proof = tree.getProof(identity_nullifier);
// 4. Return voucher with proof data
return {
election: election_id,
nullifier: identity_nullifier,
merkle_root: tree.getRoot(),
sibling_hashes: proof.siblings,
path_indices: proof.pathIndices
};
}
The system uses Groth16 proof verification on Solana:
pub fn verifier<const N: usize>(
proof_a: [u8; 64],
proof_b: [u8; 128],
proof_c: [u8; 64],
public_inputs: &[[u8; 32]; N],
vk: Groth16Verifyingkey,
) -> Result<()> {
let mut verifier = Groth16Verifier::new(
&proof_a, &proof_b, &proof_c,
public_inputs, &vk
)?;
verifier.verify()?;
Ok(())
}
The system maintains separate verifying keys for different proof types:
REGISTER_VOTER_VERIFYINGKEY
: Verifies identity nullifier generation proofsVOTE_VERIFYINGKEY
: Verifies membership proofs for voting
These keys are generated during the trusted setup phase and embedded in the smart contract.
The system prevents double voting through a spent nullifier tree:
- Registration: Voter nullifiers are added to the membership tree
- Voting: Used nullifiers are added to the spent tree
- Verification: The system checks that a nullifier exists in membership but not in spent trees
Admin → initElection(options[]) → Election Created
Voter → generateNullifier(secret, electionId) → ZK Proof
Voter → registerVoter(proof) → Nullifier added to membership tree
System → updateRoot() → IPFS updated with new tree data
Admin → closeRegistration() → Transition to voting phase
Voter → downloadVoucher(secret) → Membership proof retrieved
Voter → generateVoteProof(voucher) → ZK proof created
Voter → submitVote(proof, option) → Vote counted, spent tree updated
Admin → concludeElection() → Final tallies published
- Identity Hiding: Votes cannot be linked to specific voter identities
- Receipt-Free: Voters cannot prove how they voted to third parties
- Coercion Resistance: External parties cannot force voters to vote in specific ways
- Eligibility: Only registered voters can vote
- Uniqueness: Each voter can vote only once
- Correctness: All votes are counted accurately
- Verifiability: Anyone can verify the election results
- Zero-Knowledge: Proofs reveal nothing beyond statement validity
- Unlinkability: Individual votes cannot be traced to voters
- Forward Secrecy: Past elections remain private even if future secrets are compromised
# Install dependencies
yarn install
# Compile circuits
cd circom
circom identity_nullifier.circom --r1cs --wasm --sym
circom vote.circom --r1cs --wasm --sym
# Generate proving and verifying keys
snarkjs powersoftau new bn128 12 pot12_0000.ptau
snarkjs powersoftau prepare phase2 pot12_0000.ptau pot12_final.ptau
snarkjs groth16 setup identity_nullifier.r1cs pot12_final.ptau identity_nullifier_0000.zkey
snarkjs groth16 setup vote.r1cs pot12_final.ptau vote_0000.zkey
# Build Anchor program
cd ../anchor
anchor build
anchor deploy
// 1. Initialize election
await program.methods.initElection(electionName, options).rpc();
// 2. Register voters
await registerVoter(voterSecret, electionName, program, signer);
// 3. Close registration
await program.methods.closeRegistration(electionName).rpc();
// 4. Voters download vouchers and vote
const voucher = await downloadVoucher(voterSecret, electionName);
await performVote(voucher, electionName, program, signer, "Option 1");
// 5. Conclude election
await program.methods.concludeElection(electionName).rpc();
This system provides a robust, privacy-preserving voting platform that maintains democratic principles while leveraging cutting-edge cryptographic techniques for voter privacy and election integrity.