This tutorial is written against the Noir documentation pages fetched on 2026-05-20, specifically the dev docs that note the latest stable version as v1.0.0-beta.21. Where behavior may differ across releases, you should verify against your local installation/compiler version.
If you want the shortest path to a first Noir circuit:
noirup.nargo new hello_world.src/main.nr, for example:fn main(x: Field, y: pub Field) {
assert(x != y);
}
nargo check to generate Prover.toml.x = 1
y = 2
nargo execute.At that point you have done the essential flow:
assert,The important engineering point is that Noir is not “just another programming language.” It is a way to describe algebraic constraints that a proving system can check. If you keep that mental model from the beginning, the rest of the ecosystem makes much more sense.
Field comes firstThe Noir primer says that every value in Noir has a type, and that all values in Noir are fundamentally composed of Field elements. That one sentence is the best place to start if you are evaluating whether to ship something in Noir.
In ordinary application code, you usually think in terms of machine integers, booleans, strings, arrays, and structs. In zero-knowledge circuits, the lowest-level object is generally an element of some finite field. Different proving systems may use different concrete fields, but the design pattern is standard in ZK literature: arithmetic constraints are expressed over a finite field, and the prover demonstrates knowledge of values satisfying those constraints.
Noir exposes that reality directly with the Field type.
A minimal Noir program from the quick start looks like this:
fn main(x: Field, y: pub Field) {
assert(x != y);
}
There are already several ideas packed into those three lines:
main is the circuit entrypoint.x and y are inputs.Field is the primitive algebraic type.pub Field means y is public.assert(...) introduces a condition that must hold.The docs also state that inputs in Noir are private by default. In ZK terms, a private input is known only to the prover. A public input is known to both prover and verifier and is revealed as part of proof verification.
That distinction matters operationally:
For a first circuit, Field is the right place to start because it avoids hidden assumptions about integer range, signedness, or bit decomposition. It also matches the underlying proving model directly. Even when you later use richer types, the docs are clear that these are abstractions layered on top of field elements.
From an engineering perspective, this is the first habit worth building: when you write Noir, ask yourself not just “what does this function compute?” but also “what constraints over field elements am I asking the prover to satisfy?”
That question will keep you from writing circuits that look familiar as programs but behave unexpectedly as proofs.
The Noir quick start documents the simplest supported setup flow with Nargo, the CLI tool for creating, compiling, executing, and testing Noir programs.
The installation command shown in the docs is:
curl -L https://raw.githubusercontent.com/noir-lang/noirup/refs/heads/main/install | bash
noirup
Then initialize a new project:
nargo new hello_world
According to the primer, this creates at least:
src/main.nrNargo.tomlThe useful way to think about these files is:
src/main.nr contains your circuit logic.Nargo.toml contains project metadata and environment options.For your first pass, replace the boilerplate circuit with the documented example:
fn main(x: Field, y: pub Field) {
assert(x != y);
}
This is intentionally tiny, but it is not trivial. It proves a meaningful statement:
“I know a private value
xsuch thatxis different from the public valuey.”
That is already a zero-knowledge statement. The verifier learns y and learns that the prover knows some valid x, but the verifier does not learn x itself from the circuit interface.
Next, from the project directory, run:
cd hello_world
nargo check
The quick start says this generates a Prover.toml file where input values are specified. Then populate it with valid values:
x = 1
y = 2
And execute:
nargo execute
The primer states that nargo execute will, by default:
./target/hello_world.json,./target/witness-name.gz.Two practical observations are worth calling out here.
First, the witness is not the proof. In standard ZK terminology, a witness is the collection of private values and intermediate values needed to satisfy the circuit constraints. It is the internal evidence that a prover uses when constructing a proof.
Second, the output artifact such as hello_world.json is not itself a proof either. It is a compiled representation of the circuit. Noir compiles to ACIR, and a separate proving backend consumes the compiled circuit plus witness to produce a proof.
That separation is part of Noir’s design:
For a working developer, this separation is valuable because it makes your circuit code less tied to one backend. But it also means you should be disciplined about not assuming backend capabilities that the language alone does not guarantee.
Let’s stay with the first example for a moment:
fn main(x: Field, y: pub Field) {
assert(x != y);
}
To understand it clearly, break it into the mathematical statement and the privacy statement.
The assertion is that x != y.
In normal software, that reads like a branch condition or runtime check. In a circuit context, it is better understood as a required relation. If the relation does not hold for the provided inputs, execution fails and no valid witness is produced for that input assignment.
That means your Noir program is not just a computation. It is a specification of acceptable assignments.
The type annotations tell us what is revealed:
x: Field is private by default.y: pub Field is public.The data types primer emphasizes that private values are known only to the prover, while public values are known to both prover and verifier. It also notes that public values used with smart contract verifiers should be considered known to anyone who can read the chain once the proof is verified on-chain.
That warning matters a lot in production design. New ZK developers often expose too much as public because it is convenient for integration. In practice, every public input becomes part of the externally visible statement. If a value is sensitive, keep it private unless you have a specific reason not to.
Field is a good first choiceThe docs say Noir has other primitive and compound types, but all values are fundamentally composed of fields. For a first circuit, Field has two advantages:
If you later decide to use integers or booleans, you should do so for readability or semantic clarity, not because they change the fundamental nature of the circuit. They are still compiled down to constraints over fields.
Without introducing undocumented syntax, you can still explore a different statement by modifying only the assertion:
fn main(secret: Field, threshold: pub Field) {
assert(secret != threshold);
}
This is the same shape, but the names clarify intent. The proof statement becomes:
“I know a secret field element that is not equal to the public threshold.”
That may sound simple, but naming matters when you read proofs as statements. A verifier does not care that your variable was called x; they care what claim they are being asked to accept.
The primer’s “private by default” rule is more than a convenience. It nudges developers toward the core use case of ZK systems: prove something about secret data without revealing the data itself.
As a rule of thumb for your first Noir circuits:
That discipline will help later when your circuits are larger and the public/private boundary has real protocol consequences.
assert buys you, and what it does notIf there is one concept to get right early, it is constraints.
The Noir quick start uses:
assert(x != y);
The primer page included in your reference does not provide a full catalog of assertion behavior beyond this example, so I will stay with what is verifiable and standard.
A circuit is valuable because it constrains allowable assignments. If you accept arbitrary inputs without constraints, there is nothing meaningful to prove. The prover would always be able to produce a witness because every assignment would be acceptable.
assert(...) changes that. It expresses a condition that must hold. In standard circuit language, every such condition contributes to the proof relation.
This is the biggest conceptual shift from ordinary programming.
In application code, you may think:
In circuit code, the real product is the set of legal assignments. Computation exists to define and connect those assignments, but the verifier ultimately checks that the prover’s witness satisfies the constraints encoded by the program.
So when you write:
fn main(x: Field, y: pub Field) {
assert(x != y);
}
you are defining a relation roughly of the form:
valid if and only if the prover’s chosen
xdiffers from the publicy.
The witness generation step succeeds only when inputs satisfy the relation.
The quick start shows only a successful example with:
x = 1
y = 2
If you instead try equal values, you should expect execution to fail because the assertion is unsatisfied. The exact error text is compiler- and version-specific, so you should verify against your local installation/compiler version rather than relying on a tutorial for the exact message format.
What matters conceptually is that failure to satisfy constraints prevents valid witness generation.
That property is why a later proof means anything.
A common beginner confusion is to conflate “private input” with “well-constrained input.”
Those are different dimensions:
A private input with no meaningful constraints is still private, but the proof may not establish anything useful. Conversely, a public input can be heavily constrained and be central to the proof statement.
For a secure design, you need both:
The first Noir circuit is intentionally too small for production use, but it teaches a pattern that scales:
That exact loop remains the core of much larger systems: authentication proofs, membership proofs, recursive verification circuits, and on-chain verifiable claims all reduce to the same idea.
Your first goal is not to write a large circuit. It is to internalize that the code exists to define a proof relation.
The Noir quick start is explicit that after compiling and executing, you are ready to prove, but you still need a proving backend. This separation is essential to understand before you integrate Noir into a production stack.
From the primer:
In practice, from the documented flow:
nargo check
nargo execute
you get:
./target/hello_world.json,./target/witness-name.gz.These artifacts are the bridge between your source code and the proving backend.
In standard ZK literature, a witness is the secret assignment demonstrating that the statement is true. In real systems, it often includes not only top-level private inputs but also intermediate values needed to satisfy internal arithmetic relations.
The important point for a Noir developer is this:
So the witness is evidence for the prover, not something the verifier trusts directly.
A proof is the cryptographic object that convinces a verifier that:
The verifier does not need to see the private witness itself.
That is the core value proposition of ZK systems and the reason the public/private distinction in Noir matters so much.
The quick start says the most common backend for Noir is Barretenberg, and it lists capabilities such as:
However, your provided primer does not include backend command syntax. Because of your instruction to avoid fabrication, I will not invent a CLI sequence here. If you want to go from witness to proof with a specific backend, you should verify against your local installation/compiler version and the backend’s own getting-started documentation.
That is not a minor caveat. Backend tooling often changes faster than the language tutorial examples.
For practical engineering, keep the pipeline in this order:
Write Noir source
Your main.nr defines the proof relation.
Compile to ACIR
Nargo produces an intermediate representation the backend can understand.
Provide concrete inputs
Prover.toml gives values for the current run.
Execute to generate witness
Noir checks that the constraints are satisfiable for those values.
Use a backend to prove
The backend turns “compiled circuit + witness” into a cryptographic proof.
Verify against public inputs
The verifier checks the proof against the circuit statement and public values.
This decomposition is useful when debugging. If something fails, ask which stage failed:
That will save you a lot of time compared with treating “the proof didn’t work” as one undifferentiated problem.
Now that the pieces are on the table, here is a conservative first workflow for a developer evaluating Noir.
nargo new hello_world
cd hello_world
fn main(x: Field, y: pub Field) {
assert(x != y);
}
This circuit is good for a first project because it includes:
That is enough to exercise the basic proving model.
nargo check
Per the docs, this creates Prover.toml.
x = 1
y = 2
Use values that satisfy the assertion at first. When you are learning, separate “tooling works” from “my constraints are wrong.”
nargo execute
At this point you should expect:
You do not need to understand every byte of the generated files yet. But you should understand what each file is for:
Prover.toml: your input assignmentChange the inputs so the assertion is false. For example:
x = 2
y = 2
Then run nargo execute again.
You should expect failure because the circuit relation is no longer satisfied. The exact diagnostics may vary by version, so verify against your local installation.
This is an important test because it proves your circuit is not just accepting every assignment.
Once you trust the witness-generation stage, integrate a proving backend. The primer points you to Barretenberg, but does not provide the exact commands in the material you supplied. So the correct production guidance is:
This process is intentionally slower than “copy-paste some full-stack starter repo.” But it gives you confidence in the parts that matter:
For someone deciding whether to ship Noir, that confidence is worth more than a flashy end-to-end demo.
This tutorial is version-scoped.
It is written against Noir documentation fetched on 2026-05-20, using the dev docs that reference v1.0.0-beta.21 as the latest up-to-date version. If your local toolchain differs, verify behavior and command output locally.
Backend commands are intentionally omitted.
Your reference primer confirms that a proving backend is required and mentions Barretenberg, but it does not provide exact proof-generation CLI syntax. To avoid fabrication, the correct instruction is to verify against your local installation/compiler version and the backend’s current documentation.
A witness is not a proof.
New users often blur these together. nargo execute gives you witness-related artifacts and compiled circuit output; proof generation happens later through a backend.
Public inputs are revealed.
The Noir data types primer is explicit that public values become known to the verifier, and in on-chain settings should be considered visible to everyone with access to that chain.
Do not overread small examples.
The assert(x != y) example is useful because it is minimal, not because inequality checks are representative of all production constraints. Treat it as a learning scaffold.
Avoid inventing semantics from other languages.
Noir looks familiar syntactically, but it is not ordinary application code. The right model is “constraint system over field elements,” not “general-purpose program that happens to use cryptography.”
Noir Quick Start
https://noir-lang.org/docs/dev/getting_started/quick_start
Noir Data Types
https://noir-lang.org/docs/dev/noir/concepts/data_types/
Noir Documentation root
https://noir-lang.org/docs/dev/
ACIR reference entry point
https://noir-lang.org/docs/dev/
Barretenberg getting started reference mentioned by Noir docs
https://noir-lang.org/docs/dev/getting_started/quick_start
Awesome Noir reference mentioned by Noir docs
https://noir-lang.org/docs/dev/getting_started/quick_start
Thanks for reading this far. If “Noir circuit basics” connected with where you are, three concrete next steps:
The full Midnight ZK Cookbook index has 17 tutorials across Midnight, Aleo, Aztec, Noir, and risc0 plus 4 Chinese translations. Adjacent tutorials are listed by ecosystem on that page.
Bounty Radar tracks open ZK bounties across Algora, GitHub labels, Drips Wave, Code4rena, and Bountycaster. Browse the Noir sub-feed; JSON at /noir.json. The free tier is poll-based; the $19/mo Hobbyist tier pushes one filter to your Telegram in real time.
zk-pipeline-doctor is the free MIT-licensed CLI that scores any ZK project on tests, CI, docs, security, reproducibility, and language toolchain (supports Compact, Leo, Noir, Cairo, and 7 Rust zkVMs). Drop it into a GitHub Action with zk-doctor-action for diff-aware PR comments. The $15/mo Pro tier adds four cross-ecosystem deep detectors (circuit complexity, proving-system pitfalls, verifier soundness, multi-file consistency).
Drafted with AI assistance and reviewed by the author before publishing. See DISCLOSURE for the full process.
Three ways to support this work, pick whichever matches your situation:
Free alternative: Sponsor on GitHub · Star the repo · Share with one ZK developer who'd benefit