Introduction

Imagine proving to a border guard that your passport is valid — without letting them read your name, nationality, or photo. That impossibly sounds like magic. In 2013 it became engineering.

zk-SNARKs — Zero-Knowledge Succinct Non-Interactive Arguments of Knowledge — are cryptographic proofs with three extraordinary properties at once:

  • Zero-knowledge: the proof leaks nothing about the secret beyond the bare fact that it exists.
  • Succinct: the proof is tiny (a few hundred bytes) and verifies in milliseconds, regardless of how hard the computation was.
  • Non-interactive: there is no back-and-forth dialogue — the prover sends one message and the verifier is done.

The theoretical foundations were laid by Goldwasser, Micali and Rackoff (1989) in their landmark paper on interactive zero-knowledge proofs, work that earned them the Turing Award in 2012. The "succinct non-interactive" variant that powers today's blockchains was made practical by Groth (2016) and the Zcash team.

These proofs sit squarely inside the crypto complexity class — proven secure under plausible hardness assumptions, requiring a one-time trusted setup, and transforming any NP statement into a compact, verifiable secret.

Try It

This demo captures the spirit of a zk-SNARK using a simple analogy: Sudoku-style commitment. You pick a secret number, commit to it with a hash, then prove your committed value satisfies a constraint — without ever revealing the number itself.

<p class="hint">Pick a secret number (1–20). The prover commits to it with a hash, then generates a proof that the committed value is <b>even</b> — without revealing the number. The verifier checks the proof using only the commitment.</p>
<div class="panel">
  <label>Your secret number: <input id="secret" type="number" min="1" max="20" value="4" /></label>
  <button id="commit" type="button">1. Commit</button>
</div>
<div class="panel" id="commitOut" style="display:none">
  <div class="tag">Commitment (public)</div>
  <code id="hashDisplay"></code>
  <div class="tag" style="margin-top:.6rem">What the verifier sees: commitment + proof response only</div>
</div>
<div class="panel" id="provePanel" style="display:none">
  <button id="prove" type="button">2. Generate proof (prover)</button>
</div>
<div class="panel" id="proofOut" style="display:none">
  <div class="tag">Proof (public)</div>
  <code id="proofDisplay"></code>
</div>
<div class="panel" id="verifyPanel" style="display:none">
  <button id="verify" type="button">3. Verify (verifier)</button>
</div>
<div class="status" id="status"></div>
<div class="panel" id="explainPanel" style="display:none">
  <div class="tag">What the verifier learned</div>
  <div id="explain"></div>
</div>
<button id="reset" type="button" class="ghost" style="margin-top:.8rem">Reset</button>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.panel { background: #f0f4f8; border: 1px solid #cdd9e3; border-radius: 10px;
         padding: .65rem .85rem; margin-bottom: .55rem; }
.tag { font-size: .75rem; font-weight: 700; color: #5a7088; text-transform: uppercase;
       letter-spacing: .04em; margin-bottom: .3rem; }
label { font-size: .9rem; display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
input[type=number] { width: 4rem; padding: .3rem .4rem; font-size: .9rem;
                     border: 1px solid #adb1b8; border-radius: 6px; }
code { font-family: ui-monospace, monospace; font-size: .78rem; word-break: break-all;
       color: #1d3557; display: block; }
.status { font-size: 1rem; font-weight: 700; min-height: 1.5em; margin: .4rem 0; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
#explainPanel { font-size: .85rem; line-height: 1.5; }
#explainPanel b { color: #1d3557; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
         border: 1px solid #1d3557; background: #1d3557; color: #fff;
         border-radius: 8px; cursor: pointer; margin-top: .4rem; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Zero-knowledge commitment demo (conceptual / simplified).
// The "commitment" is a deterministic hash (djb2 + salt) of the secret.
// The "proof" is a single response bit that encodes parity without exposing the value.
// This captures the essence of a zk-SNARK: commit → prove → verify,
// where the verifier learns ONLY that the statement "secret is even" is true.

// ---------- Simple deterministic hash (djb2 over string) ----------
function djb2(s) {
  let h = 5381;
  for (let i = 0; i < s.length; i++) h = ((h * 33) ^ s.charCodeAt(i)) >>> 0;
  return h.toString(16).padStart(8, '0');
}

// Fixed public salt (represents the trusted setup / common reference string)
const SALT = 'kipu-zk-crs-2024';

function makeCommitment(secret) {
  // commitment = hash(secret || salt)
  return djb2(secret.toString() + SALT);
}

function makeProof(secret) {
  // The "proof" encodes parity without revealing the value.
  // In a real zk-SNARK this is an elliptic curve point; here we simulate with
  // a derived value that encodes parity and is tied to the commitment.
  const parity = secret % 2; // 0 = even, 1 = odd
  // proof = hash(parity || salt || commitment) — binds proof to commitment
  const commitment = makeCommitment(secret);
  const proofHash = djb2(parity.toString() + SALT + commitment);
  return { parity, proofHash };
}

function verifyProof(commitment, proof) {
  // Verifier re-derives expected proof hash for parity=0 (even claim)
  const expectedHash = djb2('0' + SALT + commitment);
  return proof.proofHash === expectedHash && proof.parity === 0;
}

// ---------- UI ----------
let currentCommitment = null;
let currentProof = null;

function show(id) { document.getElementById(id).style.display = ''; }
function setStatus(t, cls) {
  const s = document.getElementById('status');
  s.textContent = t; s.className = 'status ' + (cls || '');
}

document.getElementById('commit').addEventListener('click', () => {
  const raw = parseInt(document.getElementById('secret').value, 10);
  if (isNaN(raw) || raw < 1 || raw > 20) {
    setStatus('Pick a number between 1 and 20.', 'bad'); return;
  }
  currentCommitment = makeCommitment(raw);
  currentProof = null;
  document.getElementById('hashDisplay').textContent = fmt('0x%currentCommitment%  (hash of secret + salt)', { currentCommitment: currentCommitment });
  show('commitOut');
  show('provePanel');
  document.getElementById('proofOut').style.display = 'none';
  document.getElementById('verifyPanel').style.display = 'none';
  document.getElementById('explainPanel').style.display = 'none';
  setStatus('');
  console.log('Commitment created. Secret:', raw, '|', 'Commitment:', currentCommitment);
});

document.getElementById('prove').addEventListener('click', () => {
  const raw = parseInt(document.getElementById('secret').value, 10);
  if (!currentCommitment) { setStatus('Commit first.', 'bad'); return; }
  currentProof = makeProof(raw);
  const proofStr = 'parity_bit=' + currentProof.parity + '  proof_hash=0x' + currentProof.proofHash;
  document.getElementById('proofDisplay').textContent = proofStr;
  show('proofOut');
  show('verifyPanel');
  setStatus('Proof generated. The verifier receives commitment + proof only — not your secret.');
  console.log('Proof:', proofStr, '|', 'Secret NOT in proof');
});

document.getElementById('verify').addEventListener('click', () => {
  if (!currentCommitment || !currentProof) { setStatus('Generate proof first.', 'bad'); return; }
  const valid = verifyProof(currentCommitment, currentProof);
  if (valid) {
    setStatus('✓ Proof verified: the committed value is even!', 'ok');
    document.getElementById('explain').innerHTML = 'The verifier confirmed the secret is <b>even</b>. They did <b>not</b> learn the actual number — only that it satisfies the constraint. That is zero-knowledge: sound (cannot fake the parity) + private (secret stays hidden).';
    show('explainPanel');
  } else {
    setStatus('✗ Proof rejected: committed value is not even, or proof is invalid.', 'bad');
    document.getElementById('explain').innerHTML = 'The proof failed. Either the secret is <b>odd</b>, or the proof was tampered with. Try committing to an even number (2, 4, 6 …).';
    show('explainPanel');
  }
  console.log('Verification result:', valid, '|', 'Commitment:', currentCommitment);
});

document.getElementById('reset').addEventListener('click', () => {
  currentCommitment = null; currentProof = null;
  ['commitOut','provePanel','proofOut','verifyPanel','explainPanel'].forEach(id => {
    document.getElementById(id).style.display = 'none';
  });
  setStatus('');
  document.getElementById('secret').value = 4;
});
Result
Console

Notice the key asymmetry. The verifier sees only your commitment (a hash) and your proof response. They can confirm you know a valid secret — but they learn nothing about which secret you chose. That is zero-knowledge in action: the proof is computationally sound (you can't fake it) yet perfectly private (the verifier gains no information beyond "yes, it works").

The Real Complexity

A zk-SNARK is not one trick — it is a chain of beautiful reductions:

Step 1 — Arithmetize the statement. Any NP computation can be written as a circuit of addition and multiplication gates over a finite field. The prover must convince the verifier that they know an input making every gate consistent.

Step 2 — Flatten to a QAP. The circuit is encoded as a Quadratic Arithmetic Program (introduced by Gennaro, Gentry, Parno & Raykova, 2013). Gate satisfiability becomes a polynomial divisibility check: does a polynomial h(x) times a target polynomial t(x) equal p(x)? If yes, all gates are consistent.

Step 3 — Evaluate blindly via elliptic curves. The prover needs to evaluate those polynomials at a secret random point τ — but must do it without knowing τ. This is where the trusted setup (or ceremony) comes in: it computes encrypted powers of τ on an elliptic curve and destroys τ afterward. Anyone who keeps τ can forge proofs — this is the notorious "toxic waste" problem.

Step 4 — Pair-based verification. Using bilinear pairings on elliptic curves (the same mathematical tool that enables identity-based encryption), the verifier checks the polynomial relationship in encrypted form. The full proof is just a handful of curve points — under 200 bytes for Groth16.

The catch — trusted setup: the ceremony must be run honestly by at least one participant. The Zcash team ran a multi-party ceremony in 2016 with six geographically separated participants; if even one destroyed their share, the setup is safe. Newer systems like PLONK (2019) use a universal trusted setup reusable across many programs, while STARKs eliminate the setup entirely (at the cost of larger proofs).

The security rests on the knowledge-of-exponent assumption and the hardness of the discrete logarithm problem on the chosen curve — well-studied assumptions, though not reduction-proved from a classical hardness result.

Where It Matters

zk-SNARKs started in cryptography theory and have become critical infrastructure:

  • Private cryptocurrencies: Zcash was the first blockchain to use zk-SNARKs (Groth16) so that transaction amounts and addresses are hidden on-chain. You can prove you spent a coin you owned without revealing which coin.
  • Ethereum scaling (zkRollups): zkSync, StarkNet, Polygon zkEVM and others bundle thousands of transactions into a single zk-proof, post it to Ethereum, and inherit Ethereum's security — at a fraction of the gas cost. Rollups are now the dominant Ethereum scaling path.
  • Digital identity: zk-proofs can attest "this person is over 18" or "this passport is valid" without transmitting the underlying data — the dream of privacy-preserving KYC.
  • Verifiable computation: outsource heavy computation to an untrusted server and get a proof that the result is correct, verified cheaply on your phone.
  • Zero-knowledge machine learning: prove a machine-learning model produced a certain output (useful for audits) without revealing the model weights.

The connection to complexity theory is direct: because zk-SNARKs work for any NP statement, they can prove anything you can check efficiently. The gap between P and NP is, paradoxically, what makes them useful — verifying is easy, so the proof is small; finding a witness remains hard, so the prover cannot bluff.

Conclusion

zk-SNARKs are one of the most surprising ideas in modern computer science: they turn the hardness of NP into a privacy guarantee. Because finding a witness is hard but verifying one is easy, a prover can commit to a witness and then demonstrate — without revealing it — that the witness makes a statement true.

The math is deep (elliptic curves, bilinear pairings, polynomial commitments), but the core intuition is simple: you can prove you know a secret by showing its effects, not the secret itself.

From Zcash's shielded transactions to Ethereum's rollup revolution, zk-SNARKs are quietly becoming the privacy infrastructure of the internet. And as trusted-setup requirements shrink (PLONK, STARKs) and hardware accelerators make proving faster, the boundary between "cryptographic curiosity" and "everyday tool" is dissolving fast.

The next time an app tells you it verified your age without storing your birthday, there is a good chance a zk-SNARK is behind it — a tiny proof, carrying a secret it will never reveal.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/zk-snarks/Content licensed under CC BY-NC 4.0.