
Introduction to Zero-Knowledge Proofs in Web3
Why Zero-Knowledge Proofs Matter
As a mathematician working in Web3, the intersection of cryptography and state verification is where abstract algebra stops being a textbook exercise and starts doing real work. Blockchains are designed around transparency: every node re-executes every transaction to reach consensus. That transparency is a feature for auditability, but a bug for privacy and scalability.
Zero-knowledge proofs (ZKPs) resolve that tension. They let a prover convince a verifier that a statement is true—without revealing why it is true. A rollup can prove it executed thousands of transactions correctly while posting only a compact proof on-chain. A user can prove they meet an eligibility criterion without disclosing their identity. A protocol can verify a computation ran correctly without any party seeing the inputs.
This post walks through the mathematics at a high level, then connects it to what backend engineers actually build: proof generation pipelines, verification services, and the on-chain contracts that consume the results.
The Three Properties
A zero-knowledge proof system for a relation —where is a public input (the statement) and is a private input (the witness)—must satisfy three properties:
- Completeness. If , an honest prover can convince an honest verifier.
- Soundness. A cheating prover cannot convince the verifier of a false statement, except with negligible probability.
- Zero-knowledge. The verifier learns nothing about beyond the fact that some valid witness exists.
These are not philosophical niceties. They are precise security definitions. Completeness guarantees the system works for legitimate users. Soundness is what prevents fraud. Zero-knowledge is what makes the proof private rather than a leaky disclosure.
A Concrete Starting Point: Discrete Logarithms
Before jumping to SNARKs, it helps to see a minimal example. Consider a cyclic group of prime order , with generator . Given a public element for some secret exponent , can you prove you know without revealing it?
This is exactly the setting behind expressions like
where exponentiation in a group acts as a one-way map: easy to compute forward, hard to invert (the discrete logarithm problem). In Web3, might be an elliptic curve group rather than , but the algebraic structure is the same.
The Schnorr protocol solves this interactively:
- The prover picks random , sends commitment .
- The verifier sends random challenge .
- The prover responds with .
- The verifier checks .
If you expand , the algebra checks out. The verifier learns that the prover knows , but the response is uniformly random for any fixed challenge—so remains hidden. This is zero-knowledge in its cleanest form.
From Interactive to Non-Interactive
Schnorr is elegant, but blockchains cannot run interactive challenge–response protocols between prover and verifier. You need a single message: a non-interactive proof.
The Fiat–Shamir transform replaces the verifier’s random challenge with a hash of the transcript:
Hashing behaves like a random oracle in practice, so the prover cannot choose after seeing . This trick—making an interactive protocol non-interactive via hashing—is the foundation of most deployed ZK systems.
zk-SNARKs: Proving General Computations
Schnorr proves a single algebraic relation. Real applications need to prove arbitrary computation: “I executed this EVM transition correctly,” or “These encrypted inputs satisfy this circuit.” That is where zk-SNARKs enter.
SNARK stands for Succinct Non-interactive ARgument of Knowledge. Breaking that down:
- Succinct — Proofs are small (hundreds of bytes to a few kilobytes) and cheap to verify, even when the computation being proved is enormous.
- Non-interactive — One proof, one verification. No back-and-forth.
- Argument — Security holds under cryptographic assumptions (not information-theoretic), typically in a random oracle or idealized group model.
- Knowledge — If you can produce a valid proof, you must “know” a witness. You cannot forge one without the underlying secret.
The Pipeline: From Program to Proof
At a high level, a SNARK workflow looks like this:
-
Write a circuit. Express the computation you want to prove as a set of arithmetic constraints over a finite field . Tools like Circom, Noir, or Cairo compile high-level code into these constraints.
-
Trusted setup (for some systems). Schemes like Groth16 require a structured reference string (SRS) generated once for a given circuit. If the setup’s toxic waste is destroyed, the system remains secure. Newer schemes like PLONK or STARKs reduce or eliminate per-circuit setup.
-
Generate the proof. The prover takes public inputs , private witness , and the proving key, then runs a heavy computation—often seconds to minutes depending on circuit size.
-
Verify the proof. The verifier takes public inputs , the proof , and the verification key, then runs a lightweight check—typically a handful of elliptic curve pairings or field multiplications.
The asymmetry is the whole point: proving is expensive, verifying is cheap. On Ethereum, verifying a Groth16 proof on-chain costs on the order of ~200k–300k gas; generating that same proof off-chain might take a beefy server several seconds.
R1CS and Polynomial Commitments (Briefly)
Most SNARKs reduce the circuit to a Rank-1 Constraint System (R1CS). Each constraint has the form
where is a vector of all variables (public inputs, private witnesses, and intermediate values). The prover’s job is to demonstrate that a satisfying assignment exists without revealing the private entries of .
Modern systems (PLONK, Halo2) wrap these constraints in polynomial commitment schemes—KZG commitments, FRI for STARKs—so the verifier checks polynomial identities rather than individual gate evaluations. The details vary by protocol, but the pattern is consistent: encode computation as algebra, then prove algebraic relations succinctly.
Backend Verification: Where Engineering Meets Mathematics
On paper, ZKPs are pure cryptography. In production, they are backend systems with latency budgets, failure modes, and versioning problems.
The Verification Service
A typical architecture separates concerns:
Client / Prover Backend Chain
─────────────── ─────── ─────
Build witness ──► Proof generation API
Generate proof ──► (worker pool, GPU/CPU)
Submit proof + inputs ──► Verification service ──► Smart contract
(sanity checks, caching) (final authority)
The verification service sits between provers and the chain. Its responsibilities:
- Validate inputs before they hit expensive on-chain verification. Reject malformed proofs early.
- Cache verification keys per circuit version. When you deploy circuit v2, you need vk v2 available everywhere.
- Rate-limit and queue proof submissions. Proof generation is bursty; verification should be steady.
- Log and monitor proof sizes, generation times, and failure rates. A sudden spike in invalid proofs is a security signal.
The smart contract remains the ultimate verifier for trustlessness, but running verification off-chain first saves gas and gives you observability.
Example: Verifying a Groth16 Proof in Code
Groth16 proofs consist of three elliptic curve points . Verification checks a pairing equation:
where is derived from the public inputs and the verification key, and is a bilinear pairing. Backend libraries (snarkjs, arkworks, gnark) implement this. Your service wraps them:
async function verifyProof(
publicInputs: bigint[],
proof: { A: G1Point; B: G2Point; C: G1Point },
verificationKey: VerificationKey
): Promise<boolean> {
// 1. Deserialize and validate point encoding
// 2. Compute L = vk.IC[0] + Σ(publicInputs[i] * vk.IC[i+1])
// 3. Run pairing check
// 4. Return result (never crash on malformed input — return false)
}
Defensive parsing matters. Proofs are untrusted input. Malformed curve points, wrong lengths, or inputs that don’t match the circuit should all fail gracefully.
Circuit Versioning and Migration
Circuits change. You fix a bug, add a field, optimize constraints. Each change produces a new verification key and breaks compatibility with old proofs. Backend systems need explicit circuit version headers in proof payloads:
{
"circuitId": "transfer-v3",
"publicInputs": ["0x..."],
"proof": { "A": "...", "B": "...", "C": "..." }
}
Your verifier loads the correct vk by circuitId. Your on-chain contract may only accept the latest version, or maintain a whitelist of still-valid versions during migration windows.
Web3 Applications
Layer-2 Rollups
ZK rollups batch thousands of L2 transactions, prove their correctness in a SNARK, and post the proof to L1. Validity is cryptographic, not economic—unlike optimistic rollups, there is no challenge period. Projects like zkSync, Starknet, and Polygon zkEVM all sit on this model, with different proof systems and VM designs.
Privacy
Tornado Cash (before sanctions), Zcash, and various identity protocols use ZKPs to break the link between actor and action. You prove “I am in the set of eligible participants” or “I know the nullifier for this note” without revealing which participant you are.
Verifiable Off-Chain Computation
Oracles, AI inference, and game engines can produce proofs that their output is correct. The chain verifies the proof rather than re-running the computation. This is still early, but the math is ready—the engineering is catching up.
Tradeoffs Worth Knowing
| Approach | Proof size | Prover time | Trusted setup | Post-quantum |
|---|---|---|---|---|
| Groth16 | ~200 bytes | Fast | Per-circuit | No |
| PLONK | ~1 KB | Moderate | Universal SRS | No |
| STARKs | ~100 KB+ | Moderate | None | Yes |
Groth16 remains popular for fixed circuits where minimal proof size matters (on-chain verification cost). PLONK-family systems offer flexibility—one setup, many circuits—with slightly larger proofs. STARKs avoid trusted setup and resist quantum attacks, at the cost of larger proofs (though verification stays fast).
For backend design, prover time dominates user experience. Parallelizing witness generation, using GPU acceleration, and choosing the right field size for your hardware all matter as much as the choice of proof system.
Conclusion
Zero-knowledge proofs are where backend engineering meets pure mathematics. The verifier checks a few equations; behind those equations sits a pipeline of circuit compilation, witness generation, pairing arithmetic, and careful input validation.
If you are building in this space, invest in three things: understand the algebraic primitives (groups, fields, pairings), treat proof verification as an untrusted-input API with strict schemas and versioning, and measure everything—prover latency, proof size, verification gas, invalid-proof rates.
The math guarantees soundness. The engineering guarantees it ships.