New Ethereum talks, every Monday. The week's conference uploads by event, in your inbox.

Loading player…

Public-Private Composability by Mike Connor | Devcon Bogotá

DevconMon, Oct 9, 2023, 12:00 AM

Visit the https://archive.devcon.org/ to gain access to the entire library of Devcon talks with the ease of filtering, playlists, personalized suggestions, decentralized access on Swarm, IPFS and more. https://archive.devcon.org/archive/watch/6/public-private-composability/ Learn about the challenges of designing a private execution layer for Ethereum. Previously, smart contract execution (L1 & L2) has been fully public. Some apps provide basic private functionality for a single private state (e.g. privacy coins). We'll discuss ways to execute general private and public state changes across multiple smart contracts in one transaction, within a zk-rollup. This unlocks programmable private smart contracts. Speaker(s): Mike Connor Skill level: Intermediate Track: ZKPs: Privacy, Identity, Infrastructure, & More Keywords: zero knowledge,privacy,smart contracts Follow us: https://twitter.com/efdevcon, https://twitter.com/ethereum Learn more about devcon: https://www.devcon.org/ Learn more about ethereum: https://ethereum.org/ Devcon is the Ethereum conference for developers, researchers, thinkers, and makers. Devcon 6 was held in Bogotá, Colombia on Oct 11 - 14, 2022. Devcon is organized and presented by the Ethereum Foundation, with the support of our sponsors. To find out more, please visit https://ethereum.foundation/

Transcript

Here. Uh okay, yeah. I'm Mike uh and I'm going to talk to you about public private composability and what that really means is I'm going to talk about Aztec's private smart contract architecture. So, uh we expect it all out and we're going to be spending the next year, maybe more, uh building a general private smart contract platform for Ethereum. Uh so, what were our aims when designing this private smart contract platform?

Well, firstly, private states. And what private states are are values known to only one person. So, a private state has an owner and no one else in the world knows the value of that state. Okay? And then private function execution.

And this is really cool. This is the ability to execute a function without revealing to anyone in the world which function you've just executed. So, similar to like the Zcash protocol. And we want people to have a similar experience to Ethereum. So, we want people to be able to deploy contracts permissionlessly.

So, just anytime you want to deploy a contract to the system, you can just deploy it. And again, similarly to Ethereum experience, we want composability. So, we want someone to be able to call a function and that function might make a call to another function of another contract. And that function might make another call to another function of a different contract and so on. Uh so, that's really important.

And we want a different kind of composability as well. We want composability between private and public functions, which I'll talk about more as we get into it. Um and altogether, we just want intuitive transaction semantics. So, we want we don't want a developer to have to think about the underlying cryptography or any of that. We just want them to have as close to a smart contract experience that they have with Ethereum.

Okay, so I'll take a step back. What fundamentally is a smart contract? Well, at its core it's just a collection of state variables and a collection of functions which may edit those state variables. And here we can see a naughty example of an ERC-20 contract. Balances is a state variable and there's a function called transfer which has rules which allow people to edit the balances state.

And there are 2 to the 160 such balances states in this example. But what would an Aztec smart contract look like? Well, people will still be able to write functions and states in for layer one, so for Ethereum. They'll also, similarly to all the other rollups in existence, the ZK rollups and the optimistic rollups, they'll be able to deploy functions and have persistent state on a layer two just to save on costs. But the key differentiator with Aztec contracts is they will also be private functions and private states.

And crucially you can see here these will be on a user's device rather than executed by Ethereum nodes or rollup sequencer. Okay, so what do I mean by private state? Let's look into it a bit more. Get everyone up to speed on some background. So a public state is just a value in a Merkle tree.

It's the same in Ethereum and it'll be the same on our layer two. What's a private state? Well, it's just a commitment to a value in a tree. And it has an owner. So here we can see an example of a commitment.

It's got an owner public key. It's got some value that you want to hide and store persistently in our layer two storage. And it's got a salt which is something random to give the commitment hiding properties and maybe some other stuff depending on the application that someone deploys. Public states will follow an account model just like states in Ethereum smart contracts. And with the account model, a state lives at one position in a Merkle tree, one leaf, and will occupy that leaf forever.

So, if someone ever wants to edit that state, the person executing the contract will have to go to that exact leaf position, modify the value. But, that's no good with private states because if you have to tell the whole world, "I'm editing this particular leaf of this tree, please update your node accordingly." you've revealed which state is being edited. So, that doesn't work. Which is why we adopt a UTXO model.

And in a UTXO model, once you add one of these commitments, one of these hashes of a private value to the tree, it never changes. If you ever want to edit that state, similar to the Zcash model, you have to nullify that state, which allows you to change it or prevent it from being used again without revealing, and you have to add a new commitment to the right-hand side of the tree. So, you're always appending new values. Uh so, if someone comes along and they want to edit your state, they will create a new commitment representing maybe the addend that they want to add to the state or something like that. And just to continue with the background and carry on with this Zcash-like protocol description, or indeed our current Aztec Connect protocol description, uh let's say someone inputs a commitment into their circuit, and they want to modify the value contained within that commitment.

They pass it into the circuit. The first thing they would do is prove that this commitment does indeed exist somewhere in the Merkle tree of notes. And in Aztec Connect, the Merkle tree is 2 to the 32. With this private smart contract platform, it might be quite quite a lot bigger, because apps will probably create a lot more commitments. Okay, the next thing you do is you open the commitment, and you would hash the values to prove that you indeed know the preimage of the commitment.

And Alice in this example is the owner of this state. And so for her to prove that she is indeed allowed to modify the state, she has to prove knowledge of her secret key, and she can do that by just hashing it. However, or following however the secret key is derived. And then she produces a nullifier. And the previous talk was quite helpful cuz it explained nullifiers, so I don't have to.

Um and then they prove it doesn't yet exist in the nullifier tree. And the nullifier tree at the moment is a sparse uh Merkle tree. It allows you to prove non-membership. Um but we're actually going to update this to a linked list tree, which allows you to prove much more efficiently that something doesn't yet exist in the tree. And suppose Alice wants to perform some simple operation on a state, she the circuit, she'll add five to it.

And you can see we've incremented the value that Alice is hiding. We've changed the salt to re-randomize the commitment, and she adds it into the tree. So this is quite a general way of just modifying state within a public protocol. And crucially, this step has to happen on a user's device. Or at least in the most basic systems, this has to happen on the user's device.

You can do some delegating schemes, but we won't talk about that here. And so this gives our roll-up something different from other roll-ups. There is a layer where if someone wants to modify a private state, they have to do it on their own device before then passing it to some sequencer who will deal with all the public state changes. And indeed, because it has to happen on a user's device, possibly in the browser, we need extremely efficient proving systems, which you know, we've been working on and we're continuing to improve. So on an L2, functions are slightly different from a Solidity function.

They're actually ZK-SNARK circuits. So similar to uh other ZK roll-ups that you might have read about, except our roll-up will need to be actual ZK, so provide hiding properties. So if someone begins with a circuit, suppose they write a circuit in some circuit language like Noir, um they write the rules of their function, you know, it might include state changes, computations, stuff like that. And using something like Noir, they can take that circuit and compute a proving key and a verification key. And a user will come along and they want to execute this function, or rather prove that they have executed the function correctly.

And that function will take some secret inputs and some public inputs. And they'll pass it into Noir, a big black box, and it will generate a proof. Out will come the proof. And you can send the proof and the public inputs to some verifier who already has the verification key, and they can verify that you did indeed execute the function correctly uh for the given set of public inputs. One thing I want to point out with this slide and that we'll need going forward is that the verification key can be used as a unique identifier for the circuit because it was derived from the circuit.

And the hash of the verification key is like a unique single number that can completely represent the circuit. And yeah, this this can all be handled by Noir. So, if a verification key can represent a circuit, and if a smart contract is a collection of functions, then we can create uh well, we can represent our set of functions as just a set of verification keys. And if we want to prove one of our objectives that we have executed a function without revealing which function we have actually executed, we can just Merklize all our verification keys into a big tree, and we can add our contract or our set of functions to a giant contract tree, which contains all functions that have ever been deployed to this layer-two smart contract platform. And in doing so, if this computation, a Merkle membership check, is done within a circuit, you can hide which function someone has executed.

Uh so, Zcash already came up with a system which was really good a few years ago. Uh it's a good paper if you haven't read it uh for hiding which function has been executed. And this hash here represents a commitment that someone might add to a tree that we talked about earlier. And within the commitment they had a a nice idea that you would put in a birth predicate, i.e.

, details of the function which created this commitment, and a death predicate, a function which the only function which may nullify this commitment. But if people are familiar with Solidity smart contract development, it's not quite good enough to just have one function a modify state. You might want multiple functions to be able to modify a state, which is why on the previous slide we've kind of collected all possible functions of the smart contract and added them to a contract tree. And so instead, what we do in our smart contract spec, is we include a contract address. And that represents just the set of all functions which may edit this particular state.

And again, this is how Zcash proposed to hide the function being called so that no one knows really what's happening in the system. You know, all observers see is that some function got executed according to some set of rules for some smart contract. Uh you take the private circuit and generate a proof. And if we stop there, the world would see which function got executed. So we pass the proof into this kernel circuit.

And the kernel circuit verifies the earlier proof and kind of masks from the world which function got executed, which is really cool. And we take this and we take it a step further, which I'll talk about in a second. I'll talk about it here. One of the things we said, one of our aims is the ability for functions to make calls to other functions and so on and so on and so on and so on. So how are we going to do that?

Well, first, why would we want to do that? Here's a really simple reason. If you consider a decentralized exchange uh just on layer one at the moment, if Alice wants to swap tokens A and B, she must first call the exchange contract, and then the exchange contract has to call the ERC20 contract for token A, and then she and then the exchange contract has to call the ERC20 contract for token B. So, the most basic kind of applications on Ethereum require nested function calls, and we want to mimic that with our private programming uh smart contract language. What does a function call look like?

Well, in Ethereum, it's a contract address, some identifier for where people can locate the function, and a set of arguments. And so, similarly for Aztec, it will be the contract address, the VK index, as we saw in the previous slide, the verification keys all get added to some contract tree, and a hash of the public inputs to the circuit, which represents a function. So, let's work through an example, and you don't need to read the code for this. All you need to really look at is the colors. So, we've got to Imagine someone calls the red function over there, function one.

And the red function is making call to an orange function in a different contract. And in turn, that orange function is calling a blue function in a different contract. It's returning some values, um and then it's calling a green function. And this green function is a public function. So, what I'm going to try and demonstrate is how functions can call functions of other smart contracts, and can also you can have functions from private world to the public world.

Well, what we first have to do is each of those individual functions is an individual circuit. And so, you would have to generate six distinct proofs, one for each function that you want to prove you have executed correctly. But then the question is, how do we prove that they relate to each other in a way that the code wants us them to relate? So, how do we prove that they were executed in order, and the parameters that were sent to functions, and the return values all intertwine correctly. Well, we we use call stacks.

So, say we want to execute function one first. Well, the public inputs to every single function on our smart contract platform will have to adhere to a very rigid static ABI. And one of the fields in that ABI will be call stacks. So, because function one makes a call to the orange and yellow functions, this circuit will expose through its private inputs the call arguments and the function signatures of the orange and the yellow functions. And similarly, the orange function is making a call to the green function.

So, oh, and the blue function. So, it will also expose calls to the blue function and the green function in its public inputs. And notice the green function is labeled as public. And so, because public functions get executed by the sequencer and private functions get executed by the user, they have to be separated in this way. And on we go.

The green function is making a call to the purple function, so we add that add to the call stack. And here's the really meaty cool bit, uh, and that's kind of extending the the Zexe example we saw before where if you pass a function into a kernel circuit, it hides which function function has been executed. We add some extra logic to our kernel circuit to allow these call stacks to be interpreted and to check that the parameters being passed between all these functions make sense and are according to the rules of the smart contract that somebody wrote or smart contracts, plural, that some people wrote. So, we begin with function one. We pass it into the private kernel circuit.

And the private kernel circuit will read the public inputs of function one and push onto its own call stack the two items that function one makes a call to. Um, and then we will carry on with our recursion. Uh the private kernel circuit will spit out a proof, and we will pop the next call from the call stack. And we'll pass it in to another iteration of the private kernel circuit. So, each of these arrows is a proof.

And within the private kernel circuit, two proofs are being verified each time. And so, we can see the call stack has shrunk a bit. And then, we pop call number two uh the orange call off it and pass that into the private kernel circuit. And we do recurse until the call stacks are empty, and then we know we're done. Except the orange function is making calls to another two.

So, we'll just keep going until Okay, finally, the private call stack is empty. And therein ends the work that needs to happen on a user's device. So, because the user only executes private functions, public functions are executed by the roller provider. This proof here will be sent to the roller provider. What will the roller provider see?

Well, they'll just see that somebody executed some function of some smart contract and changed some kind of states. But the roller provider will have no idea which. Um which is really, really cool. What they will see if the private function is making a call to a public function, they will be able to see the public functions which need to be called. There's no way around that.

So, seeing the call stack as it is, the roller provider will take uh this function and execute it and pass it into a public kernel circuit, which has, instead of hiding logic, it just executes states out in the open, similar to, I guess, other ZK rollups. And this function's making a call, and we're almost done. We pass that into the kernel circuit, and hooray, the call stacks are empty. We finished executing our function trace or our stack trace. And this can then be sent to the rollup circuit.

This entire thing represents a single user transaction on our smart contract platform and it can get rolled up with all the other transactions. Uh you don't need to read this. All this is saying is that the in order for the kernel circuit Yeah. In order for the kernel circuit to be able to operate on any kind of function we need one giant static interface which accommodates as many possible function permutations as possible. And that's what these interfaces are.

There's one for private circuits and you can see there's a lot of arrays uh to allow for various sizes and various permutations of stuff that an app might wish to do or a function of an app might wish to do. And we've got one for public circuits. Similarly the private inputs and public inputs to the kernel circuit, there's loads of them and there's a lot happening. Um there is so much computation happening in these kernel circuits that we've been working on a a better proof system and more efficient proof system for ZK Snarks. And it's actually similar to Hyper Plonk which Espresso Systems have been working on.

Um it's really cool idea um and I guess there'll be more on that at some point in the future. So we have this transaction and I mentioned when we get to this stage you get sent to the roll up. What does the roll up topology look like? Well, first I want to point out that that example transaction was doing some private computation for the user and some public computation. But it doesn't need to be that way.

It can just be this transaction is just public computation um similar to other ZK roll ups. Or it can be completely private computation. You can think Zcash transactions are completely private. So you could deploy an app which does Zcash-like transactions on this smart contract platform. And I won't go into it today, but there's also the ability to deploy contracts and you can even have similar to uh Ethereum uh functions deploying contracts for you from a smart contract.

Okay, so we've got these transactions and they get sent to the sequencer or roll-up provider. And this sequencer is going to compress, compress, compress, compress until they're well left with one proof. Uh, and the way you do that is by verifying proofs within circuits. So, this base roll-up circuit verifies two proofs, and this one verifies two proofs. And then we go to the next level.

And we verify two more proofs. And then we end up with one proof. And because the proof system we use here ends up being quite expensive to verify on Ethereum, we'll squish it down to a cheaper proof like Plonk or Groth16, and then send it to the smart contract. Right. And then within the smart contract this proof will be verified, and the state trees of all the various trees we're managing will be will be updated, or rather the roots of the trees will be updated.

Okay, and I think this is the final slide. So, what I want to point out here is that there's always this arrow of time with these transactions. So, you begin with a private function executions or proving you've executed private functions, always on the user's device. And once you've done that step, then you can go send your transaction or your call stacks to the roll-up provider, who will execute their stuff. And we'll ignore the contract deployment step.

And then it gets sent to the roll-up contract. And from there, similar to Aztec Connect if you've looked into it, you can then make calls to any smart contract in Ethereum and interact with any DeFi contract that already exists. But, suppose your private function wants to make a call to a public function or to a layer one function and receive return values. Well, it can't receive return values immediately because of this arrow of time and the order things need to happen. Excuse me.

So, you'll have to wait until the next roll-up. And so, what we have, and I don't have time to go into it here, is if a private function is making a call to another layer of this stack, it will have to record callback functions similar as to if you're working with JavaScript. And those record of those callback functions will be added to a tree. And then in the next roll up the user will have to prove that they are executing the correct cess or success or failure callback function in order to carry on with the execution of their function. So there's a lot of complexity there.

Um Uh we've written a lot of documentation which I think we're going to publish which kind of explains all of this complexity. Um But yeah, that's everything I wanted to talk about today. Uh oh and I guess I do have time to just briefly say what the kernel circuit does. It does tons. Um and my throat's hurting so much that um I'm not going to through go through this, but uh feel free to take a picture and uh ask me questions afterwards.

Uh yeah, thanks very much. We still have time for one question maybe. If someone wants to do a question, we have our volunteers with the mics. There you go, here to the left. And maybe one there depending on time.

Um one thing that you didn't touch it on the presentation was about the participants. So you mentioned in the beginning that you might have private state with participants. Um so for example, GoQuorum with the Zera and Besu with the Zera which I'm sure you're aware that platform that allow you to have public and private state contracts. They have different ways to tackle it. So Besu does like groups.

You define your group and that group will share a private state. And GoQuorum which is a fork of geth uh with modification for private state allow you to like select for each transaction which participants will see those change. And that means that you can have the versions on the state. So like you might have a transaction that you change one smart contract uh and you change and you apply that state change for two participants, but then in the next transaction, you change the same value and only one participant will see it, not the other. How do you solve that in your platform?

Uh I didn't fully follow the long question. Um maybe we can chat afterwards. Sorry about that. Yeah, you can chat outside. That's the right place to do networking and get to know more.

And we have one more question there. Please stand to that mic. Hello. Um just one quick question. Uh is is is this private call for contracts already available in Aztec or it's going to be in the next release?

Uh so, we'll be releasing the specs, but we we haven't built this. Um we're kind of we've begun building it um but it's we're probably maybe a year away from testnet. Um So, yeah. It's a it's a big beast. Big round of applause once again for Mike Hearn.

Thank you.

Automatic transcript — names and jargon may be misspelled.