Trace bug in Noir/Aztec with Claude

Elena

Cryptography Engineer

During our zkTLS project in Aztec, we encountered a blowup in circuit size upgrading from v3.0.0-devnet.5 to 4.0.0-devnet.2-patch.1. A verification function that previously cost 711k gates was now 3.6M (yes million) gates. Similarly, a function that cost 794k in v3 now exploded to a gatecount of 2.5M. The most interesting part was that another function with lower cost, 321k before, only increased slightly in comparison: 396k gates. No other team seemed to experience the same issue. So ultimately we decided to try to figure out the root cause ourselves, with the use of Claude.

We have been working with Noir and Aztec for various projects and have experienced our fair share of version upgrades. Our suspicion, coming from those previous experiences, was that this change seemed like a bug - just for a quite particular case. We encountered such a situation once before, and then we also enlisted the help of Claude to make progress in the issue, which was successful. In this post we'll take you on the journey of getting down to the root of the problem and see if it got fixed in the end.

The issue we encountered before, and got sort of solved with Claude

When this writer was still blessed with a MacBook Pro with an Intel chip strange things started happening when upgrading from v3.0.0-devnet.5 to v3.0.0-devnet.6-patch.1. At first, it seemed there was an issue with compilation for larger contracts since no verification keys got generated. An issue in Github was reported.

Then, trying to compile with newer Aztec versions whenever it was possible, I realized compilation of Aztec contracts simply failed silently. I asked around on Noir Discord, Slack channel with some Aztec team members and the ecosystem Signal group, but no-one else seemed to encounter the same issue. This is when I started suspecting the Intel chip was the problem and opened a new issue.

While I had been using Claude to figure out what to do differently so I could compile, Mikerah (@badcryptobitch) suggested trying to find out what the bug in the Aztec stack was. In the meantime, I was already waiting for my laptop upgrade (with non-Intel chip) to arrive, which I knew would happen to solve the problem. So it became a battle between the solutions; will Claude beat Apple delivery?

The plan for Claude was simple:

  1. Find hypothesis of bug root-cause. It had to fit with:

    • the timeline of when the bug was potentially introduced

    • the fact that it only seemed to fail for MacBooks with Intel chips

  2. Fix bug.

  3. Build fixed version locally and compile contract to check if the fix worked.

For step 1 I let Claude do its thing in Plan mode. At first, I did not include the requirement of the timeline and this made Claude hallucinate a couple of potential issues that I did not agree with. Realizing that I discarded them based on the fact that that code hadn't changed in a few years, I added the additional context that it had to concern a bug that had been introduced in the last few months or so. Of course, existing code can prove to be an issue, especially in combination with newer code, but it seemed logical to go for lower hanging fruit first.

Then Claude presented several new hypotheses and after reviewing the options, it convinced me the issue could potentially be in CMakePresets.json, since it contains settings for builds. This would align with the timeline, because changes had been made in October 2025 and formed part of the changes between version 3 and 4. The proposed fix by Claude was to change 2 lines in the mentioned file. The explanations of why this had to work did not convince me, as I don't have the full understanding of these build settings. However, I reasoned that the correctness could be confirmed by testing it out.

We created a local build of Aztec with the fix and tried to recompile the contract that gave the issues before. It worked!

Celebrating this win, I shared Claude's finding in the Github issue thread and pointed to the fix Claude had suggested. At this point I was pretty convinced Claude had proposed the right fix and that the Aztec build would soon be fixed for my Intel chip MacBook. The Apple delivery was still a few days out, so it seemed the contest had a clear winner.

But then it turned out that the "fix" Claude and I did was actually the fact that the Aztec version was built locally, as the team responded in Github. I had only tried to build a local Aztec version including the fix, not without it, and when I tried the latter the suspicion of the Aztec team was confirmed. We did find the problem, but we did not propose the right fix.

However, this piece of information did make it possible for the Aztec team to reproduce the issue and ultimately find the root cause in the next few days. The deeper cause was an issue in the build system Zig and the Aztec team in turn reported this themselves. (Interestingly, it seems that issue was closed because of a strict no LLM/no AI policy for opening issues.)

In the end, I didn't wait for the fix to be finalized and backported into Aztec v4, because my new laptop had arrived and it had compiled the contract in mere seconds.

There were some good lessons learned from this experience. You can actually be helpful by trying to find out what the bug is yourself by using AI to investigate it. In this case, reproduction of the issue was a problem and it could not be given high priority by Aztec. But after dedicating some time to find out what was wrong, we were able to help the team look into the right direction and focus their efforts more effectively.
On the other hand, while I could verify the fix Claude "worked", I did not have the full understanding of the issue or the proposed fix itself since Claude did the work. If not careful, you can create junk for the team and potentially give them more work having to review your proposed solution. All in all, it is as with all the usage of AI: use with care and make sure you keep thinking yourself.

Gatecount blow-up in zkTLS verifier contract

Now onto the main case of this post; the circuit size blow-up we encountered during integrating Primus zkTLS on Aztec. In short, zkTLS allows us to bring verified web2 data on-chain; Primus provides attestations over data from a chosen website and this data gets verified in the Aztec smart contract. If you're interested in more details, there is a write-up on the project itself, as well as a tutorial on how to use the SDK we built to incorporate zkTLS in an Aztec app. For this post all you need to know is that in the zkTLS verifier contract, we would use the json_parser library to parse the private data (bytes) and do some checks on it. It was in this library that we saw the gatecount increase happen.

The first version of the zkTLS project was built for v3.0.0-devnet.5, a version compatible with the Aztec devnet at the time. Apart from the SDK, it contained 3 examples of verifying an attestation json; two "commitment-based" and one "hash-based". The larger the private data that is passed the more expensive the circuit becomes, both because larger input in itself is more costly and because doing the JSON parsing is expensive. One commitment-based example and the hash-based example depended on larger input data and were therefore more expensive. The other commitment-based approach was smaller. See table below for circuit sizes (v3).

Then, when upgrading to 4.0.0-devnet.2-patch.1 we saw a huge increase in the larger commitment-based example, as well as the hash-based example. The smaller commitment-based example also had an increased gatecount, but the difference was much smaller.

Method

Circuitsize v3

Circuitsize v4

Increase

Commitment-based verification

711.763

3.907.426

5.5x

Commitment-based verification (small)

321.513

396.142

1.2x

Hash-based verification

794.646

2.509.592

3.1x

Changing between versions it is possible that the cost will change too, but what stood out here is the difference in increase across the different examples and the fact that other teams did not report this type of increase in circuit size.

Ultimately, what in our opinion confirmed the suspicion that this was caused by a bug was the information coming from the flamegraphs that show where the gatecount can be attributed to. See the images below; the top one is for v3, the one below for v4. In the flamegraphs for v4 the function call to get_values (from the json_parser library) accounts for 3.2M of the total 3.9M gates for the full verification function. And what's more, we can clearly see the contribution of acir::memory::op as the major root cost of get_values, whereas for v3 that contribution is not overpowering and you have to zoom in to distinguish it.

We notified the Aztec team of our findings, but only internally. In the meantime, since we were quite blocked by this we found a workaround that allowed us to eliminate the json_parser library completely and cut the circuit size in half in comparison to the v3 cost. In the last version of our repo, we have added 4 different examples (2 commitment-based and 2 hash-based). In the end, the circuit size blow-up did us good, because it pushed us to remove the json_parser dependency.

Recently we checked if the circuit blowup issue for the json_parser still existed in the newer version 4.2.0-aztecnr-rc.2 and it did. So we decided to let Claude look into the issue and see if we can make some progress on it.

Tackling the circuit blowup using Claude

Starting this experiment I have the following assumptions:

  • the issue is probably present in the repository aztec-packages

  • it has something to do with acir::memory::op

  • the bug was introduced within the last couple of months (after v3.0.0-devnet.5 release to be precise)

Furthermore, a proposed solution can convince me if: the gatecount for my example lowers AND the original tests in the repo still pass.

Finally, it would be my preference to have Claude point to the problematic code and find the code myself. Or, at least fully understand the issue it finds. (This would be my preference given the previous experience of not quite understanding Claude's proposed solution and it turning out to be only partially correct.)

For the first investigation I give Claude basically the context that I sent to the Aztec team regarding the blowup in circuit size + the findings from the flamegraph. I let it have the context of the aztec-packages repo and ask it to focus on a change that happened in the last few months. It comes up with 3 hypothesis, none of which I can directly confirm because they are all quite complex.

I create another example contract where I trigger the same circuit growth using the json_parser for various sizes of input. With this experimental data I ask Claude to confirm any of the hypotheses. In addition, I want to be pointed to the files of interest in the aztec-packages repo, in the hope that I maybe can find the bug manually when knowing where to look.

According to Claude there is definitely a bug, the new example confirms this and helps pinpoint where it goes wrong within the json_parser library. Its suggestion then is to make changes to this library. This makes me wonder how often I have to repeat that I want to find the bug in aztec-packages.

My patience running thin with AI proposing solutions I have not asked for, I take a stab at reviewing the files Claude referenced as where the root cause in aztec-packages could be. Unfortunately, these are all located in the Noir compiler (noir-lang/noir, a submodule of aztec-packages) and I don't have enough understanding what could be going wrong here. Furthermore, I'm sceptical that this is the right location because of my assumption the bug would originate in Aztec. So I ask for a minimal example in pure Noir that shows the gatecount explosion, and Claude delivers:

fn fill_local<let N: u32>(enable: [bool; N]) -> ([Field; N], [Field; N]) {
    let mut data_a: [Field; N] = [0; N];
    let mut data_b: [Field; N] = [0; N];
    let mut ptr: u32 = 0;
    for i in 0..N {
        data_a[ptr] = i as Field;
        data_b[ptr] = i as Field;
        ptr += enable[i] as u32;
    }
    (data_a, data_b)
}

fn main(enable: [bool; 256]) -> pub ([Field; 256], [Field; 256]) {
    fill_local(enable)
}
fn fill_local<let N: u32>(enable: [bool; N]) -> ([Field; N], [Field; N]) {
    let mut data_a: [Field; N] = [0; N];
    let mut data_b: [Field; N] = [0; N];
    let mut ptr: u32 = 0;
    for i in 0..N {
        data_a[ptr] = i as Field;
        data_b[ptr] = i as Field;
        ptr += enable[i] as u32;
    }
    (data_a, data_b)
}

fn main(enable: [bool; 256]) -> pub ([Field; 256], [Field; 256]) {
    fill_local(enable)
}
fn fill_local<let N: u32>(enable: [bool; N]) -> ([Field; N], [Field; N]) {
    let mut data_a: [Field; N] = [0; N];
    let mut data_b: [Field; N] = [0; N];
    let mut ptr: u32 = 0;
    for i in 0..N {
        data_a[ptr] = i as Field;
        data_b[ptr] = i as Field;
        ptr += enable[i] as u32;
    }
    (data_a, data_b)
}

fn main(enable: [bool; 256]) -> pub ([Field; 256], [Field; 256]) {
    fill_local(enable)
}

Using this code, I can generate gatecounts for various values of N, which shows the gatecount grows approximately quadratic with respect to N.

N

N^2

Gates

32

1,024

1,246

64

4,096

4,542

128

16,384

17,278

256

65,536

67,326

To me this confirms the issue is indeed in Noir itself, and not necessarily in the smart contract functionality that aztec-packages puts around Noir. In addition, I won't be able to pinpoint a bug myself without getting very deep into the workings of the compiler. So at this point I let Claude create the fix, accompanying tests and an explanation of what is wrong and why this fix is correct.

The good news is that for the minimal example the gatecount goes down, as does the gatecount for the example contracts if I compile Aztec with this adjusted Noir version. But as I'm not very familiar with the Noir compiler, I can't definitively confirm the issue is correct and the fix is correct from the code. My main issue with Claude's fix is that it proposes a few changes to existing tests, claiming they were "too strict". This is a red flag and goes against my initial set out criteria for evaluating the fix: it would improve the gatecount AND pass original tests. I challenge Claude's solution multiple times and push back on changing the existing tests, e.g:

  • why this is the fix and how can I verify this?

  • I see that you changed 2 existing tests. Are you saying the tests were incorrect?

  • create the fix with the original tests unchanged

    • this was not possible i.e the original tests didn't pass

  • review this issue & fix as an Aztec/Noir engineer

Finally, accepting that the changed original tests are part of the fix, my only concern is that this might be a design decision rather than a bug. The only way to find out which one is the case is to have the Aztec/Noir team look at it and thus it's time to create a Github issue.

I filed the issue in noir-lang/noir; you can find it here. In the introduction I explained the issue was flagged by us before, and we had Claude look into a potential root cause and fix. The description of the issue is similar to what was communicated to the team before, to make clear where the issue surfaced for us. Then follows the part of Claude's analysis, including the minimal reproducer in pure Noir, the point in the Noir compiler that is the root cause and the question whether this is by design or a bug that can be fixed. I kept this part as succinct as possible, reasoning that I didn't want to waste the team's time in case it wasn't a bug and I could always provide more information if it was necessary. In addition, I created a draft PR that contained Claude's proposed fix and mentioned this in the issue.

Given what happened with the previous issue Claude and I "fixed" I was expecting there to be some pushback, but the issue was picked up swiftly and the team ended up fixing it in a new PR that was essentially the same but cleaner. My worry that the fix required existing tests to be adjusted proved not necessary, since this was indeed part of the improvement. The fix is already included in nightly-2026-04-25 and up and will also be part of Noir v1.0.0-beta.21.

The final check was to see whether the original contracts for which we flagged the issue went back to the gatecounts as in v3. For this, I compiled a local version of Aztec that used Noir before and after the fix. The contracts required small changes in order to be compiled, but the overall functionality stayed the same. This was the final confirmation that we indeed found the bug and it was fixed!

Function

v3 Baseline

Before (nightly-2026-04-23)

After (nightly-2026-04-27)

example1::verify_comm

711,763

3,898,410

694,267

example1::verify_hash

794,646

2,500,493

787,864

example2_small::verify_comm

321,513

387,518

313,884

Conclusions

The experiences finding the issues and ultimately helping to find their fixes definitely is a motivation to keep trying to contribute in this way whenever such a situation occurs. In both cases, we were convinced there were actual bugs happening and they seemed pretty specific. Previously, when the team would not look into it we would keep bugging (no pun intended) them about it. Now, we are actually able to help out in trying to find the issue, even when we don't have the deep knowledge about the codebase.

It is cool to know we were able to contribute to future Aztec developers not experiencing a huge circuit size for the specific use-case, which could, for example, help the adoption of the json_parser. The same holds for developers out there who still have a MacBook with Intel chip; they might never know one day they couldn't develop on Aztec because compilation was broken! On the other hand, it is important to be respectful of the team and their time. As we saw for Zig issue that the Aztec team filed because of the first bug we found, it was closed due to the strict no LLM/no AI policy. It is understandable teams want to prevent AI slop clogging up their workflow or codebase, but at the same time above experiences show that using AI can truly be helpful in letting the community help to solve issues.

My main takeaway is that I see it as an attempt to be helpful and that this can really work. In this attempt to contribute I think it's important to try to deliver the highest quality possible with the knowledge I possess and be as succinct as possible towards the team. I don't consider it "helpful" if they have to comb through a huge AI generated report, of which we have not tried to verify its correctness. But as an experienced member of the community, with a (correct in this case) suspicion that a bug is occurring, presenting our findings to the team with the question on what their assessment is has proven to be valuable in the 2 cases that were presented to us.

We'd like to thank the Aztec team for their collaborative posture and patience while finding out what these issues were. Different members of the team got involved in the GitHub threads, including co-founder Zac Williamson himself, Jan Beneš, jfecher and Jonathan Hao. Highly appreciated!