Skip to main content

Command Palette

Search for a command to run...

Zero-Knowledge Proofs: Privacy-Preserving Verification

zk-SNARKs and zk-STARKs for blockchain and authentication systems

Published
8 min read
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Content Role: pillar

Zero-Knowledge Proofs: Privacy-Preserving Verification

zk-SNARKs and zk-STARKs for blockchain and authentication systems

The Privacy Paradox in Modern Systems

Modern distributed systems face a fundamental challenge: how do you prove you know something without revealing what you know? Whether it's verifying account balances without exposing transaction history, authenticating users without storing passwords, or validating computations without re-executing them, the need for privacy-preserving verification has become critical.

Traditional verification methods require exposing the underlying data. When you prove you have sufficient funds for a transaction, you reveal your balance. When you authenticate to a service, you transmit credentials that could be intercepted. When you verify a computation, you must share the inputs and re-run the entire process.

Zero-knowledge proofs (ZKPs) solve this paradox by enabling one party (the prover) to convince another party (the verifier) that a statement is true without revealing any information beyond the validity of the statement itself. This cryptographic primitive has evolved from theoretical curiosity to practical implementation in blockchain systems, authentication protocols, and verifiable computation frameworks.

Understanding Zero-Knowledge Proof Fundamentals

A zero-knowledge proof must satisfy three properties:

Completeness: If the statement is true and both parties follow the protocol honestly, the verifier will be convinced.

Soundness: If the statement is false, no cheating prover can convince the verifier except with negligible probability.

Zero-knowledge: If the statement is true, the verifier learns nothing beyond the fact that the statement is true.

The two dominant approaches to zero-knowledge proof implementation are zk-SNARKs (Zero-Knowledge Succinct Non-Interactive Arguments of Knowledge) and zk-STARKs (Zero-Knowledge Scalable Transparent Arguments of Knowledge).

zk-SNARKs: Succinct and Efficient

zk-SNARKs produce small proofs (typically 200-300 bytes) that verify quickly, making them ideal for blockchain applications where on-chain storage and computation are expensive. However, they require a trusted setup ceremony and rely on elliptic curve cryptography assumptions.

zk-STARKs: Transparent and Scalable

zk-STARKs eliminate the trusted setup requirement and rely only on collision-resistant hash functions, making them quantum-resistant. They generate larger proofs but scale better for complex computations.

Implementing Zero-Knowledge Proofs

Let's explore practical zero-knowledge proof implementation using modern tools and libraries.

Basic ZKP Circuit with SnarkJS

First, install the required dependencies:

npm install snarkjs circomlib
npm install --save-dev circom

Define a simple circuit that proves knowledge of a number whose square equals a public value:

// square.circom
pragma circom 2.0.0;

template Square() {
    signal input x;
    signal output y;

    y <== x * x;
}

component main = Square();

Compile and generate the proving and verification keys:

import { groth16 } from 'snarkjs';
import * as fs from 'fs';

async function setupCircuit() {
    // Compile circuit (typically done via CLI)
    // circom square.circom --r1cs --wasm --sym

    // Generate proving key
    const { zkey } = await groth16.setup(
        'square.r1cs',
        'powersOfTau28_hez_final_10.ptau'
    );

    fs.writeFileSync('square_final.zkey', zkey);
}

async function generateProof(privateInput: number, publicOutput: number) {
    const input = {
        x: privateInput
    };

    const { proof, publicSignals } = await groth16.fullProve(
        input,
        'square.wasm',
        'square_final.zkey'
    );

    return { proof, publicSignals };
}

async function verifyProof(proof: any, publicSignals: any) {
    const vKey = JSON.parse(
        fs.readFileSync('verification_key.json', 'utf-8')
    );

    const verified = await groth16.verify(vKey, publicSignals, proof);
    return verified;
}

Authentication System with ZKP

Implement a password-less authentication system where users prove knowledge of credentials without transmitting them:

import { buildPoseidon } from 'circomlibjs';
import { groth16 } from 'snarkjs';

class ZKAuthSystem {
    private poseidon: any;

    async initialize() {
        this.poseidon = await buildPoseidon();
    }

    // User registration: store only the hash
    async register(username: string, password: string): Promise<string> {
        const passwordHash = this.poseidon.F.toString(
            this.poseidon([Buffer.from(password)])
        );

        // Store username -> passwordHash mapping
        await this.storeCredential(username, passwordHash);
        return passwordHash;
    }

    // User login: prove knowledge of password without revealing it
    async login(username: string, password: string): Promise<boolean> {
        const storedHash = await this.getStoredHash(username);

        // Generate proof that password hashes to storedHash
        const input = {
            password: Buffer.from(password),
            expectedHash: storedHash
        };

        const { proof, publicSignals } = await groth16.fullProve(
            input,
            'auth_circuit.wasm',
            'auth_circuit.zkey'
        );

        // Verify proof server-side
        return await this.verifyAuthProof(proof, publicSignals, storedHash);
    }

    private async verifyAuthProof(
        proof: any,
        publicSignals: any,
        expectedHash: string
    ): Promise<boolean> {
        // Verify the proof is valid
        const vKey = await this.getVerificationKey();
        const isValid = await groth16.verify(vKey, publicSignals, proof);

        // Verify the public output matches stored hash
        const matchesHash = publicSignals[0] === expectedHash;

        return isValid && matchesHash;
    }

    private async storeCredential(username: string, hash: string) {
        // Implementation depends on your database
    }

    private async getStoredHash(username: string): Promise<string> {
        // Implementation depends on your database
        return '';
    }

    private async getVerificationKey() {
        // Load verification key
        return {};
    }
}

Blockchain Privacy with ZKP

Implement a private transaction system where users can prove sufficient balance without revealing the exact amount:

interface PrivateTransaction {
    sender: string;
    recipient: string;
    encryptedAmount: string;
    proof: any;
}

class PrivateBlockchain {
    async createPrivateTransaction(
        senderBalance: bigint,
        amount: bigint,
        recipient: string
    ): Promise<PrivateTransaction> {
        // Verify sender has sufficient balance
        if (senderBalance < amount) {
            throw new Error('Insufficient balance');
        }

        // Create proof that balance >= amount without revealing either
        const input = {
            balance: senderBalance.toString(),
            amount: amount.toString(),
            threshold: amount.toString()
        };

        const { proof, publicSignals } = await groth16.fullProve(
            input,
            'balance_check.wasm',
            'balance_check.zkey'
        );

        // Encrypt the amount for the recipient
        const encryptedAmount = await this.encryptForRecipient(
            amount,
            recipient
        );

        return {
            sender: await this.getPublicKey(),
            recipient,
            encryptedAmount,
            proof
        };
    }

    async verifyPrivateTransaction(
        tx: PrivateTransaction
    ): Promise<boolean> {
        const vKey = await this.getVerificationKey('balance_check');

        // Verify the proof without knowing the actual balance or amount
        return await groth16.verify(vKey, tx.proof.publicSignals, tx.proof);
    }

    private async encryptForRecipient(
        amount: bigint,
        recipient: string
    ): Promise<string> {
        // Implement encryption using recipient's public key
        return '';
    }

    private async getPublicKey(): Promise<string> {
        return '';
    }

    private async getVerificationKey(circuit: string) {
        return {};
    }
}

Common Pitfalls in Zero-Knowledge Proof Implementation

Trusted Setup Vulnerabilities

The trusted setup ceremony for zk-SNARKs is critical. If the toxic waste (random values used during setup) is not properly destroyed, it can be used to create false proofs. Always use multi-party computation (MPC) ceremonies with numerous participants.

Circuit Constraint Bugs

Underconstrained circuits allow malicious provers to generate valid proofs for false statements. Always audit circuits thoroughly and use constraint counting tools:

// Bad: Underconstrained
signal input a;
signal input b;
signal output c;
c <== a + b; // Missing constraint that c actually equals a + b

// Good: Properly constrained
signal input a;
signal input b;
signal output c;
c <-- a + b;
c === a + b; // Explicit constraint

Information Leakage Through Public Inputs

Public inputs are visible to everyone. Ensure sensitive data is never exposed as public inputs:

// Bad: Password as public input
const input = {
    password: userPassword, // Exposed!
    publicKey: userPublicKey
};

// Good: Only hash as public input
const input = {
    passwordHash: hash(userPassword), // Private
    expectedHash: storedHash // Public
};

Performance Bottlenecks

Circuit complexity directly impacts proving time. Optimize by minimizing constraints:

// Bad: Multiple hash operations
for (let i = 0; i < 1000; i++) {
    hash = poseidon([hash, data[i]]);
}

// Good: Batch operations
hash = poseidon([hash, ...data]);

Replay Attack Vulnerabilities

Always include nonces or timestamps to prevent proof reuse:

const input = {
    secret: userSecret,
    nonce: Date.now(),
    expectedHash: storedHash
};

Best Practices Checklist

  • [ ] Use established libraries (SnarkJS, Circom, libsnark) rather than implementing cryptography from scratch
  • [ ] Conduct multi-party trusted setup ceremonies for zk-SNARKs with at least 50+ participants
  • [ ] Audit circuits with automated tools (circom-mutator, picus) and manual review
  • [ ] Implement constraint counting tests to detect underconstrained circuits
  • [ ] Use zk-STARKs for quantum resistance and transparent setup when proof size is not critical
  • [ ] Include nonces, timestamps, or commitment schemes to prevent replay attacks
  • [ ] Minimize public inputs to prevent information leakage
  • [ ] Optimize circuit complexity by batching operations and using efficient hash functions
  • [ ] Implement proper error handling for proof generation and verification failures
  • [ ] Monitor proving times and set reasonable timeouts
  • [ ] Version your circuits and maintain backward compatibility for verification keys
  • [ ] Document circuit logic and security assumptions clearly

Frequently Asked Questions

Q: When should I use zk-SNARKs versus zk-STARKs?

Use zk-SNARKs when proof size and verification speed are critical, such as blockchain applications where on-chain storage is expensive. Choose zk-STARKs when you need transparent setup, quantum resistance, or are proving very complex computations where their better scaling characteristics outweigh larger proof sizes.

Q: How long does it take to generate a zero-knowledge proof?

Proving time varies dramatically based on circuit complexity. Simple circuits (100-1000 constraints) generate proofs in milliseconds. Complex circuits (1M+ constraints) can take minutes to hours. Verification is typically fast (5-50ms) regardless of circuit complexity for zk-SNARKs.

Q: Can zero-knowledge proofs be used for machine learning model verification?

Yes, ZKPs can prove that a model was correctly executed on specific inputs without revealing the model weights or input data. However, neural networks require millions of constraints, making proving times impractical for real-time applications. Research into specialized ZK-ML systems is ongoing.

Q: Are zero-knowledge proofs quantum-resistant?

zk-STARKs are quantum-resistant as they rely only on collision-resistant hash functions. zk-SNARKs based on elliptic curve pairings are vulnerable to quantum attacks. For post-quantum security, use zk-STARKs or lattice-based ZKP systems.

Q: How do I debug a failing zero-knowledge proof?

Start by verifying your circuit logic with known inputs. Use witness generation tools to inspect intermediate signals. Check that all constraints are satisfied. Verify public inputs match expected values. Use circuit testing frameworks like circom-tester to automate validation.

Q: What's the difference between interactive and non-interactive zero-knowledge proofs?

Interactive proofs require multiple rounds of communication between prover and verifier. Non-interactive proofs (like zk-SNARKs and zk-STARKs) require only a single message from prover to verifier, making them suitable for blockchain and asynchronous systems.

Q: How much does it cost to verify a zero-knowledge proof on Ethereum?

zk-SNARK verification on Ethereum typically costs 200,000-300,000 gas (approximately $5-15 at moderate gas prices). The cost is constant regardless of computation complexity, making ZKPs economical for verifying expensive computations.