I’ve been learning about Zero-Knowledge Proofs (ZKPs), and one thing I quickly realized is that understanding the theory is only half the battle.
At some point, you need to actually write a circuit, compile it, generate a witness, create a proof, and verify that proof.
So in this article, I’m going to go from zero to my first working ZK proof using Noir and Barretenberg.
By the end, we’ll have this entire pipeline working:
Noir → Circuit → ACIR → Witness → ZK Proof → Verification
What is Noir?
Noir is a programming language designed for writing zero-knowledge circuits.
Its syntax is heavily inspired by Rust, which makes it relatively easy to pick up if you're familiar with Rust-like languages.
Instead of manually writing cryptographic constraints, we can write something much closer to normal code.
For example:
fn main() {
let x = 5;
let y = 7;
assert(x + y == 12);
}
Noir takes this program and compiles it into a representation that a proving system can work with.
For this tutorial, I’ll be using Noir + Barretenberg.
1. Installing Noir on Windows
If you're on Windows, there is an important thing to know.
The default Barretenberg backend doesn't provide native Windows binaries, so I used WSL (Windows Subsystem for Linux).
I’m using Ubuntu through WSL.
Once WSL is set up, open Ubuntu and install curl if you don't already have it:
sudo apt update
sudo apt install curl
2. Installing Noir
Noir provides a tool called noirup for installing Noir.
Run:
curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash
Then reload your shell:
source ~/.bashrc
Now install Noir:
noirup
Check the installation:
nargo version
In my setup, this returned:
nargo version = 1.0.0-beta.26
nargo is Noir's command-line tool.
We'll use it to create projects, check circuits, execute them, and generate the artifacts needed for proving.
3. Installing Barretenberg
Noir lets us write the circuit, but we still need a proving backend to actually generate the proof.
For this, I'm using Barretenberg.
Install bbup:
curl -L https://raw.githubusercontent.com/AztecProtocol/aztec-packages/refs/heads/next/barretenberg/bbup/install | bash
Then:
source ~/.bashrc
The automatic version detection didn't recognize my Noir version, so I installed the Barretenberg version explicitly:
bbup -v 5.0.0-nightly.20260522
Then check:
bb --version
This gave me:
5.0.0-nightly.20260522
Version compatibility between Noir and Barretenberg is important, so if you're following this tutorial with a different Noir version, check the current compatibility information rather than blindly using this exact version.
4. Creating Our First Noir Project
Let's create our project:
mkdir Zk-noir
cd Zk-noir
Now initialize a Noir package:
nargo init --name zk_noir
One small thing I ran into here:
The folder can be called:
Zk-noir
but the Noir package name cannot contain -.
So:
Zk-noir → folder name
zk_noir → package name
Our project now looks like:
Zk-noir/
├── Nargo.toml
└── src/
└── main.nr
5. Writing Our First Circuit
Open:
src/main.nr
Replace its contents with:
fn main() {
let x = 5;
let y = 7;
assert(x + y == 12);
}
At first, this looks like a very simple program.
And it is.
But we're not just writing a normal program.
We're writing a program that will eventually be transformed into a zero-knowledge circuit.
6. Checking the Circuit
Before generating anything, let's make sure the circuit is valid.
Run:
nargo check
If everything is correct, Noir should successfully check the project.
Now execute the circuit:
nargo execute
This generates artifacts inside the target directory.
You'll see files similar to:
target/
├── zk_noir.json
└── zk_noir.gz
These files are important.
7. What is ACIR?
When Noir compiles our program, it doesn't directly produce a zero-knowledge proof.
Instead, Noir produces an intermediate representation called ACIR.
ACIR stands for:
Abstract Circuit Intermediate Representation
You can think about the pipeline like this:
Noir code
↓
Noir compiler
↓
ACIR
↓
Barretenberg
↓
ZK proof
The zk_noir.json file contains the compiled circuit representation.
The circuit describes the constraints that must be satisfied.
For example:
assert(x + y == 12);
creates a constraint that essentially says:
x + y must equal 12
8. What is the Witness?
When we execute the circuit, Noir produces a witness.
The witness contains the values required to satisfy the circuit's constraints, including intermediate values generated during the computation.
In our example:
x = 5
y = 7
satisfies:
x + y = 12
The witness is stored in:
target/zk_noir.gz
So now we have:
zk_noir.json → Circuit
zk_noir.gz → Witness
9. Generating the ZK Proof
Now comes the interesting part.
We can give Barretenberg our circuit and witness and ask it to generate a proof.
Run:
bb prove -b ./target/zk_noir.json -w ./target/zk_noir.gz -o ./target
Barretenberg takes:
Circuit + Witness
and generates a cryptographic proof.
You'll get files such as:
target/proof
target/public_inputs
The important one is:
target/proof
That's our actual zero-knowledge proof.
10. Verification Key
The verifier needs a verification key (VK) to verify the proof against the circuit.
The documented form for generating the verification key during proving is:
bb prove -b ./target/zk_noir.json -w ./target/zk_noir.gz --write_vk -o target
This produces:
target/vk
Think of the verification key as information that allows the verifier to check whether a proof is valid for the circuit.
11. Verifying the Proof
Now let's verify our proof.
Run:
bb verify -p ./target/proof -k ./target/vk
If everything is correct, you should see:
Proof verified successfully
And that's it.
We just generated and verified our first ZK proof.
12. The Entire Pipeline
What we just did can be summarized as:
Noir Code
↓
Compilation
↓
ACIR
↓
Execution
↓
Witness
↓
Barretenberg
↓
ZK Proof
↓
Verification
This is the basic workflow behind what we just built.
13. Understanding the Files
The target directory can initially look confusing, so here's what the important files represent.
zk_noir.json
Compiled circuit / ACIR.
It describes the circuit and its constraints.
zk_noir.gz
The witness generated when executing the circuit.
proof
The cryptographic proof generated by Barretenberg.
vk
The verification key used to verify the proof.
public_inputs
The inputs that are public to the verifier.
14. Prover vs Verifier
One of the most important concepts in ZK is the distinction between the prover and the verifier.
The prover has the information needed to satisfy the circuit.
The prover generates a proof:
Witness
↓
Prover
↓
Proof
The verifier then checks that proof:
Proof + Verification Key
↓
Verifier
↓
Valid / Invalid
The important idea is that the verifier can be convinced that the statement is true without simply receiving the entire private information used by the prover.
That's the fundamental idea behind zero-knowledge proofs.
15. But Is Our Example Actually Private?
Not really.
This first circuit is mainly a mechanics demonstration.
We're literally writing:
let x = 5;
let y = 7;
There isn't an interesting secret being protected.
A more meaningful ZK circuit would involve something like:
"I know a secret value that satisfies a particular condition, but I don't want to reveal the secret itself."
For example:
I know the secret
↓
I prove that I know it
↓
The secret remains private
That's where zero-knowledge proofs become much more interesting.
16. What I Learned
Before actually building a circuit, concepts like:
- ACIR
- Witness
- Proof
- Verification Key
- Prover
- Verifier
can feel extremely abstract.
Building the entire pipeline myself made the architecture much clearer.
The key thing I understood is that a ZK proof isn't some magical object that appears out of nowhere.
There is a concrete pipeline behind it:
Noir
↓
Circuit
↓
ACIR
↓
Witness
↓
Barretenberg
↓
Proof
↓
Verification
Once you actually run this yourself, the theory starts making much more sense.
What's Next?
This was only my first step into Noir and ZK.
Now I want to move beyond simple arithmetic circuits and start building circuits where the privacy actually matters.
Some of the things I'm exploring next are:
- Private vs public inputs
- Hash functions inside circuits
- Merkle trees
- Commitments
- More complex constraints
- Noir functions and modules
- Loops and arrays
- SNARKs vs STARKs
- ZK-rollups
- On-chain proof verification
- Real-world ZK applications
The goal is to go from:
"I can generate a ZK proof."
to:
"I understand what the circuit is proving, why the proof is secure, and how to use it in a real application."
Final Thoughts
If you're learning zero-knowledge proofs, I highly recommend getting your hands dirty early.
Don't spend weeks only reading about provers, witnesses, commitments, SNARKs, and polynomial commitments.
Write a circuit.
Break it.
Fix it.
Generate a proof.
Verify it.
Once you see the entire pipeline working on your own machine, zero-knowledge starts feeling much less mysterious.
And this is just the beginning of my ZK journey.
Resources
Noir Documentation: https://noir-lang.org/
Noir GitHub: https://github.com/noir-lang/noir
Barretenberg: https://github.com/AztecProtocol/barretenberg
Top comments (0)