One Knowledge Proof
The following video acts as proof that I have some knowledge about zero-knowledge proofs:
[🌍 LINK] I built a really shitty website to play around with Zero Knowledge Proofs. It’s a simple app that allows you to prove you are over 18 without revealing your age.
[💻 LINK] Source code if anyone’s interested.
The following blog post will briefly touch on ZKPs, how I built the app and some random tooling I used.
Why am I doing this?
I want to learn more about Zero Knowledge Proofs, how they work, what are the practical applications, and especially HOW DO I WORK WITH THEM. I want to be able to build cool things with it. In the blockchain industry, all the new shiny chains on the block use Zero Knowledge Proofs to achieve privacy, scalability, or marketing buzz… Also, it’s a highly marketable skill, and I want to sustain my job security 😅🤷♂️.
What is a Zero Knowledge Proof?
A Zero Knowledge Proof is a cryptographic protocol that allows you to prove that you know something without revealing what you know. It’s a magic trick. You can prove that you know how to do a magic trick, but you don’t show how you do it.
Here is a video that will explain it for every age group that may read this article:
And if you didn’t watch it, here is a summary of the explanation he made for a child:
- Imagine you have a picture of penguins with a puffin hidden among them.
- You don’t know where the puffin is.
- I can take a piece of paper and cut a hole in it. I can then put the paper on top of the picture and move it around until the puffin is in the hole.
- I just proved that I know where the puffin is without revealing where it is.
Imagine that all the proving is done via fancy math and cryptography. That’s a Zero Knowledge Proof. The idea is to encode the information into polynomials. And you end up executing “mathematical circuits”. When I was reading the materials, they made sense, but I’d be lying if I said I could re-explain it to you in simpler terms. I’ll just link the materials I used at the end of the article.
What did I build?
One of the most common “advertised” use cases of ZKPs is proving that you are over 18 without revealing your age. `Everyone keeps talking about the “bouncer scenario” where you need to show your ID to the bouncer, where he can see your date of birth (to prove that you are over 18), but he doesn’t need to see your name or other information that might be there.
I chose this idea because “if everyone keeps talking about it, then it must be a good starting point!” I thought. Surprisingly, I was satisfied; the actual code is dead simple. My main requirement was that everything needs to happen on the client — no server, no trusted party, no centralised anything.
How did I build it?
I started with the demo because I believe in seeing the big picture first rather than starting from the bottom up. So, let’s clear up some terminology first:
- Bouncer - someone who’s guarding the entrance to a club. In ZKP terms, this is called the verifier.
- Entrant - someone who wants to enter the club. In ZKP terms, this is called the prover.
- “I am over 18” is the statement that the entrant wants to prove. In ZKP terms, this is called the claim.
- Proof - the proof that the entrant is over 18. In ZKP terms, this is called the proof. (I know, right?). The entrant generates the proof because only he knows how old he is.
After the bouncer receives the proof from the entrant, he can verify it. We’ll get to the proof generation later.
Another domain-specific language???😭
This was one of the rare projects in my life where I didn’t write everything in Rust 😲. Because no one wants to write fancy polynomials by themselves, many much smarter people than me have written custom programming languages that do it for you. They compile the code into the math circuits I mentioned previously (more like some kind of representation of the circuits) that can be executed by the appropriate backend.
Yes, I know. I am not a fan of DSLs, either. But it’s the only way to make the math circuits work for a mere mortal man like me. Three of the most popular DSLs are:
- Noir - has excellent documentation and examples, a working compiler, and a friendly community. It’s also the one I used for this project. It has this idea of “public” and “private” variables, which will be discussed later.
- Cairo - I just know it exists, it’s similar to Rust. And it allegedly doesn’t have the private/public variables? I may be wrong. I’m always happy to share misinformation on the internet.
- Congrats, you get a link to a repo with a list of examples and ten other DSLs here.
As my mentor in ZKP subjects said: “The newer the ZKP language, the better ZKP abstraction it has, and modern ones are mostly Rust-based (syntax and thought).” Obviously, I went with the newest one.
So, what does this mean in the actual code? Here you can see all of the ZKP Noir code I used for making my app:
fn main(age_of_person : Field, minimum_age : pub Field) {
assert(age_of_person as u64 >= minimum_age as u64);
}
That’s it. That’s the whole code. The only thing that I need to elaborate on is the pub
keyword. It means that the variable is “public”. Let me explain by an example:
- When the entrant generates the proof, he knows that he needs to be at least 18 years old to enter the club. So he sets the
minimum_age
to 18. This is a public variable because it is commonly agreed between the bouncer and the entrant. - When the bouncer verifies the proof, he doesn’t know how old the entrant is. But he knows what the minimum age is. So, he verifies the given proof from the entrant against the minimum age that he knows.
- If the entrant generated the proof with a different minimum age, the proof would be invalid. Because the bouncer would be verifying the proof against a different minimum age.
In practice, this means that the public variables must be agreed upon by both the prover and the verifier, either by some smart contract, API communication, or just having the variables hardcoded in the code for both parties (my case).
Getting the proof into the browser
After I’ve written the Noir code, I compile it, and it generates a bunch of files. But I ignored them and used the main.json
file. It has the ABI specification and the bytecode for the code.
Because Noir is fancy, it can compile to multiple “prover backends”, which allows you to use different proving system backends. But that’s already too much information for this article and more advanced than I can elaborate on. I just used the default one, acvm-backend-barretenberg
, because this backend has a wasm
version that allows the ZKP program to be executed in the browser.
Importing and “making use” of the proof is as simple as just importing the JSON file and instantiating the Noir
class:
import {
BarretenbergBackend,
CompiledCircuit,
ProofData,
} from '@noir-lang/backend_barretenberg';
import { Noir } from '@noir-lang/noir_js';
import noir_demo from '../../circuits/target/main.json';
const backend = new BarretenbergBackend(noir_demo as CompiledCircuit);
export const AgeVerifier = new Noir(noir_demo as CompiledCircuit, backend);
export const MINIMUM_AGE = 18;
export async function generateProof(age: number): Promise<ProofData> {
return AgeVerifier.generateFinalProof({
age_of_person: age,
minimum_age: MINIMUM_AGE,
});
}
export async function verifyProof(proof: Uint8Array): Promise<boolean> {
return AgeVerifier.verifyFinalProof({
proof,
publicInputs: [
Uint8Array.from([
// Left pad with zeroes like a champ 💪
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, MINIMUM_AGE,
]),
],
});
}
And essentially, from everywhere else in the code, I can just call generateProof
and verifyProof
, and I’m done. The rest is just UI stuff.
The UI stuff
I don’t want to write a separate blog post about this little victory of mine, so I’ll add it here. I am not a UI developer. And I am not really trying to become one. Throughout the years, I’ve come to dislike the JS ecosystem and the tooling it provides, and I am always looking for ways to make the experience better when I have to do anything in the JS land.
I used this tool, which I found online: image to code generator. With this, I was able to transform my shitty UI mockup into actual HTML code with Tailwind classes! I am not joking. I just drew a shitty mockup in Figma, took a screenshot, and then generated the code (let’s not mention the part where I had to get a GPT4 API key and pay 10$ to “top my account”). I was still using the default colours for “boxes” 😂.
How do I rate the result? Well, it worked and did a good job. However, the requirements were later changed as I was iterating on the app, and after I completely broke the mobile UI, I gave up and asked my wife to fix the styling code (she does frontend work and she’s good at it). But she didn’t touch any generated colours because I wanted to keep it authentic - just as the AI gods created it. Anyway, the tool still provided a great starting point, and this will definitely be in my toolbox for future projects. Some random tooling:
- I really dislike Prettier and Eslint. Too many versions and incompatibilities with plugins are just a disaster to use. Once you configure it, you’re afraid of ever touching it again. God forbid upgrading it… In this project, I used Biome - it does formatting and linting, is easy to configure, and fast. As with all good things in life, it’s also written in Rust, btw 🦀.
- I used Bun instead of NodeJS. It’s faster, easier to set up, and works great; Cloudlfare pages has support for it as well, and it takes 30 seconds to install all the dependencies and generate a production build. Sadly, it’s not written in Rust, so I can’t use it as bragging leverage of why Rust is so great when talking to my friends 🦎.
Random facts about ZKP
- Blockchains use ZKP for L2 scaling solutions. ZKPs have the property “succinctness”, which means that the proof size is small. This is important because it allows you to verify the proof cheaper than if you were to verify the whole computation. This is why ZKPs are used for L2 scaling solutions. Imagine like 50 transactions are happening on Ethereum L2, and you want to verify that they are all valid. You can do it with ZKPs, in a single proof, in a single transaction. Sounds great, right?
- Computations are weird in ZKP. Everything happens in a finite field, and you can’t use the regular operators. Well you can but under the hood hey will work over the finite field, meaning that modulo operations will be happening left and right. My simple use-case didn’t expose any of that directly, but I can imagine that it gets complicated quickly.
- Bit shifts, divisions, regular hashing (SHA, keccak), comparisons and branching are expensive af, because they are hard to do when your whole app is made out of math polynomials under the hood. Here’s a 1.5h lecture on how to optimise your circuits: [LINK]