DEV Community

Aditya41205
Aditya41205

Posted on

Zero-Knowledge Proofs with Noir: From Installation to Your First ZK Circuit

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then reload your shell:

source ~/.bashrc
Enter fullscreen mode Exit fullscreen mode

Now install Noir:

noirup
Enter fullscreen mode Exit fullscreen mode

Check the installation:

nargo version
Enter fullscreen mode Exit fullscreen mode

In my setup, this returned:

nargo version = 1.0.0-beta.26
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then:

source ~/.bashrc
Enter fullscreen mode Exit fullscreen mode

The automatic version detection didn't recognize my Noir version, so I installed the Barretenberg version explicitly:

bbup -v 5.0.0-nightly.20260522
Enter fullscreen mode Exit fullscreen mode

Then check:

bb --version
Enter fullscreen mode Exit fullscreen mode

This gave me:

5.0.0-nightly.20260522
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now initialize a Noir package:

nargo init --name zk_noir
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If everything is correct, Noir should successfully check the project.

Now execute the circuit:

nargo execute
Enter fullscreen mode Exit fullscreen mode

This generates artifacts inside the target directory.

You'll see files similar to:

target/
├── zk_noir.json
└── zk_noir.gz
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

creates a constraint that essentially says:

x + y must equal 12
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

satisfies:

x + y = 12
Enter fullscreen mode Exit fullscreen mode

The witness is stored in:

target/zk_noir.gz
Enter fullscreen mode Exit fullscreen mode

So now we have:

zk_noir.json → Circuit
zk_noir.gz   → Witness
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Barretenberg takes:

Circuit + Witness

and generates a cryptographic proof.

You'll get files such as:

target/proof
target/public_inputs
Enter fullscreen mode Exit fullscreen mode

The important one is:

target/proof
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This produces:

target/vk
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If everything is correct, you should see:

Proof verified successfully
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The verifier then checks that proof:

Proof + Verification Key
          ↓
       Verifier
          ↓
    Valid / Invalid
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)