Skip to content

Zero-knowledge in plain English

View Markdown

A zero-knowledge proof lets one party convince another that a statement is true without revealing why it’s true. This page builds the intuition you need before touching any code. There’s no math beyond arithmetic, and it takes about five minutes.

Every zero-knowledge system has two roles:

  • The prover knows a secret and wants to convince someone of a fact about it.
  • The verifier wants to be convinced, but should learn nothing about the secret.
Prover Verifier
┌──────────────┐ ┌──────────────┐
│ secret: 42 │ ── proof ──────────────► │ accepts or │
│ public: 18 │ ── public inputs (18) ─► │ rejects │
└──────────────┘ └──────────────┘
the secret (42) never leaves the prover

In ZeroJ the prover is usually your application or a user’s wallet running Java. The verifier is a backend service (verifying in Java) or a Cardano validator (verifying on-chain).

You have two balls that look identical to your friend, who is colour-blind. You claim one is red and one is green. How do you convince your friend without telling them which is which?

  1. Your friend takes both balls and hides them behind their back.
  2. They either swap them or don’t, secretly, and show them to you again.
  3. You say “swapped” or “not swapped”.

If the balls really are different colours, you answer correctly every time. If they’re secretly the same colour, you can only guess, and you’ll be right half the time. After 20 rounds, the chance of a lucky cheater getting every answer right is about one in a million.

Look at what your friend learned: that the balls differ. They never learned which one is red.

That’s the whole idea. The proof convinces without revealing. Real ZK systems replace the balls with math, but the shape is the same.

A zero-knowledge proof convinces the verifier of a statement of this form:

“I know secret values that, together with these public values, satisfy these rules.”

The rules are fixed in advance and agreed by both sides. In ZeroJ they’re a circuit you write in Java. The public values are visible to everyone. The secret values stay with the prover.

For example:

  • Rules: age ≥ threshold. Public: threshold = 18. Secret: age = 42.
  • Rules: hash(password) = H. Public: H. Secret: password.

Every proof system worth using guarantees three things. Here they are in plain terms.

PropertyIn plain EnglishIn the ball game
CompletenessIf the statement is true and the prover is honest, the verifier accepts.You really can tell the colours apart, so you always answer correctly.
SoundnessIf the statement is false, a cheating prover can’t make the verifier accept, except with negligible probability.With identical balls, you can’t keep guessing right.
Zero-knowledgeThe verifier learns nothing except that the statement is true.Your friend never finds out which ball is red.

Soundness is the property attackers target. In practice, the most common soundness failures come from circuits that don’t enforce what their authors intended, not from broken cryptography. Circuits, constraints & witnesses shows how that happens.

The ball game is interactive: the verifier has to be there, flipping coins, round after round. That doesn’t work for a blockchain, where a validator can’t chat with you.

Non-interactive proofs fix this. The prover produces a single proof object that anyone can check later, with no conversation. The verifier’s random challenges come from somewhere both sides can compute instead:

  • a hash of everything said so far (the Fiat–Shamir technique, used by PlonK and BBS), or
  • a structured setup created in advance (the trusted setup, which Groth16 relies on).

The proofs ZeroJ generates by default are zk-SNARKs:

LetterStands forMeaning
SSuccinctThe proof is tiny and quick to check, no matter how big the computation was. A Groth16 proof on BLS12-381 is 192 bytes.
NNon-interactiveOne message from prover to verifier.
ARARgumentSoundness holds against computationally bounded cheaters. That’s the standard assumption for real-world cryptography.
Kof KnowledgeThe prover must actually know a valid secret, not just show that one exists.

Deciding what’s public and what’s secret is the first design decision in any ZK application.

You want to prove…Secret inputsPublic inputs
I’m at least 18your age or birth datethe threshold (18), and usually a commitment or issuer signature that ties your age to something real
My balance is at least Xyour balanceX, and a commitment to your balance
I’m on the allowlistyour leaf in a Merkle tree and the path to the rootthe Merkle root of the list
I know the passwordthe passwordits hash
I own this Cardano addressyour root keythe address’s payment key hash

The verifier sees the public inputs in full. Everything on the secret side is covered by the zero-knowledge property.

ZK is powerful, but it’s easy to expect too much from it.

It doesn’t make data true. A proof shows that some secret satisfies the rules, not that the secret matches reality. If a user can type in any age they like, “I proved I’m over 18” means nothing. Real systems bind secrets to something trustworthy, such as an issuer’s signature, a commitment published earlier, or on-chain state. Garbage in, proof of garbage out.

It doesn’t authorize anything. A valid proof is just a fact. Anyone who sees a proof can copy it and submit it again, possibly in their own transaction. Your application has to bind the proof to its context and prevent replay, for example with nullifiers or by including the spender’s details in the public inputs. ZK on Cardano covers this in depth.

It doesn’t hide public inputs. Anything you mark public is visible to the verifier, and on-chain it’s visible to everyone, forever.

It doesn’t hide low-entropy secrets behind a bare hash. If the public input is hash(age), anyone can hash 0 through 150 and find your age. Mix in a random salt (a blinding factor) when you commit to small values.

It doesn’t hide metadata. Who submitted the proof, when, from which address, which fee they paid, and how often they act can all leak information. ZK hides the witness, not the envelope around it.

If you remember one thing from this page, make it this.

A circuit is like a pure function that returns true or false, whose parameters are split into public and secret:

// Conceptually (not ZeroJ code), a circuit is a predicate over public and secret values:
static boolean check(int threshold /* public */, int age /* secret */) {
return age >= threshold;
}

A proof is a small certificate, 192 bytes for Groth16, that says:

“I ran check on these public inputs with some secret, and it returned true.”

The verifier never learns the secret, and never has to re-run check. Verification cost stays roughly the same whether the circuit has ten constraints or ten million. The prover does the heavy lifting, from well under a second for small circuits to minutes for circuits with millions of constraints.

The analogy breaks in one important place. A circuit isn’t executed like Java; it’s a set of equations that the secret values must satisfy. That’s why you can’t use if or && on secrets, and why forgetting an equation creates a security hole. The next page explains what that means.