Noir Contract Syntax

Inside Aztec Labs · 22 Aug 2023 · 50:09

Aztec compiler developer Jake Fecher gives a concise overview of Noir contract syntax, showing code examples of contract structure, global state manipulation and the note interfaces used to manage cryptographic notes. He also introduces mapping constructs for private state. The talk is part of the Inside Aztec Labs series and highlights the language's user-friendly design.

Speakers

Topics

From Inside Aztec Labs, hosted by Aztec Network.

Transcript

Noir contract syntax now I anticipate this presentation to be very short it'll be rather like a whirlwind tour of all the different bits and parts of syntax Noir contracts could have potentially in the future and that is another point in these bits of syntax are all non-final so they are all subject to change as well I also wanted to thank Mike as well and a lot of these syntax and examples were taken from his initial nor three Noir Aztec 3 examples document so thank you to Mike and if anyone does have any questions as

well since this is such a short presentation just feel free to stop me at any time as well because otherwise I'll be going through these rather quickly all right um and before I get into the actual code examples themselves I'll mention that the general philosophy behind these initial syntax concepts are to make the user experience as easy as possible which generally means hiding much of the actual more complicated mechanisms like extracting

Rel extracting these specific public variables for example out of the Aztec 3 API those are sort of hidden under the hood and so the examples you will see will look a lot more like natural Noir code rather than having their logic obfuscated by specific Aztec 3 logic and pulling things out of the public ABI Etc all right with that let us jump in so here we start with a normal main function in Noir accompanied with double and square helper functions and this is normal Noir code that you would see today

if we wanted to wrap this in a contract we'd simply introduce this new contract keyword and put the functions that we want inside of the contract inside of these curly braces you'll also note that we removed the main function because contracts don't really have the same concept of a main function that a normal Noir program does in that respect all functions of a contract are considered to be callable so there is no single entry point like we would see with normal programs all right but right now our contract functions don't do anything specific to contracts so let's start

introducing some more logic one thing that contracts can do that non-contracts cannot do in Noir is that they can have these Global State variables so let's add one and let's call it a a will be secret and contrasted to a public Global variable and it's going to be this new utxo of note type what this means specifically is not incredibly important but let's note that for now that note is sort of an interface that provides various functions and we'll get into that later as well but these allow them to be

stored elsewhere essentially and shared as a global um yes I saw heard someone raise their hand I'm not uh what's the question sorry I think it's from laissez yeah I don't think anyone raised their hands but I have a question I just texted it over here so we're just you mentioned what callable is it this callable play everything or do we have similar to insulity where you have internet public private external uh like disabilities uh yes there will be visibilities and just none of them are shown here in this earlier example um so right now the visibilities are sort of this

public um which we could say is FN and whereas secret is sort of private um this is sort of an underexplored section though I'll admit um we're sort of still picking out what exactly these keywords should be for visibilities if we need all four that celebrity has as well but for now we have these public and then private to distinguish it from um this sort of like normal private visibility you have in programming languages which controls what you can import from modules to distinguish those two we call like the private contract we call those secret instead of private um but yes there will be visibilities

all right um so after we add our note variable we need to initialize it to give it a value and we can do that by adding this new Constructor construct and to give it an initial value we call the init function on the utxo obstruct and we initialize it with an initial note value which we can create by giving the note an initial value and the note also needs to know its owner we can access this owner through this self field which is somewhat like the implicit message variable that others may know from solidity whereas solidity you can access message.sender the same in Noir contract syntax could be

accessed through self.sender all right so now we have our shared Global and we have it initialized now we can actually use it let's change our Square function into a multiply function multiplying with our global and this only reads the value it does not write it so to read it we can call a.get which gets the note out of the utxo and then dot value gets the field out of that note and then we just multiply that field value with the x value we were passed in all right if everyone's following so far let's introduce how we would essentially mutate these globals

so let's create a new function add to a and here this is actually a secret function that we've made and we're going to be adding this new B value we were passed in and this um breaks things out more explicitly now we have this a DOT get which returns a note that we saw before and then the dot value which we also saw before returns a field we can calculate our new value just by adding them both together just a normal nor code and now we'll call this new note remove function to remove the old value of the note before we insert a new one in so essentially if to mutate this because

it's a utxo we'd remove the entire old value and then we'd insert an entire new one and going back to what I said before about all the Machinery that was hidden there is a lot of Machinery that is hidden here as well most notably getting this B parameter out of the public inputs and just keeping track of all this all these calls essentially it may be in the future that we want we will have a more General API that allows us to work with more than just Aztec 3 but for now this will be sort of adapted specifically for Aztec 3 and so this all

will be rather easy to use rather than focusing on flexibility all right in case no one saw that what we did was we changed the utxo to a utxo set and so now our secret a value will be distributed amongst several tree nodes essentially so now let's introduce another one if we wanted to subtract from a now and again a is not a utxo anymore it's this utxo set and because it's distributed among many nodes we need to introduce some sort of method to collect these nodes essentially and sum up their values

one way to do that is if we want to request two of them we need to define a function which takes sort of all of the notes and then reduces them to a concrete number in this case too in this case we have this local reduce function which is a new unconstrained function this is different from normal Noir functions being that it does not introduce constraints um which is what lets it do this non-deterministic logic of looping through potentially any amount of notes and returning a concrete amount of them we can pass this function directly into this a DOT get call which will return us

our array of two notes and we need to make sure to constrain these notes that are returned so we ensure that their owner is essentially ourselves and you'll note that we don't check the owner field of the notes directly we do it sort of indirectly through this owner underscore or function and that's because if these notes don't exist for example if for whatever reason there was only one note in the tree instead of two or more if these notes don't exist we'll have what are called dummy notes so they're not there but we need to return an array of two notes anyway so

the rest of the array will get filled with these dummy notes and in the case of the dummy notes this owner or function will return the parameter that it is given instead of the actual owner field all right after we've constrained then all the owners of the notes we can go and we can get the value out of the notes again accounting for the possibility of them being dummies if they're dummies we return zero we get zero for the value otherwise it gives us the actual value and then our new note value is just going to be the sum of these distributed notes and then minus the value that we actually wanted to subtract from our

total which is B and then finally we can insert this new value into our Global a now we're going to be working with some a little bit larger code next so what I've done is I've gone ahead and refactored this code into three different functions so if I go back right now and then forward again you will see that our actual subtract from any function is much smaller and I've broken I've promoted the reduce helper function into a full-fledged free function finally breaking out this constrained nose logic into its own function

so now for constrained notes we have a function which can take in any amount of notes is generic over this number Loops through all of them constrains all of their owners and then via the map function returns a new array holding all the values of each note and then looking back up to the subtract from a function we can see how we use these functions calling get with the reduce function getting the values via self.constrained notes and now note values as an array so to get the sum of the array we call Dot sum subtract it from B and insert our new value

so so far we have introduced these Global values we have introduced um these basic utxo utxo set structs and now we'll introduce a third type of this mapping construct and if anyone is confused about these with these utxo you take those sets or even really with these map mappings are um it isn't really the focus of this presentation to discuss them in detail but rather discuss how they will be used essentially so as long as you understand the syntax should be good all right now for these map constructs

when we want to initialize it and we can access it with these Square braces syntax to get a value out of these map here for our self.sender as the key we're initializing the value for that key to be the initial Supply field that we were passed in and again we have this self variable which is sort of like the implicit message variable in solidity which we can use to access the message sender now if we want to make a transfer function and we want to transfer some amounts to a given user here the user type is just a type Alias over another field which is just going to be an address for a user

we can access the relevant value of the map using the square brace syntax again of self.sender to get our current balance and again this gives us our utxo set from before and if we remember from before if we want to get a certain number of nodes we call the dot get method with the amount of nodes we want and our reduce function to filter through these nodes here I'm just reusing the same reduce function as before then we're using the same constrained notes function as before and we're summing up the values to get the sum of the two notes that we requested finally if we're transferring money we should make sure that we actually have

that amount in our balance so we're making sure that we have enough money to pay the given user by introducing this constraint here with that done we can create our New Balance by subtracting the amounts that we have to distribute and then decrease our balance by that amount by inserting it the new value in the map and then increase someone else's balance by introducing their New Balance into the map and then just note for this we're using the user we were passed in this two variable rather than our self.sender so in self that sender we'll have our decreased total and in the two we will

have the new amount that is increased by this difference all right finally here's just one more example of an unconstrained function this isn't really correlated to any of the previous examples but we can note that there are some sort of special functions that may be allowed within unconstrained functions that aren't allowed outside of them for example this get all function if we wanted to get all of the notes rather than just a concrete number of them this introduces non-determinism so that is the reason why we can't do it in a circuit which has to be sort of bound

over concrete value um but other than that this example is just as we've seen before we Loop over an array of notes assert on their owners and do some logic with the actual value of these notes all right so that is essentially what it would look like as a user using these and we'll move on now to what a library developer might need or might use so if you've come to this point you've already gone over almost all of what you would be using day to day and this rest of

this presentation is going to be talking about this note trait what it is how Library developers might use it implement it and what it might be used for essentially um yeah users won't use these functions directly if we remember our previous examples we weren't really using any of these functions but we were using Global variables with notes in them if that makes sense so this is an important type but it's not one that users would have to always Implement directly all right so as I mentioned before note is an interface and it has these General functions in them along with several

more that I'm emitting just to save space in this PowerPoint so is dummy we talked about before if we try to dot get with some number of notes and there aren't actually that many notes for example the remaining number of notes will be dummies so we'll have to have something in the interface to actually check if these notes are dummies along with Computing the note hashes and nullifiers Etc with some other functions however there's a key wrench in our plans as of today at least Noir does not actually have traits so we can't encode this directly what we can do however is a workaround where we

can represent this trait as a struct instead now all the functions that we want to include in this interface are now fields of destruct and each field has a function type because they're all functions that we wanted to require you'll note that we also had to explicitly add this self-type variable for the first parameter of each function so our struct is also a generic one all right now we have this struct and this will be defined within noir's standard Library so users would not need to define the structure manually

users would however may possibly have their own custom note type here we're saying that a user has their custom note type and they're calling it an Aztec connect value note and this struct that they've defined has a value it has an owner and an is dummy field and possibly some others as well and they want to Implement essentially note for this new note type that they've created how would they go about doing that well they have an impul for their struct already which has some methods on it they have a way to construct a new note they have a way to compute the hash and

they have a way to compute the nullifier they're pretty much all there and their question now is essentially how do we signal that we implement this note interface Wilson's note is a struct we would the first thing we would do is add this struct as a field to our Aztec connect value note at the top there so you'll note we now have a fourth field called note and it has that actual note struct in there containing all the functions since we have an extra field we'll need to initialize that field in the new function as well so you'll note that we manually create that an instance of that note struct is dummy we initialize to a Lambda function with one parameter that I

called this and it just returns that this dot is dummy field compute notehash well we already had that function it's self colon colon compute note hash and compute nullifier same deal there so we just reference that method and we are good to go and we've initialized our note and then finally we just initialize self with that note value and that's how you essentially Implement note for a given custom note type I'll emphasize again that most users won't need to do this but if the user is particularly Savvy they might have a use for doing this um we will have a standard note type in the Noir

standard Library so for most users they could just import that one and wouldn't have to go through any of these steps all right there's one more um process to this though and it's actually using this note we've created the methods and we've initialized them we've included note in our Aztec connect value note struct but how do we actually use it in a normal function here is a function that you might write in Rust and it's using rust's trait syntax with the N colon note you see in yellow at this at the top but if we remember from before Noir does

not have these actual traits so how do we translate this to use the structs instead well we add the struct essentially as another parameter a note of n and if we remember before a note was generic over the actual parameter type which was the note type so my note now is just this is just of Type n or it was before as well but um and so n is the actual note type whereas note of n can be thought of as the methods for note type if you're used to C plus plus you could think of this as sort of passing in the v table for the struct separately if that makes sense

so methods now contains our structive note methods so to access any method in there it's the same as accessing any other struct field you can access the isdummy function via methods dot is dummy and it takes one parameter which is my note same for compute node hash same for compute no nullifier I'm going to go back and forward again just to show you the transformation between these two again so here's the first example with trait syntax and you'll note when calling the methods we do my note dot compute node hash for example and once we add the explicit struct parameter

it changes to methods dot compute node hash and then we pass in my note there in between the parentheses rather than where the dot is foreign ly when actually using the note and the bottom you'll notice before the only difference was this function it took one parameter we had balance oh and we created the Aztec connect value note a new version of that and that's just the same struct as before so we pass in the one balance parameter and then we just have to manually pass in the balance.note field as well for this note value and this has been sort of an aside because Noir does not have traits today just to show how it is still possible

but requires perhaps a bit more manually threading through these variables in the future one day Noir may get traits and we may smooth this over but for now as an initial prototype this is how it would be done at least and that is contract syntax in Noir does anyone have any questions uh Joss I was wondering about the choice of the keyword self on the contract functions

uh I mean I'm I usually understand self is referring to say a method on a struct but here it seems to refer to the transaction context yes that's correct so originally um we had originally we had copied this message variable from solidity entirely whereas there's this implicit MSG parameter to sort of every contract function we thought that might be confusing especially since Noir is sort of adapted from rust syntax where rust made this explicit into self and we wanted to sort

of do the same or I wanted to at least um do the same transition for message to instead of have it be implicit to make it more appearance where it's coming from essentially so if you think of like the entire contract as sort of like an impulse on like a message then self is like the message itself um but yeah I don't know that was just my decision I just thought it was a bit more clear than having it be implicit and we already have self as a reserved word so I thought it worked out but again all this is non-vinyl so if anyone finds that particularly confusing we can

always change it just quickly chumming in yeah I think if we get people from viva especially they will find it very confusing in Viper you have self and message being like keywords where itself is the contract itself and yeah messages the message context yeah that's a good point to consider thank you last night does that mean that in so how do you reference a state variable in Viper and give yourself the yeah so what's the point of it being global uh a thing you're just using yourself

whenever it's like a storage and then if it's not don't have a self it will either be uh what it would just be like yeah memory or stack very clear where you're working right hmm absolutely you don't like just by the name you or like how you reference it might not know if it's storage or if it's stack variable or whatever it is so can you do still do message dot sender and the messages I see um okay yeah sort of makes sense I guess

one thing to also bring up is the fact that secret is being used um instead of something like private uh this is mainly because there's actually different you can have a private function which is just not visible outside of the contract uh then you can have a secret function which is a function which is executed by the private kernel circuit so it's there's different Notions of private and you can have a public function which is callable from outside of the contract then you can have a well we don't have a word for it but you can have a public function which is callable by the uh so it's not callable by the private kernel circuit so it operates on public State uh I

don't think we have any we haven't thought about what we say for a public yet but this is the reason why you don't just see the word private um before we get to Joss Tom I saw you raising your hand earlier as well do you still have a question uh I was basically saying the same thing as Lassie was about uh Viper and the self keyword um I think it'd be very nice for the state variables to be kind of read through a similar kind of mechanism just to separate them uh from kind of

memory uh variables within a function um it avoids a bunch of issues that have kind of happened in solicity development where there's definitely been some bugs in kind of reasonable size projects where someone is accidentally re reading from Storage a stale value where there's kind of um got a similarly named variable in in memory um which they meant to read from but they actually read an old variable and that breaks the maths and causes a host of issues

but if we're using self for the transaction context then yeah things get a bit messy I do wonder if that is somewhat mitigated by us having these explicit methods on these contract variables where to extract the actual field value out of a utxo of a note you have to do I believe it was dot get to get the node out and then dot value to get the actual field out of the note yeah it's a bit less of a kind of priority for that reason um it's although we

yeah yeah it's less of a worry but it's so nice to have it's definitely something to keep in mind I'd say preventing bugs like this um that you might see as common and solidity for example preventing bugs is probably the number one priority in designing this new syntax and API so the most we can keep that to a minimum uh the better I think um Joss so I've got the same question the last I put in the chat earlier during the transfer function how come you didn't need to explicitly remove the note that was being spent

yeah let me go back to the transfer function so um I believe the answer may be because this is a utxo set rather than a um just a utxo but I could be wrong on that um I might have to defer to Mike for that one because I just took this example from the document there so I'm not exactly sure on the precise reasoning yeah I think we've got back and forth on

this um like in terms of it myself today but uh it may be better to explicitly do it I think there's a few versions of this uh but as as Jake said at the start this is kind of fairy draft syntax uh the goal is just to introduce the uh the concepts so people think it's better to explicitly pull remove but probably trying to lose up I'd vote make it explicit because I think there's a use case for building a collection of states and being able to reuse them and then only like destroying that state later through some other method it could be also that we recognize that one case is the common

case of inserting new value and removing the old one and so we provide say a method to do that while also keeping the methods to insert without remove and remove explicitly um but yeah all of this is subject to change David along the same um train of thought like can't you you can insert a note without removing right so I'm wondering like like you don't necessarily need to be creating a nullifier every single time you're inserting um a node right so like I'm a little confused like what determines like I am

creating another fire this time versus I'm not a good question my understanding here is that if you are constraining anything so if you're reading anything from the UTS are set under the hood a nullify needs to be created anyway um uh because it was your leak information um so just depending on uh the version of this syntax maybe in this version uh it's assuming that every everything that is read from YouTube so he said it's not a fight and then you're only inserting what's back as a mental model um versus kind of the the other one which I think Joseph's hinting out where explicitly need to do that yourself but if we go back a few examples to when we

were just adding to the balance um uh and we weren't actually mutating uh okay yeah this one I'd say sorry uh that this one yeah uh so here under the hood a if the lead still gets nullified um in some versions so it's up in the air um is what I'm trying to say [Music] okay if I don't know if you have anything to add um yeah speaking with Mike I think he said all reads will uh create a nullifier so as soon as you read it nullifies and if you basically don't want to modify the value you just write you just write back to

storage the same value so each read is accompanied by a right could you guys go into I know you said it would Bleak information otherwise could you guys maybe expand a little bit on that like why why do you need to uh nullify on on only a read I think there's two reasons one is um when you're reading you need to be sure um that you're reading the latest value of uh the state the only way to do that is to have nullified it after every read so let's imagine I I have a million dollars um in a note from like very early on in

the protocol's life um I could create valid Noir proofs um by just kind of reproving um some hash path uh saying that I have a and it's still worth a million dollars with the actual State the chain would be much further along um so you need to nullify after every week to ensure that the kind of current value of a like my giant note in this case is is up to date um and I think the simplification probably came from that but if you have to do it on reads then um maybe it may both sense to kind of um only insert on your writing to read you need to check not nullified and so you reveal the nullifier I think

to the to the server to do that which would leak information if you didn't they actually then nullify it and recreate it the thing that's my understanding yeah that makes sense that makes sense to me thank you guys Santiago this is [Music] how much does a charity go with rust compiler how much do they could be used for so to speak written from scratch um you were going a bit quiet there um I

heard you ask does the Noir compiler share any code with the rust compiler um yeah was that your only question or did you have any others as well okay so does the Noir compiler share any code with the rest compiler no the Noir compiler is completely our open source um project and you can find it in Noire Lang slash Noir on GitHub and yeah we do use um some Rust libraries but we do not use any parts of the rust compiler itself uh I guess the asterisks is that I did

copy from rust's analyzer which is the linter the module uh the way they did a module system we've got some design inspiration but we haven't used the QR compiler itself um yeah I I've seen the code in question though it's um yeah it's nothing too major and it's just part of the sort of module and name resolution system all right if that is all the questions

then thank you everyone for coming um I hope you got something out of this presentation as non-final and subject to change as the syntax is it's always nice to be able to get more eyes on it and more opinions on it so that the whatever is chosen for the final syntax can be all that much better oh I had one other question um just I know we've talked about this a lot with different people with Mike but um kind of utxos versus notes

um like in in the single case where you have the utex of type of trait note um why isn't the why aren't those wasn't the name of the thing that's a great YouTuber I guess it feels like we have an additional wrapper on something like not additional um kind of participants having no and utxo as two different words have in the language here all right so your question is why we have these two layers or you could say three layers of an onion instead of combining utxos and notes and combining

you takes those sets and notes uh yeah mainly for detection no no case um I think the answer is just for reusability um it could be that we'd had like a we could provide like a type Alias for a utxo of note um but for if we're implementing notes we only want to implement note once and so having one like struct for a note makes sense um if we were to combine that with or I'm sorry and then having several different wrapper types that contain notes internally um rather than them all re-implementing

the note logic is convenient a suggestion note to utxo yeah that's what I was yeah I Believe In the initial um proposal that Mike gave as well that this note was separate from a utxo um it does not itself have the utxo um not sure what to call it but the API of where there's one distributed thing and then can be removed and then replaced with another um it's rather just an internal interface for something that provides

these like hashing methods and has a nullifier Etc so it's a bit more General than what you take so oh I think I get it you mean you mean utxo has a method get whereas like a note doesn't have that method it has the the interface of the mode um yes and but yes it may be that in the future we want to smooth over these differences and we could definitely look into exploring other ways of organizing these renaming existing types yeah I would ideally like to change it

but no good name has come up um yeah if anyone has any really good ideas for names that are very readable and are immediately apparent what they are especially to someone with less experience in this field then that would be great Raul um potentially a dumb idea again just joined but I think we should also think about not using the word utx shows Etc on the syntax only because it's confusing to people who aren't familiar with these kinds of things like most of our developers will probably come from the blocks like ethereum or who don't know what utxos are

and shouldn't really have to know if they're just using Noir mm-hmm um yeah yeah I agree with David I'm happy to like brainstorm as I onboard into these things though um how about this then whenever we do revisit these names um because these aren't actually implemented in nor yet they're not even in the standard Library whenever we do want to sort of think of these names I'm going to put

a note about them in the Noir Lang channel on Slack and anyone with an opinion can sort of look at that thread and put their own comments on it of you know I think this would be clearer perhaps we should consider this instead um perhaps that might be a good way to get everyone's um opinions in um I'll note actually as well on to my thinking of it on I don't actually so I'm I'm very much in the case for usability for users and for new users especially however I'm also of the opinion that

sometimes names can be too simplified as to they're trying to sound too General too friendly and it makes them more confusing as a result for example um if we name it something like I don't know uh Leaf it's it's could be very friendly but it also comes with like connotations that utxos might not have whereas if you go instead the other direction for this more confusing but precise name like a utxo um there's no other existing like Concepts in someone's mind that it could be

confused with um you're essentially forcing them to start from scratch and like actually learn what it is um I'm not saying like this is the approach we have to take but it's something to consider if we don't think of any um other more applicable names Savio um I do want to quickly bring up that it might be worthy to do the discussion on Discord this course instead of um slack where things might get lost so yeah living in um tying into the presentation last week we might want to use this course to just um

discuss the longer term discussions that we should be having all right um so it looks like there's been some comments as well in the meantime would it be possible for someone to write a library that completely abstracts the concept of utxo note away from the user or no it is fundamental to the way developers need to reason about quote-unquote secret state um so I suppose it depends on what you mean by completely abstracting away these Concepts

because no matter how you abstract them away you'll still have to provide methods to interact with it somehow and even as soon as you provide like a method to like just get a value um you're already like one step closer to recreating the existing abstraction where we already sort of have this getting a Value method but that's also sort of getting into the weeds because the real answer is that um yes we do want to explicitly allow users to do this and create their own wrappers and that's part of the reason why um I sort of went into the whole offshoot at the end about notes being a

sort of interface that you can think of and provide your own values of like these functions for because we could have just used our own concrete functions for those but in making it an actual struct our plan was for really to users to create their own notes um to do the things that they might specifically need to do which makes it allows them to create their own wrappers for these um and they could theoretically do the same for utxo and friends as well um yeah um I was just going to add that um it depends who you need the user here David do you mean like a nor developer or an Aztec 3 contract developer um I think yeah kind of what what uh

what Jake said but the goal of aspect.js is to obstruct that beliefs so uh if you're kind of using a ZK the rc20 contract you don't need thinking txos um you can think in terms of like balance plus transfer and so I suggests and the kernel circuit kind of achievement thanks guys that makes sense I'll also note that an eventual goal of Noir is to be um sort of agnostic to these Aztec 3 like distributed contract

systems um so in the future it might be that we get even more flexible tools to allow users to program with any similar sort of contract system and once we get those even more flexible tools then I would very much like to say yes users would be able to implement their own versions of these structs and would be even able to implement them for different chains trees Etc but that's just in the future all right

um sounds like that was all the questions so again thank you for coming everyone um hope you got something out of this I know I did it's always good to have more discussion on language design I think the more Minds the better