NoirHack: Noir Workshop - Advanced Noir

NoirHack 2025 · April 2025 · 44:12

Noir software engineer Michael Klein breaks down unconstrained functions for optimising zero-knowledge performance in this NoirHack workshop. He compares Brillig with ACIR and shares advanced composition and debugging techniques for developers writing Noir circuits.

Speakers

Topics

From NoirHack 2025, hosted by Aztec Network.

Transcript

All right. Hey everyone, GMGM. Uh, welcome to another uh Norhack workshop. Uh, today uh I'm joined uh by Michael, a compiler engineer on the Noir team at Aztec. Um, so he will help us uh walk through a more advanced session where he'll go into unconstrained functions and optimizing performance. So I will hand it over to Michael. Uh, Michael, go ahead. Hi. Uh, great to see everybody. Um, as Rafie mentioned, I'm Michael Klein. I'm on the Noir team. And today I'm going to be talking about unconstrained functions specifically for optimizing performance and doing some things that might be hard

on uh, non CK languages. So, uh, unconstrained functions look like this in noir. You they look like a rust function where you write unconstrained instead of pub or in addition to pub. Um, and I'm going to talk about a a broad mostly strategy based with a couple examples approach to how you can use unconstrained functions. Uh I'm going to talk about MP complexity, how they can be used for searches, uh special cases for invertible functions, a general pattern about validating and connecting the input and output. I'll talk about composing unconstrained functions and

things around performance for that. And then finally, I'll talk about profiling and debugging performance. So first of all, NP complexity. So np complexity is closely related to the the scope of programs that can be easily translated to a zero knowledge proof and uh this has been studied for quite some time. There's an open problem called P versus NP uh which basically asks if it's easy to check that a solution to a problem is correct is also it is also easy to solve the problem. And this is a $1 million prize that has many many pages of of published research on studying it and optimizing it. And in short, if your problem is in this scope of already being tightly studied, um it's worth checking whether

you could just use the existing solution. Uh there's over a 100 problems that have been highly digested and identified to be nplete. Uh some of the most popular ones are boolean satisfiability, integer programming, and graph coloring. Uh these can be applied to to very broad things such as uh boolean satisfiability for permissions. Integer programming for pretty much anytime you have discrete resource constrained actions. It may be applicable and graph coloring can be used in a similar way for more social or network constrained actions. So how do you go about doing this? Because again there's there's a lot of existing research. Well, how I approach it is I start by saying does it look like a

popular MP problem? I have a small list of NP problems like SAT and integer programming that I'm a bit more familiar with because there's more solvers and translations for them. And uh these because they have the most attention, there's often international competitions on maximizing speed for these. So um if if that doesn't apply, does it look like one of these commonly known problems? Because it it might still be possible to wire together some translation to boolean SAT, for example. A lot of people will just translate to SAT as like a first approach but there can be often be there can often be a lot of overhead doing it just directly. Uh and somebody's probably already studied how to reduce that translation cost um which can be polomial. So it's worth looking

at the existing research there. And then finally, can you do a kind of a fresh translation efficiently to an NP problem? Uh, and if not, kind of the rest of the talk is for those cases. Um, where it's not obvious that somebody's already put a ton of work into it. And we know we want to optimize things with unconstrained functions. But I'm leading with this even though it's um MP complexity is is one of the first things that's al often mentioned with zero knowledge because um if I could totally answer the question of when to use unconstrained functions and when not to I'd have a million dollars. So just to make it clear that this is pretty challenging. All right. So one of the first things that we can do as I mentioned before is accelerating

searches. So you've probably already seen this general pattern but may not have applied it to unconstrained functions and that is Merkel tree access. So what we can think of that is an unconstrained search for the element of the tree where we we don't have any constraints. We were literally just like doing a database search so to speak and then we're looking in that for that the final path in the Merkel tree and then the constraint side is just the check of that Merkel tree path. Um this can basically be extended to many other types of searches where we need to find a needle in a haystack. Um which basically can be done in unconstrained or kind of out of band but then we might need specialized data structures to prove that there's no needle in the

haststack. Um and this is while a lot of algorithms require some amount of searching it's often like only one component. So I'll give some examples where this can be used a little bit more broadly. Uh so we I mentioned that we might need specialized data structures to tell that there's not an element in the list and one example that is is a little bit involved but uh I think it illustrates the point is that if we take a doubly linked list so we have each element of the list we can go forward or backwards in constant time um and it we back that within arena because we don't have arbitrary memory in a circuit um and we sort that list well now we can we can search to the middle of the list in

unconstrained where we need to add or remove or prove the non-existence of an element and then in constant time we don't have to look at the whole list anymore. The constrained side can only look at our target element or our target slot for an element as well as the immediate smaller and immediate larger elements. And this is kind of a general pattern. If we can convert the validation into a a constant time or nearly constant time check then everything else can be done out of band and unconstrained and we get in my mind basically ideal performance for this this part. Now of course doing the actual check itself. We might want to optimize that but at least it's it's split efficiently. uh for a slightly

more involved example that uh I can't claim is solved but it it shows how they can be a little bit more uh nuanced uh there would be parsing parenthesis. So when you're parsing parenthesis normally you might use like a stack or a recursive um like you're adding stack frames basically every time you find a new parenthesis and you're looking for the the matching parenthesis and that's not very fit for zero knowledge proofs. uh you might have like loops within loops. You might uh end up trying to do recursion in a a situation that doesn't really like recursion in the pure constraint zero knowledge side. Um, and so the alternative is instead we could just keep track of the nesting depth and

keep track of the the locations of the matching parentheses and do all of our recursion or stack handling in unconstrained and then do a single linear loop through our input in the constrained side to just check that all those all those properties are correct. Um and and this doesn't have an easy translation like nesting depth. I don't know how to translate to that to a lot of other problems. Uh but but it there's a common pattern that you add these extra bits of metadata that allow you to tell locally that things are valid more globally and you do ideally like a a linear sweep doing those constant time checks. Uh, another example that often is less thought of as a search or at

least I think of it less as a search is things like limit order matching or hello game matching. So you have with like limit orders you basically have like these ranges of things that can match and you need to do a search through possibly hundreds and hundreds of pairs uh if you were just brute forcing it. um but instead we can just say oh well the constraints are we need a match with these within these boundaries and we need to ensure that it's a valid match but then once we have that actually checking that we have a valid match could be be very fast uh similarly but um I think it's a a slightly simpler case for matching l scores for um for difficulty for like

matchmaking chess or some other multiplayer game um you probably want to do some extra checks to make sure that like the same two people aren't playing that over and over again, right? Um which kind of ruins the math for L scores, but other than that, you can basically just check that people have L in in the database and then it's correct and it's within the range for the the game mode that you're looking at. Uh so searches I mentioned are just one component but uh there's another another approach that is is very broadly applicable to unconstrained functions and that's whenever we have something invertible um and we can kind of uh uh massage the

the function a little bit to make it a little bit more invertible. But uh let let me explain the the premise which is if you have a function that's invertible you don't actually have to calculate uh the function itself. You could incent calculate the inverse and then you simply ensure that the function and inverse are match. Um so uh multiplication is cheaper than division. nth powers are much easier than nth roots um for integers uh or like over a field. Uh integer or sorry field logarithms and uh like elliptic curve logarithms can be very uh very computationally intensive to calculate but they're very cheap to to validate.

So you don't ever need to calculate one of these in a zero knowledge proof if you have unconstrained functions. you can simply validate that it's correct and calculate it out of band with more specialized algorithms. Um it's possible to extend injective functions into invertible functions by retaining lost info. This is uh a little bit abstract but the idea is that when we have an injective function we have or or even even like less than injective functions. The idea is that when we have a function and we look at the amount of data that's preserved by the function. We can kind of think of it as a invertible part and a non-invertible part where we're losing information that can't be recovered. Uh

so for example like the absolute value function just removes whether or not it's pos the input is positive or negative. The idea is that if we retain that that piece that's lost we retain well the output is let's say of neg -3 is three and a negative sign separately um then we can go back to three and all of a sudden the absolute absolute value function in some sense is extended to an invertible absolute value function. um the the co-omain or the output of the function is no longer just integers. You have the extra negative sign boolean to keep track of but but now it's an invertible operation. Uh absolute value is not super interesting but uh a more I

guess um common example is sorting. Uh sorting is not often considered to be an invertible operation because like if we have 321 we sort it to 1 2 3 how do we get back? Well, if we retain the original input and we look at the two lists as a permutation between the two lists, then all of a sudden we have a that extra piece of data that tells us how to get back to the original order. Um there there's a couple other ways to to represent or encode that original order, but it it it doesn't particularly matter. Any of them will allow us to validate that we had the original data.

Um uh a more specific example is if we have 10 mod 3 we it's it's kind of one component of the div div and mod. So if we include 10 div three then all of a sudden we have an invertible function um converting from pairs of numbers to pairs of numbers assuming we don't divide by zero and such right. Um and then like speaking of not dividing by zero, if we have like a much more specialized domain like if we have only uh square numbers then the square root is invertible. If n is divisible by m then division is invertible but we do have to check that our our premise holds in a a more constrained on the

constraint side. Uh and then for another kind of broad pattern um in general we can kind of split this up into two different steps. We can validate that the output is within our expected constraints and then we can validate that it came from the input. So for sorting we're saying that um we need to make sure that all of the output elements are exactly the input elements and we need to make sure that the output is sorted. So we can do that by checking a shuffle or checking that there's a valid permutation between the input and output order of the list. Uh but alternatively we can also um uh check it more specific check them individually if that makes sense.

Um so the this one is it's hard to come up with examples of this because it's it's kind of just a an abstract split into the two halves. But uh a more complex case I I would say is like if we have an execution trace for like a virtual machine, we can kind of think of that as well we're we're checking that the end of the execution matches what we expect. So like um perhaps the end of a a virtual machine says that there's a transa transaction constructed by the by some user to another user. Um so we we can validate validate that separately from just saying that the exe every step of the execution trace is valid. Um and

and this can kind of like help factor the problem into more modular pieces. Um more broadly we can kind of compose any functions in noir. So that includes uh unconstrained functions and functions that call unconstrained functions. So if we sort by x / 3 and as I mentioned the sorting uses uh unconstrained functions for shuffles and such we get uh three kind of uh layers of unconstrained calls. The division by three the raw sorting itself of just making sure that we have the output and then checking the shuffle. Um, and these will be three separate I mean they might be optimized together but generally these will be

three separate kind of foreign calls out to the unconstrained environment. Um, and the idea is that we can kind of like build up the the amount of information that we know about the intermediate values in our transformation and we can use that to to not have to duplicate some checks. So uh if we've already sorted the list, it becomes cheap to prove non-existence of elements doing the unconstrained search for the gap between where we would expect it to be in the sorted list. Um and there there's other cases like this, but generally if you have if you know properties like sorted largest values, all of these are valid, all of these already been have been checked with

certain things, often you can reuse those. And um you want to make sure that when you do another unconstrained call that you're not having to recheck too many of those details and if possible you can do those final checks later down the pipeline where they wouldn't have to be repeated. Um, and uh, I I'll talk a little bit more about this, but you do want to be a little bit careful with the overhead from doing unconstrained calls with with very large inputs or outputs, mainly uh, large arrays. Um, because implicitly when you return from an unconstrained call and you let's say you return like a 100 U8s, which I'll use an an example, we're checking that all those U8s like none of the values are larger than the

maximum U8 value. Um, so like if you're doing a couple unconstrained calls, you probably won't notice the difference, but if you're like iterating, as I'll as I'll show, that could become a larger overhead. So we can kind of think of the constrained and unconstrained as like a a partition of the call graph. So here I have the the black nodes are the constrained final checks and then the red nodes are the unconstrained. The idea is that if we find a large group of unconstrained calls that are all contiguous all each one calling the next uh we may be able to fuse those together and combine assumptions to reduce our overhead. So so let's give an example. I mentioned the array of U8s. I'm going to

compare a function that maps every value in the array to the value plus one and one that does and then I'm going to apply that a 100 times and then the second version we're just going to add 100 to every value in the array. Um uh it's easier to to mention now but because this function only has one input and this other function has two inputs. The second one takes one more op code. uh but they're they're virtually the same function. We're mostly going to be comparing how we can call them differently and constrain them differently. So I have three examples and I'm going to just kind of skim over them real quick to to illustrate the point. But in all three we have our 100

u8s as our input. Uh we take a create a mutable variable to store the output. Um in this case first we we we simply increment uh the array every value in the array 100 times and then assert that every value in the array is equal to 100. This is the most expensive approach and while it will ensure that every value is equal to 100 it takes over 10,000 is gates. Um now if we simply do our checks in the middle in between every increment and we make sure that each value is that value plus one uh the compiler is able to figure out the intermediate values or the intermediate constraints for these

unconstrained calls which before it it had to kind of group them all into one very unknown unconstrained call. Now it can be a bunch of less unknown unconstrained calls because we check in between each each unconstrained call. And with this it actually reduces um the amount of gates by over 2/3. With the additional information of these assertions we're able to get the number of assure gates down to 300 and uh the unconstrained op codes is the same at 255 sorry 225. Finally, if we simply do it in one pass, we save 100 gates by reducing the number of of range checks that we need

and we're able to get it down to just 200 gates. So definitely uh do experiment with adding uh constraints that are in between unconstrained calls to help the compiler tell more information in between those kind of uh blackbox calls to it. Um, but if you can fuse a large number of array calls, it it will likely be one of the fastips approaches. Uh, at least until we figure out how to automate more of this in the compiler.

Um, and then what about so so you've figured out an approach maybe you've you've found a subset of your your program that is a search or NP related or or something like that and now the performance isn't quite either you don't know if it could be as good as it could be or you already know that you want it to be better, right? Um, so how can you debug performance? Well, um, couple couple things that really help with unconstrained functions is looking for unnecessary duplications of arrays that could be passed back and forth between unconstrained and constrained. Uh, avoid duplicating assertions between the unconstrained and the constrained versions. So, for example, we don't need to check that the output is sorted or

double check the output is sorted on the unconstrained version. We we're already checking it on the constrained side. uh use the flame graph tool to look for key areas where unconstrained code could be used. This would either be large blocks or as I'll mention in a moment, it could also be kind of leafy areas at the at the tips of the flame graph. And we have this nor profiler tool to help with it. So, uh just a quick example of what it of the flame graph looks like. It's it looks like I think a lot of other flame graphs. Um you can either look at the executed op codes so the unconstrained side the at runtime or the compiled op codes uh these are also the unconstrained op codes because brillig

um and there's kind of two ways to try to figure out how to improve performance. You can either start at the base of the tree and say, "Oh, well, um, clearly SHA 256 var is taking up all the time." And then the second biggest thing is hash final block. Um, so then you can narrow in and say, "Okay, well, is there anything in hash block that I can fix?" But then the other thing which is harder to show in this example is these little tiny tips sticking out at the very top. Um while the flame I don't know of an easy way to just kind of flip the flame graph upside down. Uh what can happen is that many of these tips are repeated operations or even operations like add

or or multiply that are very simple but could be improved by unconstrained calls. So, it's worth looking at the the tips of the flame graph as well for any large chunks and seeing if it's repeated and seeing if those could be potentially batched or uh moved into unconstrained. All right. And then for unnecessary copies, um the tooling is still a bit work in progress for this, but there are a couple methods that you can use currently. Um look for unnecessary duplication of arrays. um are we're improving ownership semantics and and just a lot of features around this. So um I do expect this to to be easier as time goes on. Um currently we

have somewhat hidden ownership semantics and copy on write when in unconstrained mode. Uh and this is to try to make the kind of like the borrow checking side of um or or the lack of borrow checking to be as easy as possible. Um, but when you're trying to debug performance, all of a sudden our um somewhat conservative copy uh setup can can cause some issues potentially. So, um you can count array copies for your brilli program runs with this count array copies command. It'll tell you exactly how many array copies for that specific run uh to help you benchmark it a bit. Um, arrays have reference counts which are incre

incremented per reference to a variable containing an array and we're only copying the array if the reference count is bigger than one. So there are many cases where we can prevent the need for a copy. But it just kind of like with NP there are some very challenging uh cases where for us to ensure that uh everything is safe we might do an unnecessary copy and for a larger program where performance is more critical it can be worth uh looking at this more manually and more intentionally. Um, and like for the most amount of detail that I think makes sense for this, you can print what we call the monomorphized a of the code, it's an internal representation of the program that includes explicit clones which

increment reference counts but do not necessarily cause a copy. Um, because if the reference count over only ever reaches one, uh, we won't make a copy even if it says clone, which is a slight difference from what people might expect. Uh so in conclusion there are many ways to improve performance with unconstrained functions. We can do searches. We can take advantage of the inverses of a function or even kind of extend functions into invertible ones. Uh we can look at uh more abstract approaches like validating our input and connecting the input and output. Um, and overall this is somewhat similar to using a co a co-processor where you can

expect some overhead from moving to and from the co-processor. And you can also expect that some operations uh might be a bit faster or slower than you're used to in other languages. Uh look for cases where the amount of compute is much greater than the amount of data or at least uh noticeably larger because those are the cases where unconstrained functions can really shine. uh if the proportion of data is too large relative to how much compute uh it you might not actually gain a lot of benefit from using the unconstrained functions it it uh counterintuitively might be much faster to do everything constrained or a lot more of that constraint. Uh and then definitely remember that all essential crypto needs to be constrained. What do

I mean by essential crypto? Well, any final checks and any checks that are part of the crypto algorithms that you incorporate need to be constrained. So for a Merkel tree is one of the easiest examples. Uh looking up the path in the Merkel tree does not need to be constrained because um it doesn't guarantee whether or not somebody is in in the tree. Uh but that final checking all of the hashes that match the root that does need to be constrained because otherwise there are there's not really guarantees that um it'll be done correctly.

And with that, uh, thank you very much. And any questions? Anyone? Uh, I have some. If if no one has any questions, I'm happy to to kind of take a step back and ask. Uh, I I I have like a whole whole list. So, I'll start with one. So, um, since there maybe are some beginners who are listening, um, maybe on 2x speed as I like to do for my recordings, um, I'm wondering if, um, you could share a little bit, uh, for those new to ZK in simple terms, what is useful about unconstrained functions and why it's such a powerful concept for improving performance in ZK circuits.

Yeah, definitely. So, um, ZK circuits translate we're we're trying to translate general CPU instructions to the closest we can get to elliptic curve crypto so that we can use this cool property that uh, one person um, does the work and knows the data and the other person can check the work without running the comput the whole computation themselves or knowing the exact data that's being checked. Um, and the issue is there's a lot of overhead between that the normal CPUs and elliptic curves. So, and and some problems can't just simply can't be translated straightforwardly. So,

unconstrained functions do two things. One, they make it possible to run functions that otherwise we couldn't fit into elliptic curves and couldn't easily fit into circuits. And secondly, it allows us to offload a large often a large amount of the computation. Um, and it it it looks a lot like offloading things to GPUs. So, um, when you have a GPU, you can't necessarily run everything in the GPU, right? Like we can't run our our crypto in the unconstrained side. Um, they're super super fast. Uh, but there's some overhead from moving things back and forth. We basically by supporting unconstrained functions allow you to have like a co-processor or GPU style additional computation that can

run much faster in some circumstances and generally can make your your computation in your circuit a lot smaller and a lot cheaper. Awesome. And then kind of building off of that uh maybe at a high level you could talk about like some of the tradeoffs. So when you use unconstrained functions, how do you decide what parts of a circuit to make unconstrained especially when the overhead could maybe negate some performance or maybe introduce security issues? Yeah, definitely. So um generally I would say uh if if you have a large program there's likely a lot of room for unconstrained functions.

Uh but in in general you want to look for areas where um you're you're kind of grinding on the data so to speak. Any areas where you're doing a lot of compute. Um and then ideally it's checkable stuff like if if you're doing like a large amount of compute that is a little bit more like proof of work then that might not be able to be validated depending on the how the proof of work uh works. Um but uh really you likely want to put as much into unconstrained as possible um because of the performance benefits.

Now because of the the security like maybe you're unc under constrained maybe you haven't moved uh all of your constraints properly. Um this is a bit more of an advanced technique and yeah there's more room for error than just doing everything constrained. If if you're not entirely sure, I would recommend that you just do things constrained. Um, and and potentially, I mean, you don't necessarily need to understand this stuff super deeply to use a library that has already figured out the details. Makes sense. And may maybe you could share also um some potential like real world use cases where unconstrained

functions might make something that otherwise would be totally impractable uh feasible. It could be totally just made up and then um especially if you maybe have seen some in production applications yourself like Zika Passport or other places those would be great examples but it could just be totally theoretical too. Yeah. Um, I really like the example of just kind of connect like doing an Ethereum proof, like an Ethereum storage proof, because the the final check of an Ethereum storage proof is we're basically like checking some signatures, checking a a block validity thing real quick and then we need to check like some Merkel tree paths and then all of a sudden we know the balance of a user in

an application like way deep into Ethereum, right? So that the constraint side is is super straightforward. But how are we connecting to the Ethereum network? How are we figuring out where in the storage this application is? You know, how do we know that this user is connected to this application? Like all those other details um can be really complex and involve technologies that aren't even possible in zero knowledge circuits, right? Like it might require like talking to an Ethereum node or or anything like that, right?

Um, so that's the idea of like how unconstrained can be generally applied. And as far as uh production use, I'm not entirely sure about the exact usage for ZK Passport. I imagine that they might have some similar search or Merkel tree lookup type things. It's just like they're super super common. Um, but I can say for for us, one of the main reasons why Aztec uses unconstrained functions is because we really push the limits of performance for our our kernel circuits or our our core layer 2 circuitry that gets rolled up on or will be rolled up onto layer 1. Um, and I don't have exact stats, but yeah, I I don't think our Aztec would really be

possible without this. So when when you um debug and and do profiling, so um do you have any techniques or tools you recommend for like better profiling noir programs? Maybe I know like flame graphs can help to like identify like good candidates for unconstrained functions. Maybe if you have any thoughts on like how to find patterns there. Uh yeah, so mostly what I already mentioned with uh just either starting at the bottom of the flame graph or starting at the tips. Um it it gets really domain specific like there there aren't a lot of really broad approaches.

Um, however, I I would I would kind of go through the list of of things that I just gave in the talk, which is one look like review the list of NP problems and see if anything looks like an NP problem that you're already spending a large amount of time on. I mean, it doesn't matter if you found like threes or graph coloring and it's only 5% of the flame graph, right? So, you know, start with what are your performance constraints? How fast do you need it to be or like how much do you move the needle by getting it 5% faster? Um, and then you can kind of dig into the flame graph and say, okay, which of my algorithms like are big enough bins to dig in further. And then from there, it it it's super specific to

what you found. Like potentially you found just a search that you were doing by hand with some for loops. uh potentially you found an invertible function that you were doing the more expensive side of that invertible function. Uh but really like this is fairly advanced and and if you don't find something that's already been done and clearly has a benefit from the flame graph, it could require a bit of creativity. Yeah. Um maybe shifting gears a bit then I I'm curious like from your perspective as a compiler engineer uh what maybe some of the most interesting challenges were in implementing unconstrained functions. Um and were there any like surprising uh like things like hurdles

or different design decisions that you guys made? Yeah. Um I I guess I can say that um unconstrained functions look a lot like async functions. And so if I don't know if you've heard of like this I think it's a blog post but it's like what color is your function that async functions often behave so differently than non-async functions that you end up partitioning your entire codebase into two copies. Um and we were able to to to reduce this in in a couple different ways. Um, one is, uh, many functions can be automatically translated between unconstrained and constrained by the compiler. Um, and and that just works.

Uh, and we also assuming that your function might be moved between constrained and unconstrained, we add we have a um a standard library function that allows you to check which environment you're in and use that to um to change how you run the algorithm for performance. I did see a question in the chat. Oh, that was just me. I shared the uh the link to that. I had not seen it. Uh so I I was like looking at that really quickly. That's that's cool. So I I wanted to share it. But um maybe one followup, I guess, like what what are improvements that um you're excited about uh just maybe in general for Noir, but uh especially those around unconstrained functions now that we're

post like version 1.0. Are there any features that you think will make um it easier to use unconstrained functions, add more safety? Yeah, so um as we approach 1.0, I would say most of the features around unconstrained functions have already landed. Um I I mentioned the stuff around array copies. We are in the middle of trying to pin down some of those semantics and add tooling to to make it easier to deal with array copies and debugging their performance. Um but uh in general I think we're we're trying to improve any kind of tooling around unconstrained functions and uh there's some talk about potentially working on a tool that uh would help

know whether you're under constrained whether you've used the the boundaries properly. uh we do have an existing tool that will tell you if you completely forgot to constrain uh an unconstrained part of the circuit but it's not currently uh very aware of the semantics of the program. So if you have like if then it might it might mess up the analysis or make it a little bit less use not mess it up but make it a little bit less useful or informative. Um and in general we're we're really kind of working on testing and performance at the moment. So there's just a number of cases where unconstrained functions could have bugs that we haven't tested yet or because we haven't really pushed the performance of

some of the like the detailed components that are required for unconstrained functions like I mentioned uh we have range checks but in some cases we can actually um reduce the number of range checks because they're repeated. Um we're the more that we test the more we can work on optimizations the faster this will be. And and um how does how does Noir especially Noir's approach to unconstrained functions uh compare to what other ZK languages like say Cairo do for uh like complex computations. So what what like advantages does noir have in this model especially around like how

noir has implemented unconstrained functions? Yeah, for sure. So uh most languages don't support unconstrained functions much at all. Uh often what you might need to do is add extra parameters to your entire circuit, not just the single function, and accept those inputs that are effectively the the results of the unconstrained function. Then on top of that you'll need to find an implementation in another language that matches you know your feed field element computations your big lowerendian uh integer mathematics like all the specific details that you have in for example Cairo um and then you're effectively making this the

unconstrained side on top by using what uh we call on at noir like an oracle function or a function that calls completely out of the system. Um, so it is possible to to use similar techniques in other languages. Uh, because at the end of the day, all you're really doing is adding parameters to the whole circuit and running these other programs in a slightly different environment or not slightly different, but a rather different environment. Um, but as far as I'm aware, only Noir tightly integrates those two environments in a way that you already know that you have a second language that's that can be run quote unquote async that is faster than constrained circuits that all the Indianness and field element details

match between the two of them. Um, so you can you can kind of in some cases in the ideal case you can actually forget that you're moving in between constraint and unconstrained functions. They're both Rust functions and yeah, they have slightly different performance characteristics, but um by more tightly integrating it, it it allows development to be faster. It allows more more easy debugging like if you have a bug, do you know which environment if it's in if you don't have a environment containing the two? Um, and really I mean another question that we get is like oh unconstrained functions you're literally saying that you have part of the code that's not checked like it's not constrained. Um,

and so we do require like safety comments explicit annotations for where you're calling an unconstrained function. So you don't for example accidentally uh translate a constrained function to unconstrained with our like I mentioned we have the automatic translation or anything like that. You always explicitly know okay uh when have I entered this kind of sidecar asynchronous environment. So you you also you mentioned um rust similarities um you mentioned like the array stuff in especially unconstrained context maybe for developers coming from Rust what is like uh some key differences that they should understand about noir's like memory and ownership

uh model. Yeah. So uh as I mentioned before we have uh somewhat hidden copy on write semantics. So I would say that um it there there are very few similarities with Rust's ownership model and and borrow checker and um it's just different. It's just completely different. Um, other than that, uh, yeah, I would I would vaguely compare unconstrained functions to asynchronous ones, but ours are, I would say, a lot easier to use than futures and asynchronous and in Rust. Uh, because we can do the automatic translation and um, we just have a different different

set of constraints. Um, but how I generally think about using them outside of the API is is is quite similar. when you when you have like some some tight loop or something, you're not running every line of the tight loop in a new async call, right? You're trying to bundle up that whole batch of comp computation and you're trying to put it out into async. Like similarly, like if you if you have like you're just incrementing one like the integers in like a thousand values, like you don't want to you don't want to do that. um like every single call is is async like you want to try to bundle up as much as possible and be aware of how much data

you're translating back and forth across that async boundary. Um I mean similarly like you can't translate necessarily everything across the async boundary. uh like it has to be sended. Um and and we have similar constraints that um I believe a recent one was that we only have partial support for passing function pointers across that boundary. Um you also you can't pass uh mutable things across the boundary because we can't keep track of the mutations outside of the environment where where we could keep track of them. Uh so there are a couple of sim a couple similar restrictions. Um, but in some the the exact API looks pretty different and we

hope ours is easier to use, but how you kind of generally think about what parts of the program could be async or could be unconstrained, those are are somewhat similar. Yeah, that's helpful. I think just thinking through the mindset of how to approach that is helpful. Um, but uh yeah, yeah, I think those are most of the questions I had. Um and yeah, this was a great great talk, Michael. Thank you for joining us and sharing. Um the recording for all of you guys that are here live will be posted by end of day today. Um also on the YouTube uh we have like a YouTube playlist for Norhack where you can watch uh all of these.

Maybe some of you are watching this already on 2x speed as I will probably be doing later. Um so um thanks again Michael. Thanks everyone for joining and listening. Um and we'll we'll also share the slides uh later today as well. Thanks guys. See you. Thank you so much for having Thank you. Take care. Bye Michael. All right.