A Beginner's Guide to Noir: Building Zero-Knowledge Proofs from Scratch

Hey there, devs! If you've got some Rust under your belt but zero-knowledge (ZK) proofs feel like black magic, you're in the right place. I'm going to walk you through Noir step by step, like we're pair-programming a ZK circuit. We'll start simple and ramp up, focusing on the essentials without any unnecessary hype. By the end, you'll have a solid foundation to build your own proofs. Let's dive in.

1. Introduction to Noir

Noir is a domain-specific language (DSL) designed for writing zero-knowledge circuits. It lets you describe computations that can be proven without revealing private data, all in a Rust-like syntax that's familiar and safe. Developed by Aztec Labs, Noir compiles your code into an intermediate representation called ACIR (Abstract Circuit Intermediate Representation), which can then be used with various proving backends to generate and verify proofs.

Why does Noir exist? ZK proofs are powerful for privacy-preserving apps like proving you're over 18 without showing your ID but writing them from scratch is tedious and error-prone. Noir abstracts away the low-level cryptography, letting you focus on logic while ensuring your circuits are sound and efficient. It fits into the zkSNARK ecosystem by targeting proving systems like PLONK variants, enabling succinct proofs that are fast to verify (e.g., on-chain).

Key differences:

  • Noir: The language itself, where you write circuits using Rust-inspired syntax.

  • Barretenberg: The default proving backend, a high-performance C++ library that handles proof generation and verification using the UltraHONK scheme (more on that later).

  • nargo: The CLI tool for managing Noir projects think of it as Cargo for ZK circuits. It handles compilation, execution, and testing.

How does Noir compare to other ZK languages?

Noir Comparision
Noir Comparision

Noir shines if you're coming from app development and want to dip into ZK without a PhD in math. It's backend-agnostic in theory (ACIR can target others), but defaults to Barretenberg for UltraHONK efficiency.

2. Installing and Setting Up Noir

Let's get your environment ready. We'll install nargo (the Noir CLI) and bb (Barretenberg's CLI for proving/verifying).

Prerequisites

  • Rust (stable, via rustup).

  • A Unix-like OS (macOS, Linux; Windows via WSL).

  • Git for cloning examples later.

Step-by-Step Installation

  1. Install nargo using the noirup script:

    bash

    curl -L https://raw.githubusercontent.com/noir-lang/noirup/refs/heads/main/install | bash

    This fetches and installs the latest stable version. Restart your terminal after.

  2. Test the installation:

    bash

    nargo --version

    You should see something like nargo 0.XX.0 (version as of 2025).

  3. Install bb (Barretenberg CLI) using bbup:

    bash

    curl -L https://raw.githubusercontent.com/AztecProtocol/aztec-packages/refs/heads/master/barretenberg/bbup/install | bash

    This sets up the proving backend. Test with:

    bash

    bb --version

    Expect output like bb version X.Y.Z.

If you hit issues (e.g., permissions), check the official docs for manual builds. Now you're set let's code.

3. Writing Your First Noir Circuit (Age Verification Example)

We'll build a simple circuit: prove you're at least 18 without revealing your exact age. Age is a private input; the minimum age (18) is public for verification.

First, create a new project:

bash

nargo new age_verifier

cd age_verifier

Directory Structure

  • src/main.nr: Your circuit code.

  • Nargo.toml: Project config (name, dependencies leave it default for now).

  • After commands: Prover.toml (inputs), target/ (artifacts like compiled circuit and witness).

Edit src/main.nr:

fn main(age: Field, min_age: pub Field) {

 assert(age >= min_age);

}
  • Explanation:

    • fn main: Entry point for the circuit. Inputs are fields (modulo a large prime for arithmetic).

    • age: Field: Private input (default; hidden in proofs).

    • min_age: pub Field: Public input (visible to verifiers).

    • assert(age >= min_age): Enforces the constraint. If false during execution, it fails. In ZK, this translates to arithmetic constraints proving the inequality holds without revealing age.

    • Constraints are the heart of ZK: they define what must be true for the proof to be valid. Public inputs allow verifiers to check against known values (e.g., min_age=18).

Noir handles under-the-hood details like field arithmetic safety.

4. Compiling and Executing the Circuit

Compile to check syntax and generate ACIR:

bash

nargo compile

  • Output: ./target/age_verifier.json the compiled ACIR circuit (JSON representation of constraints).

This also creates Prover.toml if missing. Edit it for inputs:

age = "25" # Private

min_age = "18" # Public

Execute to compute the witness (proof inputs):

bash

nargo execute

  • Output: ./target/age_verifier.gzcompressed witness file (execution trace).

  • Explanation: Execution runs the circuit locally to generate the witness, which includes private values and intermediate computations. The ACIR defines the circuit structure; witness proves a valid execution.

5. Generating a Proof

Use bb to prove:

bb prove -b ./target/age_verifier.json -w ./target/age_verifier.gz -o ./target

  • Output: ./target/proof (the proof) and ./target/public_inputs (public values like min_age).

  • Explanation: This generates a zkSNARK proof attesting the circuit executed correctly. "Scheme is: ultra_honk" means Barretenberg uses UltraHONK a sumcheck-based variant of HONK (itself a PLONK evolution). It's efficient for large circuits, supports custom gates/lookups, and avoids FFTs for better performance in recursive proofs. No trusted setup needed, unlike some PLONK modes.

6. Creating and Using the Verifying Key

Generate the verification key (VK):

bash

bb write_vk -b ./target/age_verifier.json -o ./target

  • Output: ./target/vk

  • Explanation: The VK is derived from the circuit (ACIR) and commits to its structure. It's public and used to verify proofs without knowing privates. Security: VKs are circuit-specific using the wrong one could accept invalid proofs. Always regenerate for circuit changes. Store VKs securely; tampering could compromise verification (though UltraHONK's design mitigates some risks).

7. Verifying the Proof

Verify:

bb verify -k ./target/vk -p ./target/proof -i ./target/public_inputs

  • Success output: Something like "Proof verified successfully."

Proof verified
Proof verified

This checks the proof against the VK and public inputs. Fast and constant-time ideal for on-chain use.

8. Full Command Sequence Recap (Quick Cheatsheet) Here's the flow in one go:

nargo new <project> && cd <project>

Edit src/main.nr and Prover.toml

Commands Image
Commands Image

Proof Pipeline (Text Diagram):

Code (main.nr) --> Compile (nargo compile) --> ACIR (.json) Inputs (Prover.toml) --> Execute (nargo execute) --> Witness (.gz) ACIR + Witness --> Prove (bb prove) --> Proof + Public Inputs ACIR --> Write VK (bb write_vk) --> VK Proof + VK + Public Inputs --> Verify (bb verify) --> True/False

9. Security Considerations

ZK is powerful, but mistakes can leak data or allow forgery. Key tips:

  • Don't leak privates: Mark inputs as pub only if they should be revealed (e.g., min_age). Accidentally making age public defeats ZK.

  • Sound constraints: Ensure asserts cover all cases underconstrained circuits might pass invalid inputs (e.g., add range checks for age < 150).

  • VK binding: VKs tie to specific circuits. Reuse or mismatch could validate wrong proofs. Always regenerate and verify VK hashes.

  • Side channels: Avoid variable-time ops in circuits (Noir handles most).

  • Audits: For production, audit circuits tools like nargo test help, but formal verification is ideal.

  • General: Assume adversaries know the circuit; security relies on proper constraints and honest provers.

Resources:

Experiment, break things (safely), and ping me if stuck. You're on your way to ZK mastery keep coding!