The Kernel Circuit
Inside Aztec Labs · 22 Aug 2023 · 39:09
Aztec team lead David Banks gives a deep dive on the private kernel circuit, the intermediary layer between user smart contract code and the proving system that keeps contract execution private and composable. The talk follows execution of a Noir function through the kernel, covers recursive proving and what happens after the private kernel, and ends with Q&A. It is part of the Inside Aztec Labs series.
Speakers
Topics
From Inside Aztec Labs, hosted by Aztec Network.
Transcript
cool um so this is kind of a perfect tee up from uh Max team and gam to uh start talking about the kernel circuit um and the reason is because um Noir function execution kind of directly plays into the kernel um its outputs go directly to the uh the kernel and so um I'll in one slide just talk about execution of an R function which we just got a whole presentation on and then we'll talk about the kernel uh what is it why is it when kernel um and then uh dive into the into like the details of it and uh specifically um go through uh the private kernel
circuit um the context of contract deployment um and then this will tie into suyasha's talk on roll-up circuits next week um so um executing an or function quick review um so essentially uh you have this Noir code it compiles down to um acir op codes and you execute each op code when a nested function gets called you need to jump to that function uh when an acid contract function is called either jumps that function and run its full execution um when you reach an OP code that needs some data from like our world State like private data from the tree for example um you're going to make a call to an
oracle for injection of that intermediate data um and then ultimately you end up generating a proof of execution of of that you know our function uh which will be sent to the kernel circuit and you also finalize some other outputs um that'll be assigned to the kernel circuit so um the actual kernel circuit let's dive in uh what is a kernel um it's corn not really um but anyway uh so it's essentially in general software terms a kernel is a software layer between user code and the CPU and Hardware so um it's a piece of code that essentially
enforces execution rules and chooses which apps can actually run on the CPU um it manages resource access and allows cross app communication and it honestly does a whole lot more but I'm trying to draw parallels here between a kernel and the kernel circuit NASDAQ 3 which is a circuit layer between user code user code which is Noir contracts and the Aztec 3 roll up um it enforces code deployment and execution rules and manages access to uh data and from our trees and other functions um within a contract it also uh maintains some privacy of
information which is uh kind of related to kind of along lines of resource access um okay so but why do we need a private kernel circuit um well number one thing is for privacy but Noir kind of already gives us that um which is mostly true but we actually want privacy of the function that the user called in the first place and there are some other privacy concerns that we'll come back to in the next slide but for now another thing that we need the kernel circuit for is composability um functions should be able to call others and even those in other contracts so what that means is that when a Noir when Noir is used in the context of smart contracts every contract function
is going to be its own ZK circuit um that can generate its own proofs so that presents a problem because that means that one transaction can actually result in multiple proofs so if if a user calls function a in a smart contract and that calls B which calls C uh and they could be in the same contract or different ones each function call has its own circuit and therefore we'll be generating its own proofs um so the problem is who combines them and how and you might think well the sequencer could just combine them but then that brings us back to the Privacy issue um so what did function a calls B and passes
some sensitive information to be well the sensitive information shared between A and B should not be shared with the sequencer so in this example on the right there's some secret um and it's passed to function a and function a then calls B and passes that same secret B actually even returns a new secret and so there's these pieces of information that are sensitive that link A and B um and so if the sequencer was to handle this um then they'd have to have some way of knowing that A and B Link in this way were like the secret is that critical information that links the two um and so that is kind of one of the main challenges that leads us to needing
a private kernel circuit um so the kernel solves this um and and it does so basically the Crux of it is that the user does this stitching together functions not the sequencer the user runs this private kernel and generates the proofs it generates the private kernel proof um it preserves the privacy of which function a user called uh and it preserves the privacy of the secret information that links those two functions um and just kind of a note here does so by basically taking public inputs of the um the function circuits like the individual Noir functions and turning them into private inputs um to
the kernel uh regarding composability every private function circuit and every contact every contract in aspect three follows the rules of the private kernel which lets us verify and Stitch together even cross-contract function calls and lastly a user only submits a single proof um basically the private kernel stitches together the nested function calls and the proofs generated by those nested function calls um and they're all stitched together uh they're verified by the private kernel and then a single proof that represents the full execution of that call stack is submitted to the transaction pool and
ultimately picked up by sequencer so I'll do like kind of a high level recap before I move on before we like really dive into the weeds um which is that user generates the private kernel proof in their browser it preserves privacy of the user sending the actual transaction request um it preserves privacy of the function arguments and return values even when those are between nested function calls uh it preserves privacy of State reads uh so anytime you need to go and read a private data Note it actually will be able to preserve privacy of which uh commitment was read from the tree even if that commitment itself can't be um can't be it means nothing to somebody
it still even preserves like which note you grab from a tree and it preserves the privacy of the function itself there's a caveat here which is basically just that there will be a way to expose private information uh by basically passing it as arguments to a public function um but we have plans to try and make it that really obvious that you're doing that uh from a development perspective so that people don't do it by mistake um but it is that we can do that for um for developing meaningful applications and the last part again the user submits a single proof of the kernel circuit to uh the transaction pool can I ask like a quick uh orienting question sure so is this like a a good
example of how the kernel circuit would work is you have a private function f you call it with some inputs that can be private it produces an output called out and then you want to plug out into another private function in a different contract uh where the input is like in and the kernel circuit would demonstrate that out equals in yep okay so it's kind of orchestrating like cross contract execution is there anything more to it so far um that's mainly it um is there anything more to basically the private kernel you're saying like in general I'm just asking like so that's a good mental model to have in mind like
going for the rest of the talk yes that is a good mental model it it gives us this composability and it it does so and and basically by um proving that functions um link to each other in a certain way and proving that they all follow some standard rules such that they can fit into this one private kernel circuit awesome thank you um okay so for each function in the call stack um the private kernel proves the following it proves that the signed transaction request so that so the actual request that the user signed uh matches the first call in the stack um it proves that the function is in the function tree and that the contract is
in the contracts tree um basically just saying yes this function of this contract does actually exist on Aztec um and it proves that commitment's referenced by um the function call are actually present in the data tree um it then collects any new commitments nullifiers contracts or messages that are created by this function um and that it verifies the previous kernel proof so like it's this is recursive which will kind of dig into next but it verifies any like basically execution of the previous um version of itself and then says also
yes the current function being processed um that proof is valid um so all right before I guess before I move on does anybody have questions before we dig I'm going to dive into like kind of the um some of the data structures and then we're going to go through examples so I I wonder how you prove that the the next function is the correct one like if you call if your contract you should call the function a which is called function B how do you pull that you really you you call it function B that's that's a great question and I actually think that the next slide
hopefully is it this way hopefully I think maybe not this slide but the next one or like one of these next few slides of the data structures might answer that so don't let me forget um we need to address basically how we prove that um function a called function B and we actually like followed that sequence properly in the kernel um so here's some of the inputs and hopefully we'll get to uh hopefully this will help answer that so the private Kernel's inputs include the signed transaction requests um the previous kernel data uh the Kernel's recursive so this is data essentially accumulated uh from previous iterations as well as an actual proof um that the of the previous iterations
and then we have this private call data which is the data relevant to the function call currently being processed by the kernel um and so you can kind of see the data structures here on the bottom although that's not so if you guys want to dig further into this I recommend you go and take a look at Mike's diagrams um so the previous kernel data um is essentially the outputs generated by the previous iteration and it includes um it's a bunch of outputs from the previous iteration as well as the proof of the previous iteration and the verification key um now this is this is all empty we're mocked for the first call of the kernel
because there was no previous iteration and so now hopefully this should help answer that question which is the private call data which essentially is the information that is input to the kernel for the current function that this iteration of the kernel is processing um it contains the current call stack item which is things like the contract address the data about the um function uh and the outputs of the function circuit um and it contains the private call stack for this function so that is a piece of information again this is information that is actually coming um from the uh the actual outcome
basically coming as an output of the app circuit so the function circuit itself that was executed previously we've got a proof and we got some outputs of that like no R function one of the things that it will need to Output is essentially uh what's my call stack so like which functions what is the current state of the call stack meaning what functions need to be executed um as a prerequisite for this current function being um being valid and so that's kind of how we make sure that like if function a calls function B calls function C that that call stack is respected is that each individual uh app circuit each individual function proof
will come with additional information which is uh what's the context of its call in respect to like other functions that need to be proved alongside it so does that kind of answer the question again yes thank you cool um so again before I move on this includes the proof from the function circuit and also the verification key which the private kernel will use to make sure that um the actual proof coming from the function is valid Okay so this is really just a diagram of all that because I just had a lot of words without very much
um without very much visual visualization so this kind of shows the recursive nature where you have the same this private kernel circuit it's the same thing in each of these boxes it takes um a private the private circuits are on the right these represent you know our function calls and and the proofs generated by them it takes that in as well as in each iteration it takes the proof from the previous kernel um circuit and so this kind of just shows that each uh yeah yeah so um so in the first iteration it would verify the sign transaction request and not the previous proof because there is no previous proof is kind of like it
that that it works because yeah this is the same it has the proof as an input but I assume that we are going to pass some random data in the first iteration yeah I think there will essentially be a mock proof um for the first iteration that will basically just be true and there'll be some like we'll be able to detect yes this is the first iteration and therefore um a mock proof is fine and we just and then there's the additional check for that first um iteration to make sure that these this uh ecdsa signature matches the actual function that's currently being validated um there's also been discussion of actually
as a later optimization having really three different versions of the kernel the um the kind of the first one would be its own circuit um that handles AC DSA signatures and doesn't verify a previous proof and then there's kind of this um intermediate one or like the recursive one that keeps calling itself and then a final one and the reason for that I think is I think the signature verification is actually expensive and it's also expensive or at least somewhat wasteful to have that additional like mock verification um when we could have that optimization okay okay thanks okay I am going to jump into an example with
the call stack I think a lot of you guys have probably seen um some slides that Mike has done in the past on this so I have a diagram on the next slide hopefully this won't be too um too overwhelming um so basically function a let's say function a calls B and C um so function a will have its own call stack which is basically B and C that is inputs to the the first kernel um and that first kernel we'll call kernel 0 is which is processing function a uh we'll first verify the proof from function a and then it'll take that B and C that call stack from a and it will put into this um put it into
this uh call stack that essentially the kernel is going to keep track of um a running call stack that the kernel will keep track of which will then also be used by the next iteration or recursion of the kernel so the call stack here ends at B and C it generates the kernel proof for this iteration and it hands off that information along with a bunch of other um information to the next iteration and I see that gam raises hand so I'll pause yeah thank you so I wonder how you can generate a post on function a if your function a is cutting B and C and you you don't have you have not processed it BNC yet so I guess you need the result of Beyond
C to to get to the execution of a and therefore the proof of a yeah so that so I'll do my best to explain this and I might tag somebody in to help me um but I think the idea is that when you get to the point where you're actually feeding the kernel um all of the functions and their individual proofs of execution with all of their inputs have already been completed um right they've already gone through everything that we talked about in the last presentation and their inputs have already been computed and some of those inputs um or I guess some of their outputs I guess we mix up those two terms sometimes in the in the ZK World it seems um some of those some of their the
outputs of those proofs will be the actual call steps so alongside function A's um proof will actually get its call stack as an output so you can prove that function a executed correctly get this call stack and then you hand that into kernel into the kernel now what it's responsible for doing um is at each stage each time you go from like one kernel to the next so you see here on the left there's no verification that the kernel stack necessarily links to the next one but now in kernel one on the right when we're processing function B it's going to pop an item off the kernel call Sac which is as comes from the previous
iteration and we're going to see that it's B and we're going to make sure that it actually matches the prep function that's currently being processed by the kernel so it's the idea that we're essentially staging a bunch of information for all three of these kernel iterations before we actually um input before we actually run them and some of that information includes the call stack for each iteration and we make sure that essentially the the call stack that's dynamically maintained throughout execution of this like iterative kernel matches the original call Stacks coming from each individual function does that make sense yes thank you sure
um so kernel one processes B I make sure that like I just said the item that comes off of the call stack from that we got from the previous iteration uh matches B verifies the proof for b um which again comes from the no R function and um there's nothing to add to the call stack because B doesn't call any functions so in the information we get from uh from processing B before this there was no call stack so we don't add anything there so now the call spec becomes C and we generate the kernel proof for this iteration and then we do the same thing with c um and at the end of C there's no there's nothing left in the kernel call stack so uh the proof that we generate for
this last kernel is actually the final kernel proof and that's what ends up being submitted to the transaction pool and ultimately picked up by sequencers uh and so this is just a little visualization that Mike has used in previous presentations and so here on the right we have function one calls function uh two one and two two and where these are different contracts and I wouldn't worry too much about the public versus private for now but basically you can see um that we have this call stack that kind of gets um uh it it keeps changing throughout based on whether or not the function currently being processed um actually made any nested calls and uh
it's it's this call stack keeps reducing in size as we um process a function but you uh each time a function gets encountered that has an existing call stack well we have to then populate the call stack with that information and we keep going until there's nothing left to process okay uh I guess any questions before we move on to the kind of the contract deployment uh example cool so um in the context of basic contract deployment which is what we're doing for Milestone 1.1
um we basically want to do the following we want to ensure that the verification key hash in this so a transaction request is going to have a verification key hash we want to make sure that it matches the VK of this of the Constructor right so every let me take a step back every contract is going to have a Constructor um which is a function a Noir function that will be called when a contract is deployed um and it can do a handful of different things it can set up the function uh set up the contract maybe set some State I think eventually we might have it be able to call other functions even but either way we're just saying this is a special function but it does have a VK it does need to be proved
so we want to make sure that the transaction request actually has that VK for the Constructor um we generate a contract address and it's nullifier so the contract address comes from things like the developer address the function tree root some information about the Constructor and then also assault where the salt is just extra information that maybe lets you have a little bit of control over what your contract address looks like um next you have the this contract address nullifier which is probably just a hash of the contract address that gets thrown into the nullifier tree at some later stage not as part of the kernel um just to make sure that nobody can reuse that same contract address
and then uh we generate some new contract data uh which ultimately will end up uh being the information we we put into the contract tree so that'll be things like contract address the portal contract address and then the function tree root uh which essentially represents all of the functions in the contract and it's again at some later stage like in the roll-up uh circuit stage that's when that information will actually be inserted um into the contracts tree and um lastly we we verify the Constructor function proof uh against its verification key um and sorry maybe that bullet doesn't belong under under this one but we then
um I guess it kind of does yeah so we use ZK stock recursion uh which makes it possible to do I kind of should have mentioned this earlier uh makes it possible to have these like proofs that are verified by proofs um and then of course just like before we generate one proof for this so this this slide take a big step back is what the private kernel does in the context of basic contract deployment so at the very end of this um a kernel proof is generated that represents the contract deployment this the Constructor call for this uh for this new contract um so the last things I wanted to mention is some things that the private
kernel doesn't do which is it does it doesn't execute the function circuits themselves this is done prior which is kind of the whole presentation we just had it does not perform tree insertions um for you know data commitments nullifiers contract leaves it that's all done later and it does not merge separate transaction it only merges uh it only essentially stitches together nested function calls for the same transaction and that that's mainly it and then I pretty much just have this one slide that's kind of teeing it up for siosh next week um basically um Aztec JS what happens after this Aztec JS Returns the final kernel proof
back to the user represents the whole transaction um and they basically can then choose do I want to send this out to the transaction pool if they do um the ARs which is the Aztec RPC server will send it to the transaction pool the user will get a transaction receipt um the sequencer will collect private kernel proofs from the transaction pool um it'll execute any public transaction requests or at least uh generate or delegate their um their the public kernel proof which we haven't talked about um to uh to approvers uh it'll order a bunch of the kernel proofs and then it'll uh use that ordering especially
for things like tree insertions which may where ordering may be important um to generate a bunch of inputs to the base roll-up circuits which are the next thing that suyash will be talking about next week and then um it'll submit the kernel proofs to those provers uh which will merge them incrementally to one single you know final um route that we end up submitting as our roll up to ethereum and that is it so yeah um love to discuss if you guys have questions I have one question hey about the roots
the of the trees that are used inside the functions are those going to be fixed across old kernel recursion like constant every every time you refer to the same old nullifier tree root for example um okay I went to a slide that's actually not relevant but anyway so you're saying basically will the root of a tree for example like a nullifier tree end up being fixed so are you saying fixed between the duration to the next um I'm actually yes oh sorry yeah so
initially it will be yeah um it makes the kernel circuit have fewer constraints if it just if you recurs through it 300 times I mean that's a terrible example 20 times um yeah we'll just use the same we'll refer to the same commitment tree root same enough all the way through um maybe in future we realize we should generalize it but for now that's good enough yeah it just simplifies the code yeah so like when we are generating all the all the weaknesses all the proofs for the private functions we must fix and one function one nullifier three root for example and use always that for fetching from the Oracle so we can have this only one fixed route when when
proving with the Canon circuit right yes so it is an important Point yeah if you if you begin executing a function in the atvm we probably want to pause automatic updates of our database so we don't accidentally you know overwrite the route that we're referring to or something like that so yeah it's a good question something to be aware of with this keeping just one of them thank you yeah yeah so I I brought up here in the so anybody I'm sure you guys have all been looking at this but this diagram is super helpful thanks Mike um so here we have in the this is the information here that essentially gets passed between iterations and one of the things here is this constants entry
um and in the constant entry we have the the tree roots um essentially I think I think by old tree roots we essentially mean um they are the tree roots before execution of these functions not like after insertions uh yeah can we get the link to that diagram sure and I think um just for Mike's benefit I want to mention that um please don't edit the diagram without contacting Mike because this diagram is his baby which I think it says that word for word at the top of the diagram foreign
want to follow up on Albert's question to make sure I understood uh so that means that if function a equals function B and function a makes any sort of State modification function B is not going to be able to be able to see those changes because they are all referring to the same great roads I can take this maybe yeah I've been uh yes so that's kind of um in in the current model uh for the the kind of uh hackathon off site that's the case um that's not the kind of end State and then the current model you'd rely on a very fast Aztec block time to have those changes um the document on hack MD that kind of
uh tries to kind of like tackle this question um you can see it here and basically we need pending pending commitments so as you as you put kind of uh new commitments into the tree you could also uh add those to the kernel context um so that you could either have um an actual commitment that's in the data tree or one that somewhere in your execution Trace was created um and the kernel circuit could kind of do the magic to work out uh like was that pending commitment actually kind of created somewhere in one of its previous kind of iterations and if so it could kind of let you proceed and ignore the check um so if you go down a little bit
there's a there's some like diagrams to try and explain it but it gets quite complicated and so uh but yeah something would have to add on uh kind of probably post off-site to the kernel circuit once we've got this working because otherwise you end up in a bad state in terms of usability costume yeah thanks for the answer um maybe you could explain why um like because another solution would be to to say like uh if I have a that calls B and C I could in line B and C inside a and just have one hoof on one call
so why are not we following this road sure so I think I think the answer to your question is um mainly composability between contracts and um like if we if you only had the context of a single contract and you could say let's just say we could we want to compile essentially using a similar way we already do today with like Noir executables we want to compile like a single executable or a single circuit for each possible configuration of function calls in that one contract but I think the problem becomes like when you want to enable basically any configuration of nestle calls across
like the entirety of all contracts deployed on Aztec um and the ability to essentially like verify all those calls well each of them needs to have like you need to build you need to have some information like a verification key that you can use to verify that one function in one contract is valid that way you can make sure that to when you stitch them together that they were that not only were they stitched together in a valid way but also that each individual circuit executed on its own uh and properly does that answer the question uh not clearly because you can do that when you generate the verification key for the big a it is in line a you can do that at that time so it's not it's not a
good reason I would say I I can jump in on this so I think I'm sorry if I'm I don't want to contradict anything um uh so sorry if I'm uh Missing context but I I do think that it's possible to yeah like when you're compiling your contracts um to like um to stitch multiple function calls together as as independent like effective independent functions um but they're a verification key uh but um we still need to spec that out because obviously like if your contract has 20 functions then you can't create a verification key for each possible like permutation or function calls that you could potentially make but um for for common code paths you absolutely could
um if that does that make sense um so like if you have a function which calls another function which calls another function within your contract then I would expect the verification key to to contain a circuit that executes all of those function calls um and that would just then just be one iteration of the kernel circuit if that makes sense yes it makes sense but I don't see the problem with that I mean you know I think I think I think so let me let me just sorry let me um so I'm I'm kind of um um think about this on the fly but uh because our programs don't have control flow um like like in terms of uh like when once you compile the program through a
circuit there is no control flow so if you have a function that calls another function calls another function you can just treat that as one big function when you compile it um which I suspect we will end up doing but I I guess the reason we still need the circuit complex the kernel circuit complexities uh you can have functions deployed to separate contracts and then you absolutely need um distinct function calls and looping through the kernel circuit because they've been compiled by different people at different times and deployed to different places absolutely that's that's the main purpose of the kernel circuit yeah it's the compose function calls across the like contract boundaries and that may
even be dynamic like based on arguments um you cannot have this control flow inside one circuit but the connect circuit allows you to have this sort of control Flow by controlling which function you are calling in another contract by the arguments for example um I don't think so because if you have control flow and you go to like if you have if and I call B L cell called C then you are you will have B and C on your call stack anyway no no you can shoot you can add one of bills you can add boc onto your cool stack yeah you need both no you don't no as in as in if you if you think about a function call where there's a there's a there's a condition which will
um either call function b or function C um in that circuit what you'll what you'll do is that circuit will push one item onto the function call stack and the address of that of the destination that call um that that will be a witness value that changes uh depending on the predicate value to be either contract b or contract C and then they this pattern is done by the kernel that way you can get this sort of control flow so the pattern I think will be done in within the the user circuit so the idea is what the kernel circuit is going to be doing is it's going to it's going to extract the public inputs of the user circuit and some of those public inputs
are going to be um like additional calls to make and so you know so there'll be some up against will be like you know the like the address of the call to make and the input parameters are cool to make um and the number of calls to make so in your circuit if you want to call either contracts B or C depending on on a predicate then all what you'll do is you'll set your Public Image to say well I'm kind of cool I'm making one function call and the address will be either B or C depending on the value of the predicate and um and and you can do the same with the parameter the the input parameters as well um does that make sense in game does that make sense to you as well yeah absolutely then then uh that I understand the argument with the click
continue with the permutation so it makes sense and it will be much more performant this way yeah any other questions if not we can I guess end a few minutes early well David thank you very much for putting this together this isn't been a fantastic presentation and yeah really really nicely done
thank you thank you and thanks for uh thanks to maximum and gam for teaming up perfectly for this one foreign