Building a Production-Ready Library in Noir
NoirCon 3 at Devconnect Argentina 2025 · 20 Nov 2025 · 25:45
Maxim Vezenov, a Noir software engineer, gives a hands-on deep dive into advanced Noir features through a real-world case study. He builds a simplified big number library using traits, generics and metaprogramming, highlighting best practices for writing flexible, performant and scalable zero-knowledge code. The session was part of NoirCon 3 at Devconnect 2025.
Speakers
Topics
From NoirCon 3 at Devconnect Argentina 2025, hosted by Aztec Network.
Transcript
Good morning, GMGM. Thank you everybody here for taking choosing [music] to spend your time at our third inaugural Noir. >> The goal here is to take you through how you might actually start to utilize some of Noir's more advanced type features. Noir is a very expressive language and a lot of times when people think of ZKD DSLs, you might think of something that is more lowle. um you have to be and as you know one of Noir's goals is to be as uh developer friendly as possible but I want to take you through like a narrative of what we actually might want to build. So as you might know uh ZK Passport is a project that's now in
production. Um and what's the what is ZK Passport actually trying to do and how does it keep your identity private from third parties. So in modern passports, we have this chip um RFID chip that basically has your personal information and it's that's what actually like the government is going to scan when it's trying to verify that you have a valid passport. And then once you actually have the ability to prove that you contain a digital signature, that is actually gives you the ability to prove personal information. Um the goal is to verify this signature in a ZK circuit using a language such as Noir. Um
however the issue is that every country like once you start to want to actually apply things in production you realize it's there's a lot of edge cases. Every country has its own uh cryptography. They use their own cryptographic curve. Um I've listed a few here but there are many many more. Um, you can probably talk to the ZK Passport guys about how many different uh different curves there are and different bit sizes and different uh fields. The issue is Noir's native field right now, the one that we support natively is 254 bits. Well, I' I've listed a few that are bigger, but there's many that are bigger.
So yes, as I as I just kind of went into uh not just in the in uh ZK passport, but in general, lots of different cryptographic systems in the real world use larger scalar fields. Um so we have to then ask the question, how would I actually verify a signature on one of these curves in a zero knowledge circuit? Basically need some way to represent our number in larger than 254 bits. So this is what we wish we could do. The field type is what's natives is what's native to noir. Um I wish we could just have a public key X public key Y RS you know message hash and then we just easy.
It's it would be really nice. Um I have a repo. Um I this we're not I don't expect you guys to code along but it's been broken up into stages that might make it easier for you to follow along. Um and then in the presentation I'll I'll uh be going through the different segments but the repo has also more content that I think would be useful for after the talk. So let's just let's get started on building basically a big number. So I want to verify on the brain pool P384 curve. So we'll start with this simple structure just four different limbs. So we're trying to represent in no something that's bigger than 254 bits. Here's
a simple this is kind of a very simplified version of how we might we might construct it. Um and everything's in big Indian. You can of course look into the repo to you know if you from you want to explore it more um because I'm going to probably go be going through these methods decently quickly. But in general the idea is we want to take in a bite array and we want to convert it into our type structure. We can then test it in Oir. You can easily use nargo test as you would expect with something like rust using cargo test.
We can add our arithmetic operations. This is not how you would implement it in production. There is you have to have you have to propagate your carries across limbs for accuracy. But this presentation I know it said a production ready library but this is to show off the type system of noir in our and the features there rather than the arithmetic correctness. We can then test that method as well. And you can see I created a a five big number three big number. We've added them together and our last limb is eight.
multiplication. It's a little bit more complicated. Once again, this is still not this is still a simplified version. Um, we're basically trying to multiply in little Indian which is a little bit easier to reason about. And then we're going to switch back to big Indian back into our actual format uh our type structure that we have. Uh this is yeah you can see noted here that there's a lot of different things that are not actually properly handled. And then of course we can check that as well. And here we're just doing a 6 * 7 = 42.
So we go back to what we wish we could do. Now we have we for our public key X and our public key Y. We have a structure for our FQ that maybe we can use and start doing some the actual arithmetic operations on that we want. But R and S in the verification algorithm, they need a different field. Uh FQ is the base field of our curve. But we also need the scalar field to actually verify our signature, which we refer to as FR. So this is what like we actually want to get to show it like so we want to be able to construct an FQ and an FR.
So let's let's construct our our FR. Um once again it looks very similar. This one at least is just four limbs. Actually the structure is the exact same. You can notice in the last bullet point though this is a finite field though they do have different like moduli. So when you like this they do behave differently in practice and when you saw the multiplication earlier one of the notes is that you need to have a modular reduction uh where you basically modulo arithmetic operation you're doing that is we're kind of handwaving that for now but let's let's start to generalize our structure so we can create a big num um in noir we have
generics and we have numeric generics so you can see here how I have both a limbs. So that is used to specify okay how many number how many limbs do I want and then a params this is where I'm just kind of handwaving the the modulus a little bit and you can just assume that there's field specific information in there. So now we just have we can make a generic structure. So we can we still only accept a bite array of 48 limbs but we're using our numeric generics limbs.
this would live inside of the uh type implementation. So if thinking from rust we have imple those those that numeric generic same thing you can see the limbs uh we're iterating over them like this and then the same thing for multiplication. So now we want to distinguish their parameters. Um we have the ability to specify traits in Oir. Um if you're unfamiliar with traits, a trait is essentially just an interface to that defines several methods. Um and types
will provide their own implementations of these methods. So for our FQ, we have this this implementation of the modulus. This is the modulus for FQ. And then for FR, this is the modulus for FR. They're very similar here, but the first limb you'll see is actually I'll just go back. You can see the D3A and then 3AB. So we do have a different implementation for this modulus function. And then now we can specify two different types using type aliases, which is also something that's supported in noir. So now we have an FQ and an FR.
Oh, okay. based off of this uh based off of this big num structure. And now we can use it in tests as well that type alias just as we would have used our big num structure and I this is the the switching to it. Well, the thing is we have now created this struct this structure but we want to have some generic interface for our big num and here we've this is the aware constraint for our trait. So we've basically said FQ and FR must be a big num a big num interface. So before we had a structure where that's that's
doesn't you can't just do that here. you need some kind of interface which is the traits that I referred to earlier but we were using it for the parameters. So now we're going to construct a big num big num trait. So every big number is expected to implement this trait. And you can see I have a simplified interface here where we just have a constructor and add mole. And then now we actually have the params is returned directly from this big num tree.
This is what we were you know specifying in our wear constraint in the earlier slide. So for each of the structures now you have to implement your trait. So I'm not I'm going to leave out the implementation now because you've seen it enough times. But every you now have for FQ and FR, we've implemented these these methods. And same thing here. We have the ability though in Noir to take advantage of operator overloading as well. And this is something very nice when you start working with our our trait interfaces. So you can see it from our old test. we do a add b. Um, it would be really nice to not have to do
that though. So, this is operator overloading lets us define operations over like different more uh custom type custom types. So, we can implement traits such as EQ, add, mole, and these operators will then become available to those types. So now in our big num trait we'll specify that it has a what we call like we can essentially inherit inherit the other traits like add mole EQ it's technically referred to as a parent trait.
So when I have my big num imple like this if I don't implement one of these parent traits I'm going to get this error here. And you'll see it's telling me that I've not satisfied a certain trait constraint which is our in this case add. So now I can make a trait implementation for my FQ big number. And this looks the same as what we previously had. But now in my test I can just do a plus b. Um which can improve our you know user experience a lot more. And also at the end you'll see I'm doing I'm using the EQ operator as well.
However, the thing is each implementation is we don't want to have to do that for every big number. Uh there's a lot like I said there's every system has like a every country for every passport has tons of cryptographic curves, tons of cryptographic numbers. Do I want to make the same boiler plate every single time for every single function? No, you don't. Um, so how can we how can we reduce this? And noir supports basically compile time meta programming to make this a lot easier. Uh, it lets us generate repetitive code in noir. So we can code noir code in noir and we can we want to be able to make a macro that can you
know have a type. So our structure so FQ or FR would be our type. you know, accept some it accepts some parameters like the number of limbs or, you know, the actual big number parameters and then we should automatically generate basically everything. And we want to then be able to specify a big number with just a single call to a macro. So I'm going to quickly before we get into that describe how noir's meta programming works and there's two concepts really. We have comp time. So the comp time keyword you see there this means everything is executed at compile time and then the quoted and and unqu is essentially just we are taking a
string of tokens basically parse this uh code as just raw as you would any noir code. So we're just injecting noir code into no code. So in this case we have quote one. So this just literally the literally just one and then we can unquote using the exclamation point. If you're familiar with Rust macros, this is how you invoke a Rust macro. If you ever seen like print line exclamation point, it's a call to a Rust macro. So here we're doing an unquote. So that that uh B is is at compile time read like is going to just be B equals 1. It reads like as it does here when you're
writing your noir program, but that's actually what it's going to be. And then we can use it in inside of our noir code. So here we're just doing a simple as we're just casting it into a different value to give a little bit more an example. Uh you can also unquote inside of your quot inside of your quote. So here we have an x that is just 1 + 2 but it's not it's not I want to be clear like 1 plus 2 is not being evaluated like it is just 1 plus 2 as written in code as you see it right there. So y is actually just going to be 1 + 2 + 4 and then that's what will be evaluated.
So let's do that for big num. Uh we do have some extra compile time specific like types. So you can see type definition refers to a structure. So this means we're accepting some struct some strruct. We're accepting a U32 and then we're accepting a quoted. So something that is quote and there's something inside those those quote brackets and we're returning a quoted as well. And obviously the goal here is like as I said earlier we want the big number add mole EQ.
So here let's let's get into how we how we can do our meta programming. So we have our implement big num trait. Big num is already it's in our crate here. We're accessing it through a path and we're calling that unquote dollar sign operator. So we are just we're saying I want to implement bignnum as it is written there for this given type. So if I was to think about the implementation we had earlier, this would just read implement bignnum for brainpool p384 fq.
And then we also have some helper functions where now we just purely operate over the limbs. And then you can also see how we've also implemented the the params function. And that's just uh returning the parameters that were supplied here. Does that make sense? Any questions on this one? Or should we just move forward? Or maybe I should wait till at the end we do questions. All right. I said how we have this generic uh limbs function. Now, this looks very similar to the function we originally had when we just had our generic big number strct, but now it's just a standalone function.
And now we can also do the same thing for our type implementation. So we have our standard lib imp uh imple and then we for the given that given type and once again we have a generic or generalized add limbs method add limbs once again looks like we saw earlier. We can do it for all our types now or all our uh trade implementations now. And this is how we now have our meta programming specified for each impul.
And now when it comes time to specify a custom big int type, I can call that macro in an attribute on top of my structure. So the you can call invoke a macro like this on top of a type and it's going to basically immediately unquote. So that's what I mean by bringing that that uh token stream back into noir. So the first thing that will be passed to derive big num is going to be the type itself. You see fq and fr that was the first parameter of our macro. It's going to be passed into that function. And then they're both four limbs and we have some certain params that we've specified earlier that I showed you where it was just a a structure with a certain modulus. This I'm using a structure
because in like a production big number you're going to have a few other parameters and it's better to just use a structure cuz you might be able to extend it more in the future or just extend it more easily. And that's how we then invoke our macros. I just want to highlight though that in our production big number we even though there's all this abstraction all this kind of uh what looks like should slow down the code it is still quite efficient. Um this is an ultra plunk and ultra honk although ultra plunk I don't believe Brettonberg supports at all anymore. So the only one that's really relevant is the one the two columns on
the right there. um 100 multiplications with a 2,00 bit big num you can see is 1.4 seconds. This is in milliseconds here. And then additions 4 400 milliseconds. Uh this is usually more operations than you need for most applications. Um so it's still quite fast and the zk passport like actually generating a zk passport proof which does a lot more. It's doing like curve operations which use this big numbum as a dependency occurs in about a minute.
Um which it's been a lot of work to to get to that time. Um I want to credit ZK Passport though if you guys should check out their circuits where you can see how they actually use this in production. And give a second for people to scan. And I want you guys to, you know, take a look, start uh here's a link to the docs and also my ex I'm going to and you can uh if you have questions, you know, tweet at me or go on GitHub, file an issue, you know, someone at the team will be happy to respond.
And yeah, I'm happy to take questions now. Um, and I can go back in the slides to as needed and stuff. Um, yeah. Um, I'm not sure who's handing out microphones. Um, but we're ready for questions. Yeah, >> it would be very unlikely to upstream that into Rust. Um, Rust has a whole process for for their process and Noir is not actually like dependent on Rust in that way. We we're built we we the compiler is built with Rust, but this is Noir's unique meta programming. We found
Rust's meta programming to be inefficient and insufficient for our our our needs. Um, I actually find our be a bit easier to to work with. Um, so yeah, no, it would not be upstreamed into Rust. The comp time keyword is much more similar to something like Zigg if you're familiar with Zigg. Um although ours is I'd say a bit more flexible. Um which has trade-offs though. >> Yeah, I was going to ask about that because um there is a way to do like compile time functions in Rust which is the const keyword. So I was wondering why the comp time keyword. I don't use zigg so I wouldn't I wouldn't know but
uh yeah const functions are a thing in rust and and I believe it should be the same thing or is there any difference >> um I am familiar with the const keyword in rust but I am not familiar with this uh that they have some advanced compile time uh interface of of I'm not aware of that maybe we can discuss it after but the in general we are closely modeled after us and their syntax but we are not it's still a distinct language and so we felt the comp time keyword was clearer that this is occurring at at comp time um everything is a value it's not only it's not just a constant that exists um it is like in rust you can specify I
know you know we have a a constant and that but that might be like a global constant that you can call to in a method here everything is also evaluated at at compile time. Everything in com time is evaluated at compile time too. I don't that might be the a distinction but we are not going to be always kind of following rust exactly even though it might be very rustl like the syntax. >> Yeah. When you do a const function it's the same thing as a compile time function. Yeah. >> Okay. Well either way it's still just a keyword because we could just have const. Uh I find comp time to be a clearer keyword.
Uh yeah, thanks. Um uh I'm trying to run the workshop demo, but it looks like it's not similar as the slides. So there is more information in the in the workshop demo. um like just the I've because I'm just took splices from here. But if you run nargo test, it should all run. >> Uh >> if not, please file an issue. We'll I'll fix it. But >> like to run now or later? >> Um well, if you go So I tried to put it into stages. Master on that repo is not going to be what you need to look at.
There should be you if you go to the pull requests. Sorry, I should have specified that. Um, and like the branches, we have a stage zero. Stage one. Stage zero is just the very simple big num. Stage one then should be our generic bignum that we is it going to I guess we're okay. Stage one is the just the our generic bignum that we had I showed originally. And then stage two is going to be we actually implemented a big num trait. And then stage three will be using meta programming to get rid of that boiler plate. So yeah, I wanted to try and split it up so you could see the progression from you know the initial just very simple big num.
Does that does that help? Any other questions? They can be about they don't necessarily have to be about the presentation. All right, sweet. Thank you for listening. Um, happy to happy everyone's here and I'll be at the the booth there if anyone has, you know, questions you don't want to ask right now and you just want to ask it privately.
All right. Thank you guys. [applause]