Zero-Knowledge Proof Systems: A Practical Guide to zk-SNARKs and zk-STARKs
Zero-Knowledge Proof Systems: A Practical Guide to zk-SNARKs and zk-STARKs
How to convince someone you know a secret without revealing the secret itself
The Magic of Mathematical Certainty
Imagine you could prove to your bank that you have enough money for a loan without revealing your actual balance. Or demonstrate to a voting system that you're eligible to vote without disclosing your identity. This isn't science fiction—it's the power of zero-knowledge proofs, and they're revolutionizing how we think about privacy and verification in the digital age.
Zero-knowledge proofs allow one party (the prover) to prove to another party (the verifier) that they know a value x, without conveying any information apart from the fact that they know the value x. Today, we'll explore how this mathematical magic works and build practical applications that leverage this technology.
🔬 The Three Pillars of Zero-Knowledge
Every zero-knowledge proof system must satisfy three fundamental properties:
1. Completeness
If the statement is true, an honest verifier will be convinced by an honest prover.
2. Soundness
If the statement is false, no cheating prover can convince an honest verifier (except with negligible probability).
3. Zero-Knowledge
If the statement is true, the verifier learns nothing other than the fact that the statement is true.
Let's illustrate this with a classic example: the cave of Ali Baba.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/**
* @title AliCaveProof
* @dev A simplified implementation of the Ali Baba cave zero-knowledge proof
* This demonstrates the concept where Peggy (prover) can convince Victor (verifier)
* that she knows the secret word to open the magic door without revealing the word.
*/
contract AliCaveProof {
address public prover;
address public verifier;
enum CavePosition { LeftPath, RightPath }
enum DoorState { Closed, Open }
struct ProofSession {
bytes32 commitment;
CavePosition chosenPath;
CavePosition requestedExit;
bool proofValid;
uint256 round;
bool sessionActive;
}
mapping(address => ProofSession) public sessions;
event ProofSessionStarted(address indexed prover, bytes32 commitment, uint256 round);
event ChallengeIssued(address indexed verifier, CavePosition requestedExit);
event ProofCompleted(address indexed prover, bool success);
modifier onlyProver() {
require(msg.sender == prover, "Only prover can call this");
_;
}
modifier onlyVerifier() {
require(msg.sender == verifier, "Only verifier can call this");
_;
}
constructor(address _prover, address _verifier) {
prover = _prover;
verifier = _verifier;
}
/**
* @dev Prover commits to entering the cave from a specific path
* The commitment is a hash of the path + a nonce to prevent replay attacks
*/
function commitToPath(
bytes32 _commitment,
CavePosition _chosenPath
) external onlyProver {
ProofSession storage session = sessions[prover];
session.commitment = _commitment;
session.chosenPath = _chosenPath;
session.round++;
session.sessionActive = true;
session.proofValid = false;
emit ProofSessionStarted(prover, _commitment, session.round);
}
/**
* @dev Verifier issues a challenge by requesting the prover to exit from a specific path
*/
function issueChallenge(
address _prover,
CavePosition _requestedExit
) external onlyVerifier {
ProofSession storage session = sessions[_prover];
require(session.sessionActive, "No active session");
session.requestedExit = _requestedExit;
emit ChallengeIssued(verifier, _requestedExit);
}
/**
* @dev Prover responds to the challenge by revealing the nonce used in commitment
* If the prover knows the secret, they can exit from any requested path
*/
function respondToChallenge(
uint256 _nonce
) external onlyProver {
ProofSession storage session = sessions[prover];
require(session.sessionActive, "No active session");
// Verify the commitment matches
bytes32 expectedCommitment = keccak256(
abi.encodePacked(session.chosenPath, _nonce, session.round)
);
require(session.commitment == expectedCommitment, "Invalid commitment");
// If prover knows the secret (magic word), they can exit from any path
// For demonstration, we assume they can always provide valid proof
// In reality, this would involve more complex cryptographic verification
session.proofValid = true;
session.sessionActive = false;
emit ProofCompleted(prover, true);
}
/**
* @dev Get the current session status
*/
function getSessionStatus(address _prover) external view returns (
bool active,
uint256 round,
bool valid
) {
ProofSession storage session = sessions[_prover];
return (session.sessionActive, session.round, session.proofValid);
}
}
🛠️ Building with zk-SNARKs: A Practical Example
zk-SNARKs (Zero-Knowledge Succinct Non-Interactive Arguments of Knowledge) are one of the most popular ZK proof systems. Let's build a practical application that proves you're over 18 without revealing your exact age.
// First, let's set up our circuit using Circom
// File: age_verification.circom
pragma circom 2.0.0;
template AgeVerification() {
// Private input: actual birth year
signal private input birthYear;
// Public input: current year
signal input currentYear;
// Public output: 1 if age >= 18, 0 otherwise
signal output isAdult;
// Calculate age
component age = Sub();
age.a <== currentYear;
age.b <== birthYear;
// Check if age >= 18
component gte = GreaterEqualThan(7); // 7 bits can represent up to 127
gte.in[0] <== age.out;
gte.in[1] <== 18;
isAdult <== gte.out;
}
template Sub() {
signal input a;
signal input b;
signal output out;
out <== a - b;
}
template GreaterEqualThan(n) {
assert(n <= 252);
signal input in[2];
signal output out;
component lt = LessThan(n+1);
lt.in[0] <== in[1];
lt.in[1] <== in[0] + 1;
out <== lt.out;
}
component main = AgeVerification();
Now let's implement the smart contract that verifies these proofs:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "./verifier.sol"; // Generated from circom compilation
/**
* @title AgeVerificationZK
* @dev Verifies age using zero-knowledge proofs
* Users can prove they're over 18 without revealing their exact age
*/
contract AgeVerificationZK {
Verifier public immutable verifier;
struct UserVerification {
bool isVerified;
uint256 verificationTime;
bytes32 nullifierHash; // Prevents double verification
}
mapping(address => UserVerification) public verifications;
mapping(bytes32 => bool) public usedNullifiers;
event UserVerified(address indexed user, uint256 timestamp);
event VerificationRevoked(address indexed user);
constructor(address _verifier) {
verifier = Verifier(_verifier);
}
/**
* @dev Verify user's age using zk-SNARK proof
* @param _proof The zk-SNARK proof array [a, b, c]
* @param _nullifierHash Unique hash to prevent double verification
* @param _currentYear Current year as public input
*/
function verifyAge(
uint[2] memory _proof_a,
uint[2][2] memory _proof_b,
uint[2] memory _proof_c,
uint[1] memory _publicSignals, // [currentYear]
bytes32 _nullifierHash
) external {
require(!usedNullifiers[_nullifierHash], "Nullifier already used");
require(!verifications[msg.sender].isVerified, "Already verified");
// Verify the zk-SNARK proof
bool proofValid = verifier.verifyTx(
_proof_a,
_proof_b,
_proof_c,
_publicSignals
);
require(proofValid, "Invalid proof");
// Mark nullifier as used and user as verified
usedNullifiers[_nullifierHash] = true;
verifications[msg.sender] = UserVerification({
isVerified: true,
verificationTime: block.timestamp,
nullifierHash: _nullifierHash
});
emit UserVerified(msg.sender, block.timestamp);
}
/**
* @dev Check if a user is verified as an adult
*/
function isVerifiedAdult(address _user) external view returns (bool) {
return verifications[_user].isVerified;
}
/**
* @dev Admin function to revoke verification (in case of fraud)
*/
function revokeVerification(address _user) external {
// In production, this would have proper access control
require(verifications[_user].isVerified, "User not verified");
delete verifications[_user];
emit VerificationRevoked(_user);
}
}
🌟 Advanced: zk-STARKs Implementation
zk-STARKs (Zero-Knowledge Scalable Transparent Arguments of Knowledge) don't require a trusted setup and are quantum-resistant. Let's build a more advanced example using StarkNet:
// File: fibonacci_proof.cairo
// Proves knowledge of the nth Fibonacci number without revealing intermediate values
%builtins output
from starkware.cairo.common.serialize import serialize_word
func fibonacci_sequence{output_ptr: felt*}(n: felt) -> (result: felt) {
if (n == 0) {
return (result=0);
}
if (n == 1) {
return (result=1);
}
let (a) = fibonacci_sequence(n - 1);
let (b) = fibonacci_sequence(n - 2);
return (result=a + b);
}
func main{output_ptr: felt*}() {
// Private input: n (the position in Fibonacci sequence)
// Public output: F(n) (the nth Fibonacci number)
let n = 10; // This would be a private input in practice
let (result) = fibonacci_sequence(n);
serialize_word(result);
return ();
}
And the corresponding Solidity verifier:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/**
* @title FibonacciStarkVerifier
* @dev Verifies zk-STARK proofs for Fibonacci sequence computations
*/
contract FibonacciStarkVerifier {
struct StarkProof {
uint256[] proof;
uint256[] publicInputs;
bytes32 programHash;
}
mapping(bytes32 => bool) public verifiedComputations;
event ComputationVerified(bytes32 indexed proofHash, uint256 result);
/**
* @dev Verify a STARK proof for Fibonacci computation
* Note: This is a simplified interface. Real STARK verification
* involves complex polynomial arithmetic and FRI protocols.
*/
function verifyFibonacciComputation(
StarkProof memory _proof,
uint256 _expectedResult
) external returns (bool) {
// Generate proof hash for uniqueness
bytes32 proofHash = keccak256(
abi.encodePacked(_proof.proof, _proof.publicInputs, _proof.programHash)
);
require(!verifiedComputations[proofHash], "Computation already verified");
// In a real implementation, this would verify the STARK proof
// using the StarkEx verifier or similar
bool isValid = _verifyStarkProof(_proof, _expectedResult);
if (isValid) {
verifiedComputations[proofHash] = true;
emit ComputationVerified(proofHash, _expectedResult);
}
return isValid;
}
/**
* @dev Internal function to verify STARK proof
* This is a placeholder for the actual STARK verification logic
*/
function _verifyStarkProof(
StarkProof memory _proof,
uint256 _expectedResult
) internal pure returns (bool) {
// Simplified verification logic
// Real STARK verification involves:
// 1. Verifying the execution trace
// 2. Checking polynomial constraints
// 3. Validating FRI (Fast Reed-Solomon Interactive Oracle Proof)
return _proof.publicInputs.length > 0 && _expectedResult > 0;
}
}
🚀 Real-World Applications and Use Cases
1. Private Voting System
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/**
* @title PrivateVoting
* @dev A voting system using zero-knowledge proofs for privacy
*/
contract PrivateVoting {
struct Proposal {
string description;
uint256 yesVotes;
uint256 noVotes;
uint256 deadline;
bool active;
}
struct Vote {
bytes32 nullifierHash;
bool hasVoted;
}
mapping(uint256 => Proposal) public proposals;
mapping(uint256 => mapping(bytes32 => bool)) public usedNullifiers;
mapping(address => bool) public eligibleVoters;
uint256 public proposalCount;
Verifier public immutable verifier;
event ProposalCreated(uint256 indexed proposalId, string description);
event VoteCast(uint256 indexed proposalId, bytes32 nullifierHash);
constructor(address _verifier) {
verifier = Verifier(_verifier);
}
/**
* @dev Cast a vote using zero-knowledge proof
* Proves voter eligibility without revealing identity
*/
function castVote(
uint256 _proposalId,
uint[2] memory _proof_a,
uint[2][2] memory _proof_b,
uint[2] memory _proof_c,
uint[2] memory _publicSignals, // [proposalId, voteChoice]
bytes32 _nullifierHash
) external {
require(proposals[_proposalId].active, "Proposal not active");
require(block.timestamp <= proposals[_proposalId].deadline, "Voting ended");
require(!usedNullifiers[_proposalId][_nullifierHash], "Already voted");
// Verify the zero-knowledge proof
bool proofValid = verifier.verifyTx(
_proof_a,
_proof_b,
_proof_c,
_publicSignals
);
require(proofValid, "Invalid proof");
// Extract vote choice from public signals
uint256 voteChoice = _publicSignals[1]; // 0 = no, 1 = yes
// Record the vote
if (voteChoice == 1) {
proposals[_proposalId].yesVotes++;
} else {
proposals[_proposalId].noVotes++;
}
usedNullifiers[_proposalId][_nullifierHash] = true;
emit VoteCast(_proposalId, _nullifierHash);
}
function createProposal(
string memory _description,
uint256 _votingPeriod
) external returns (uint256) {
uint256 proposalId = proposalCount++;
proposals[proposalId] = Proposal({
description: _description,
yesVotes: 0,
noVotes: 0,
deadline: block.timestamp + _votingPeriod,
active: true
});
emit ProposalCreated(proposalId, _description);
return proposalId;
}
}
📊 Performance Benchmarks and Analysis
Here's a comparison of different ZK proof systems for various use cases:
Proof System | Setup Time | Proof Size | Verification Time | Quantum Resistance | Use Case |
---|---|---|---|---|---|
zk-SNARKs | Trusted Setup Required | ~200 bytes | ~5ms | No | Age verification, voting |
zk-STARKs | No trusted setup | ~45KB | ~16ms | Yes | Complex computations |
Bulletproofs | No trusted setup | ~1.3KB | ~1100ms | No | Range proofs |
PLONK | Universal setup | ~400 bytes | ~10ms | No | General purpose |
Gas Cost Analysis
// Gas costs for different operations (approximate)
contract GasCostAnalysis {
// zk-SNARK verification: ~260,000 gas
// Batch verification (10 proofs): ~2,200,000 gas (220k per proof)
// STARK verification: ~800,000 gas
function estimateVerificationCost(
uint256 proofCount,
uint256 proofType // 0 = SNARK, 1 = STARK
) external pure returns (uint256 gasCost) {
if (proofType == 0) {
// zk-SNARK
gasCost = proofCount * 260000;
if (proofCount > 1) {
// Batch discount
gasCost = gasCost * 85 / 100;
}
} else {
// zk-STARK
gasCost = proofCount * 800000;
}
}
}
🎯 Best Practices and Security Considerations
1. Nullifier Management
contract SecureNullifierManager {
mapping(bytes32 => bool) private usedNullifiers;
mapping(address => uint256) private userNonces;
function generateNullifier(
address user,
uint256 action,
uint256 timestamp
) internal view returns (bytes32) {
return keccak256(abi.encodePacked(
user,
action,
timestamp,
userNonces[user],
block.chainid
));
}
function useNullifier(bytes32 nullifier) internal {
require(!usedNullifiers[nullifier], "Nullifier already used");
usedNullifiers[nullifier] = true;
userNonces[msg.sender]++;
}
}
🌟 Future Directions and Emerging Technologies
The field of zero-knowledge proofs is rapidly evolving. Here are some exciting developments to watch:
- Recursive SNARKs: Proving that you've verified other proofs
- Universal SNARKs: One-time setup for all circuits
- Quantum-Resistant Proofs: Preparing for post-quantum cryptography
- Hardware Acceleration: Specialized chips for proof generation
- zkEVMs: Running entire Ethereum virtual machines in zero-knowledge
🎯 Conclusion
Zero-knowledge proofs represent a fundamental shift in how we approach privacy and verification in digital systems. From simple age verification to complex financial compliance, these cryptographic primitives enable us to build systems that are both transparent and private.
The key to successful ZK implementation lies in understanding the trade-offs between different proof systems and choosing the right tool for your specific use case. Whether you're building a private voting system, implementing confidential transactions, or creating privacy-preserving identity verification, zero-knowledge proofs provide the mathematical foundation for a more private and secure digital future.
As the technology matures and tools become more accessible, we can expect to see ZK proofs become as commonplace as digital signatures are today. The magic of proving knowledge without revealing secrets is not just a mathematical curiosity—it's the foundation of the next generation of privacy-preserving applications.
Remember: with great cryptographic power comes great responsibility. Always audit your circuits, validate your implementations, and consider the broader implications of the privacy systems you're building.
Wang Yinneng
Senior Golang Backend & Web3 Developer with 10+ years of experience building scalable systems and blockchain solutions.
View Full Profile →