Testing and Debugging Noir Circuits: The Honest Workflow

TL;DR

This tutorial is written against the Noir documentation snapshot fetched 2026-05-20, specifically the dev docs that also point readers to v1.0.0-beta.21 as the latest version in the navigation. Everything below stays within syntax and concepts that are directly verified by the provided primer, plus standard zero-knowledge engineering practice.

The honest workflow for testing and debugging Noir circuits is:

  1. Start from nargo new and keep circuits small.
  2. Use nargo check early to generate Prover.toml and catch compile issues.
  3. Treat nargo execute as your first real test harness: it compiles, executes, and writes a witness.
  4. Encode expected behavior with assert(...), because constraint failure is your ground truth.
  5. Vary inputs in Prover.toml methodically, especially around boundaries and expected failures.
  6. Use public inputs intentionally so you can reason clearly about what is revealed and what is only witness data.
  7. When debugging, reduce the circuit to the smallest reproducer rather than guessing.
  8. If your local Noir/Nargo version exposes extra testing commands or debugging flags, verify against your local installation/compiler version.

The key mindset: in Noir, “testing” is not only about function-level unit tests. It is also about checking whether a set of constraints admits exactly the witnesses you intend, and rejects the ones you do not.

1. Version, scope, and what “testing” honestly means in Noir

When developers say they want to “test” a Noir circuit, they often mean several different things at once:

The provided Noir primer directly verifies a compact toolchain:

It also verifies core language facts that matter for debugging:

That is enough to describe a production-useful workflow, even without assuming any undocumented testing framework commands.

Here is the canonical starting point from the primer:

fn main(x: Field, y: pub Field) {
    assert(x != y);
}

This tiny circuit already contains the main testing ideas:

A valid run means the prover supplied a witness satisfying the constraint. An invalid run means witness generation or execution fails because the constraint system cannot be satisfied under those inputs.

That is the first honest framing: your assertions are your executable specification.

In conventional software, a bug often means “the code returned the wrong value.” In a ZK circuit, a bug often means one of these:

Testing Noir circuits, then, is less about broad runtime observability and more about:

If your local Nargo installation offers additional testing commands, debugger integrations, or richer failure messages, verify against your local installation/compiler version. This tutorial does not assume those APIs.

2. The basic workflow: compile, execute, and iterate

The smallest reliable workflow begins with project creation.

From the primer:

nargo new hello_world
cd hello_world
nargo check

The primer says nargo check can generate a Prover.toml file, where input values are specified. For the sample circuit:

x = 1
y = 2

Then you run:

nargo execute

The verified behavior is:

This gives you a clean three-stage loop:

Stage 1: Syntax and structure

Use nargo check as the fastest way to catch obvious problems:

Even if your eventual goal is proof generation, you should treat nargo check as the first gate on every change.

Stage 2: Behavioral testing through execution

nargo execute is where a circuit starts behaving like a tested relation rather than just parsed source code. With one command you learn:

This is the closest verified primitive in the primer to a test runner.

Stage 3: Artifact awareness

Two files matter immediately:

The JSON artifact represents the compiled output of the Noir program. The primer identifies Noir as compiling into ACIR. That means the artifact is part of the debugging surface: it tells you there is a concrete compiled circuit, not just source code.

The witness file matters because many ZK bugs are not source-level syntax bugs. They are witness-generation or constraint-satisfaction bugs. The fact that nargo execute emits a witness is evidence that, for that concrete input assignment, the relation was satisfiable.

A practical habit: keep a “known good” input set

For any nontrivial circuit, keep at least one Prover.toml assignment that you know should succeed. Whenever you change the circuit:

  1. run nargo check
  2. run nargo execute with that known-good input
  3. only then start exploring new edge cases

This helps distinguish:

A practical habit: test both success and failure paths

For the sample circuit:

fn main(x: Field, y: pub Field) {
    assert(x != y);
}

You should test at least these two cases:

Valid:

x = 1
y = 2

Invalid:

x = 1
y = 1

The first case should execute successfully. The second should fail because the assertion is not satisfied.

That sounds trivial, but it is exactly the discipline that scales. Every important assertion in a circuit should have at least:

If you do only happy-path runs, you are not really testing a circuit. You are only showing that at least one witness exists.

3. Write circuits so they are testable: assertions, privacy, and small invariants

A circuit that is hard to test is usually a circuit whose intended relation is not stated clearly enough. In Noir, the primary verified mechanism for stating correctness conditions in the primer is assert(...).

That leads to an honest recommendation: break correctness into small, explicit invariants.

Start again from the verified pattern:

fn main(x: Field, y: pub Field) {
    assert(x != y);
}

What makes this testable?

Now compare that to a more realistic development mistake: combining several intended checks mentally, but writing only one of them in code. In a ZK circuit, anything not constrained is effectively unconstrained. Standard ZK engineering practice treats this as one of the most dangerous bug classes.

So when you design a Noir circuit:

Prefer explicit assertions over implied intent

If the protocol requires several properties, constrain each property directly rather than assuming one condition implies another unless that implication is obvious and deliberate.

Even with very simple syntax, you can encode separate invariants one by one. For example:

fn main(x: Field, y: pub Field) {
    assert(x != y);
    assert(x != 0);
}

This example uses only documented Noir syntax patterns already visible in the primer:

The exact availability of every operation or literal handling should be normal in Noir, but for any nuance beyond the primer, verify against your local installation/compiler version.

The point is conceptual: if x must be different from y, and also must be nonzero, make both requirements explicit.

Be deliberate about public vs private

The primer is very clear:

This has direct debugging consequences.

If you are confused about a circuit’s expected outputs or externally visible commitments, first ask:

A large class of integration bugs is not “the math is wrong,” but “the visibility contract is wrong.”

For example:

fn main(secret: Field, expected: pub Field) {
    assert(secret != expected);
}

Here the verifier learns expected, not secret. That is easy to reason about in a test because you can vary the private witness separately from the public statement.

Keep the first main tiny

For debugging, the best Noir programs often begin life as almost embarrassingly small circuits. A small main gives you three benefits:

  1. there are fewer places for unconstrained logic to hide
  2. failing inputs are easier to interpret
  3. reducing to a reproducer later is much cheaper

If a circuit is becoming difficult to reason about, do not immediately reach for tool-specific debugging features. First ask whether the relation can be restated as a smaller circuit with one or two assertions that isolate the issue.

That is often faster and more reliable.

4. Input-case strategy: how to use Prover.toml as a real test harness

The primer verifies that Prover.toml is where input values are specified. Many Noir developers treat it as a temporary file just to get a demo running. That is a missed opportunity.

Used carefully, Prover.toml is your first real test harness.

For the sample program:

fn main(x: Field, y: pub Field) {
    assert(x != y);
}

you can drive several meaningful cases by changing only the input file.

Case 1: expected success

x = 1
y = 2

This shows that the circuit admits a valid witness for a straightforward satisfying assignment.

Case 2: expected failure

x = 1
y = 1

This shows that the same circuit rejects an assignment violating the intended relation.

Case 3: changing only the public input

x = 5
y = 7

This helps you reason about how the statement seen by the verifier changes while preserving the same structure.

Case 4: changing only the private witness

x = 8
y = 7

This helps you distinguish “the public statement changed” from “only the prover’s witness changed.”

That distinction matters because public and private values have different protocol meaning even when they look identical in source code.

Test categories worth maintaining

For any nontrivial circuit, keep a short table of input classes:

The primer does not define a dedicated Noir test file format or parameterized test API in the provided excerpts, so the portable workflow is to manage these input sets explicitly and rerun nargo execute.

If your local workflow includes scripts, CI jobs, or additional Nargo subcommands, that can be effective, but verify against your local installation/compiler version.

Why this matters in ZK specifically

In standard application testing, it is often enough to validate output values. In a circuit, you are also validating the existence or nonexistence of satisfying witnesses.

That means a good test plan includes both:

Negative tests are especially important because under-constrained circuits tend to fail silently in design, not at syntax level. If you forgot an assertion, a bad input may still execute successfully. That is the exact behavior you are trying to detect.

Keep failure expectations explicit

For each Prover.toml case, write down:

Without that discipline, “debugging” turns into trial and error.

5. The debugging loop: isolate, minimize, and inspect artifacts

When a Noir circuit does not behave as expected, there are only a few honest possibilities:

  1. the source code does not encode the intended relation
  2. the inputs in Prover.toml are wrong
  3. the private/public split is wrong
  4. a compiler- or version-specific behavior differs from your assumption

The best debugging workflow follows that order.

Step 1: reduce to the smallest failing example

Suppose a larger circuit fails during nargo execute. Your first move should not be to add more complexity. It should be to remove as much as possible while preserving the failure.

For example, if you suspect the issue is in a single inequality, create a minimal circuit:

fn main(x: Field, y: pub Field) {
    assert(x != y);
}

Then reproduce the problem with just those inputs.

This technique is standard engineering practice, but it is especially effective in ZK systems because constraint systems can be difficult to reason about globally. A small reproducer lets you verify whether the issue is:

Step 2: separate witness bugs from visibility bugs

If execution succeeds but the protocol semantics are wrong, ask whether the issue is actually about pub.

The primer distinguishes very clearly between:

Do not mix these ideas.

When debugging application-level behavior, it is often useful to temporarily simplify the circuit so that one input is clearly public and one clearly private. The sample pattern does exactly that:

fn main(x: Field, y: pub Field) {
    assert(x != y);
}

Now you can reason about whether the externally known statement is the one you meant to expose.

Step 3: confirm the compiled artifact is being refreshed

The primer states that nargo execute automatically compiles if the program was not already compiled or was edited, and writes compiled artifacts under ./target/hello_world.json.

That means artifact awareness is part of an honest workflow. If behavior seems stale or confusing:

The primer does not document cache-clearing or advanced build options in the provided excerpts, so avoid assuming them here.

Step 4: treat witness generation as a debugging signal

A successful witness generation means: for the given program and inputs, the relation was satisfiable.

A failed execution means: at least one assertion or constraint prevented witness generation for those inputs.

This binary signal is more informative than it seems.

If a negative case unexpectedly succeeds, the usual explanation is not “the prover cheated.” It is much more likely that the circuit did not constrain what you thought it constrained.

If a positive case unexpectedly fails, the usual explanation is one of:

Step 5: use a “spec first, then circuit” debugging mindset

Before changing code, write one sentence describing the intended relation.

For the basic sample:

The prover knows a private x such that x is different from the public y.

Then compare that sentence directly to the code:

fn main(x: Field, y: pub Field) {
    assert(x != y);
}

If those do not match exactly, you have found the bug or at least the ambiguity.

This sounds simple, but many circuit bugs come from a drift between prose protocol definitions and encoded constraints. An honest workflow keeps both visible during debugging.

6. A production-minded workflow for teams

Even with only the verified Noir tooling in the primer, you can run a disciplined workflow that is suitable for serious evaluation.

Keep circuits and input cases together

A practical project layout starts from what nargo new gives you:

The primer also introduces Prover.toml. Treat that as part of the circuit’s executable documentation, not just a temporary local file.

For each important circuit state, keep:

Use assertions as review points

During code review, every assert(...) should answer:

That is a more useful review discipline than only asking whether the code “looks right.”

Don’t over-read success

A single successful nargo execute proves only that one witness exists for one input assignment.

It does not prove:

The primer explicitly says that proving and verification require a proving backend such as Barretenberg. So a prudent workflow separates:

  1. circuit-level testing in Noir
  2. backend proving/verification testing
  3. application integration testing

This tutorial focuses on the first stage because that is what the provided material verifies directly.

Add version discipline

This tutorial is written against the Noir docs snapshot fetched 2026-05-20, referencing the dev docs and their navigation mention of v1.0.0-beta.21.

That matters because tooling around testing and debugging evolves quickly in ZK ecosystems. If you see examples elsewhere using commands, flags, or APIs not shown in the provided primer, do not assume they are available in your setup. Verify against your local installation/compiler version.

Know when to stop debugging Noir and start debugging protocol design

Sometimes the circuit is behaving correctly, and the real bug is in the protocol assumption.

A few warning signs:

At that point, more CLI work will not rescue the design. Rewrite the relation, then rewrite the circuit.

Caveats

  1. This tutorial intentionally avoids undocumented Noir APIs.
    The provided primer does not verify a dedicated nargo test command, test file conventions, tracing macros, or advanced debug output. If your local toolchain supports them, verify against your local installation/compiler version.

  2. Code examples are deliberately minimal.
    They use only syntax that is directly supported by the primer’s examples and language discussion: fn main(...), Field, pub Field, and assert(...).

  3. A successful execution is not a proof-system integration test.
    The primer distinguishes Noir compilation/witness generation from proving and verification, which require a backend such as Barretenberg.

  4. Public/private mistakes are common and not always obvious.
    The circuit may be logically correct yet still violate application privacy requirements if the wrong values are marked pub.

  5. Negative tests are not optional.
    In ZK development, a circuit that only passes valid examples may still be under-constrained. You need examples that should fail.

  6. Artifact behavior can vary by version.
    The primer documents ./target/hello_world.json and ./target/witness-name.gz, but file naming, exact formats, and surrounding tooling may change. Verify against your local installation/compiler version.

See also


Where to go next

Thanks for reading this far. If “Noir testing and debugging” connected with where you are, three concrete next steps:

Learn more in Noir

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.

Find paid work in Noir

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.

Audit your own ZK pipeline

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.

If this saved you time

Three ways to support this work, pick whichever matches your situation:

$15 · One-time
All 17 tutorials as one Markdown + companion code repos. Offline-readable.
Get the Bundle →
$19 / month
Real-time bounty alerts pushed to your Telegram, filtered by ecosystem.
Try Radar →
$99 · One-time
Pre-flight audit of your ZK repo. See sample.
Order Audit →

Free alternative: Sponsor on GitHub · Star the repo · Share with one ZK developer who'd benefit

Related projects