ZK Hack Kraków: Introduction to Protokit (Matej Šima)

25 Jun 2024 · 38:29

Matej Šima introduces Protokit, a framework for building privacy-enabling applications on top of the Mina blockchain, in this talk from ZK Hack Kraków. He walks through the basics of zero-knowledge proofs and o1js, the TypeScript zk-DSL used to write zk-circuits, before covering what Protokit enables and how developers can get started with its starter kit. The talk is aimed at builders looking to prototype zero-knowledge applications on Mina.

Speakers

Topics

Transcript

[Music] next up we have Mach who is going to be talking about an introduction to Proto kit this is the Mina 01 Labs uh Workshop so please enjoy take it away hi my name is mat um one of the founding Engineers of protok Kit which is a framework for building privacy enabled application chains on top of mina um you can see bunch of our socials on the so if you want to reach out join our Discord uh check out our website and let us know what you're building um this Workshop is going to be quite developer heavy there's going to be a lot of code Snippets so if you have any questions

please ask and I'll try to answer them ad hoc and he has a mic as well so yeah okay so um the first thing we need to understand before we get into um explaining Mina and prodate itself we have to understand what ZK actually is and what proofs and circuit do so let's go together from zero to ZK hero so what is a ZK proof so ZK proof is a mathematical proof of computation um using either public or private inputs it might include a public output too the things that you can do in a Zer know circuit might include things like signature verification merry inclusion um we support different signature schemes as well and a bunch of other

cool stuff that you might want to compute as well to actually comput a proof you need to write a circuit and that circuit is going to be written using a certain proof system and the proof system that we use in the workshop is the same one as the Min L1 uses and it's called kimchi if you want to compute a ZK proof you first need to run a circuit so therefore approve is a result of circuit execution and the circuit consists of constraints that determine the rules that need to be computed and valid for your proof to be valid as well um in our case when we work with circuits we have to compile them first the result of compilation of circuits is verification and PR Keys um the pr key

helps you generate the proof and the verifier key helps you independently verify the proof without knowing anything about the private inputs um verifying the proof is also super cheap that's why it's convenient to uh write circuits like we will see later the zero knowledg of the proof comes from the fact that we are not exposing the private inputs to the person who's going to be verifying the proof that's very important to to notice so um we will be writing our circuits using OJs which is a ZK DSL built by Omen Labs um the DSL is written in typescript so all of your circuits will be in

typescript as well um asides from simple circuits you can also build ZK apps which is the Mina equivalent of smart contracts you can also use it to build ZK rollups which is something that prid does and many more cool use cases um onj itself includes a comprehensive library of common cryptographic Primitives um there's functions for bunch of different operations that you might want to do like uh verify signatures verify merry inclusions and much more um it also supports infinite recursion as a property of the proof system itself so any SE you write are going to be infinitely composable with each

other um thanks to the fact that we are writing all of our logic as circuits we can run them offchain and submit the results of the execution to the chain itself omj also allows you to write custom Gates and gadgets to extend the building functionality of of the framework itself so there's also building custom Gates and Gadgets in the SDK itself for doing bit operations or fuel arithmetic and a bunch of other features as well so if you want to learn more about omj as itself and about how to build on Mina the first place you should visit is the official Mina dogs all right um let's skip directly into the code and let's take a look at

how you can Implement a circuit in 0js so the kind of a Hello World example that I chose is a counter circuit which allows us to keep a track of a counter which is a un 64 number and it can be incremented using the logic that's embedded in our circuit that we see um on the slide the way this circuit works is that um it allows us to transition transition the counter state from one number to another but it doesn't expose what the counter number actually is as a result or as an input so um the first thing we have to do when we're writing a circuit uh we have to Define the types that the circuit is working with namely

we begin with public input and public uh output so in this case um both the public input and the output is a primitive that we called field that's a basic building block of all the circuitry that we will be writing I believe it's a 25 55 bit number so um our program our circuit will be taking a field as an input and also as an output um our circuit is going to offer a single method called increment that additionally additionally to the public input and output is going to take further arguments and these will be private inputs and this will be of type un 64 so um when we look Below on line number nine and we look at the method and how it's implemented right here um

we notice the following so the circuit code takes in a public input which is going to be a hash of the counter state to keep it private because it's hashed and then two more arguments a is going to be the unhashed value of of the counter and B is going to be the number that we will be incrementing the counter by and the return value of that function is going to be a hash of the new counter state so if we look through the implementation so line 10 to 13 um the first thing that we have to do is we have to verify that the private input that we have received which is the uh unhashed state of the of the counter or the preimage to the hash that is the public input matches the public input

itself so in the circuit itself we hash number a we turn it into a set of fields then we hash it and then right below that we assert that it matches the public input so we give the circuit a hash which is the hash of the current state then we provide a private input with the r State and we make sure that we are providing a private input that matches the public uh the public input um then we do the operation that we actually came here to do so line number 12 we incremented two numbers so the unhashed state plus the number that we want to increment it by and as a result right below that what we do is we take the new state of the counter and we

hash it again and we return it as public output so um I know this looks a little bit silly as an example but um it's the simplest that I could think of what this allows us to do um it allows us to essentially hold like a counter state that is has Ed and if everybody in the room wanted to increment the number and we would go person to person the first person would choose the starting number of the counter would increment it by a certain number and then would have to tell the second person um what the resulting number is so they would be able to run the circuit again and increment the counter again themselves so it opens up bunch of cool use cases

like peer-to-peer NPC computation so moving on um if you want to from the counter circuit you first have to compile it um the result of the compilation is two artifacts it's the prover and the verification key um the pro key obviously lets you generate the proof and the verification key can be then used independently by the party that's verifying the proof to to verify it so um in order to run the circuit um the first three lines of the code that we do here is we Define the initial State that's going to be zero then we Define the public input which is going to be the HZ of the initial State and then we also Define the number that we are incrementing by then uh we call the counter increment method itself we

provide all of these variables and we wait until the prover generates a proof so then we get the proof in uh as a return value so what happens from here is that uh the public output of the proof itself is going to be the hash of the original state plus one since that was the number we were incrementing by by and we can also additionally I mean that's the most important step we can verify that the proof of increment was done correctly using the E increment uh the counter increment circuit so um one more thing that's super important when we talk about circuits and I've already mentioned it before is that the circuits are

infinitely recursive what does that mean um it simply means that we can take two completely independent circuits proof of one circuit execution can be input to another circuit method as we can see here as a private input and that allows us to distribute the competition again like between multiple parties like for example you could do a proof of having some sort of zkk YC send it to me and I could increment the counter on your behalf because the counter would only allow you as a ky person to increment the counter so that's a very very powerful building block that we will be using later on now um let's talk a little bit more

about why are circuits or proof systems important for blockchains so we all know that blockchain is basically just a distributed database with consensus on top of it right now we know that users sign transactions and send them to nodes which then execute the transactions right so if you are invoking a solidity contract on an evm chain um what happens is that you sign a transaction you sign the parameters that you're providing to the um solidity method that you want to run plus you specify the address of the contract that you want to execute and then the node eventually executes that solidity code on your behalf on the network itself once the note does that when they

produce a block um they just take all the user transactions execute them bundle them together and we have a block um if you want to verify the block or essentially bootstrap a note you have to go through all the transactions since the start of the chain reexecute them to ensure that you arrived at the same uh final block State as the rest of the network that is what bootstrapping essentially is if you want to do um if you want to design blockchains like this like ethereum did uh back in the day um you end up with a system which has quite High uh quite High Hardware requirements therefore that makes it difficult for more and more participants to join the

network and further decentralize the network the thing that Mina does um significantly differently is by turning all of that block production into circuits therefore it can be proven and when you bootstrap and you node that comes to the network the not operator doesn't have to recompute anything they just verify the proof of the consensus or the latest uh chain State and thanks to the design um every block is essentially a singular recursive proof that can be verified by anyone including light clients Brower clients or other participants in the network so yeah there we go um apart from the chain itself um

being cool because it's designed uh with ZK circuits from the ground up Mina also offers smart contract capabilities so let's take a look at how you can build smart contracts using one js on Mina so you will have to install OJs that's pretty straightforward npm install and you can call it a day um the class that you will have to import to do that is called smart contract that's pretty straightforward we also call Smart contracts ZK apps to like um just so you know um smart contract Doma is uh significantly different or like fundamentally different from a smart contract on evm as I've said before if you want to deploy a contract on evm you

have to take your solidity code and send it to the node to be stored there to be executed at the later time what happens on Mina is that you write a circuit you compile it and you only send the verification key to the chain itself and then when you send transactions um you execute the contract locally on your device let's say in your browser and you only send the result the proof of the contract execution to the chain itself so so the chain itself no longer has to execute contracts anymore because you can provide a proof of the correct execution done by the client this leads to some interesting benefits for example um the chain being gasless because you don't have to pay for any

computation anymore because you do it yourself so I mean you pay for it indirectly but not with gas anymore um yeah leads to many different benefits so yeah as I said the contracts are executed of chain um the transactions don't tell the note what code to run anymore the transactions only send the proof of the correct contract execution plus the changes that the contract is requesting to be made on the chain for the contract's account we call these account updates yeah so every transaction is a list of account updates each account update can be authorized either by a signature or approve and then um when a Blog is being

produced um a block like a block producer or a note operator goes through the transactions that should be in the block takes a look at every transaction takes a look at every account update verifies the proof or the signature that's attached to the uh account update and to the account itself and applies the changes that the account update is requesting the account update could ask the blog producer to for example reduce the balance of the account increase the balance of the account change the state of the account or any other things that might be a result of the the contracts execution all right so we come back to the block proofs again that's easy um one thing that comes out of the design of the smart contract Z Mina is

that we have potential race conditions um and we have the rise conditions because all of the contracts are being executed locally of chain and if you imagine two users trying to interact at the same time with the same contract creating valid proofs of execution on top of the same data and submitting them at the same time to the chain then the first transaction invalidates the second one in most of the cases because they have computed the proof on top of the same data but they transition from State a to State B and then the second transaction tries to transition from State a to State C so we have something uh that we call invalid preconditions okay um let's take a look at how a counter smart contract would

look like so it's essentially the same circuit as we've seen at the start but this time we have it as a smart contract the only difference here is that with smart contracts you don't really control the public input of the proof but uh you have access to things like um onchain storage the onchain storage for every smart contract is limited to eight Fields word of storage where every field represents roughly 255 bits so you have eight times 255 bits of storage available the API looks like the following um you just write a JavaScript class that extends the uh buildin smart contract class you have to Define your State properties on the class plus you have to Define your uh specially

decorated methods that represent the contract code which you can then run to issue account update for the chain so we again have an increment method that takes into private inputs A and B like the previous circuit but stores the commitment aka the current state of the counter as on Jan State and again the first thing we do um we hash the pre-image again to make sure that it matches the onchain state then line number eight we fetch the onchain state plus we do this special piece of code that tells the chain that the proof that we are calculating the proof of increment is being done on top of the current on chain state which is the

commitment to the current state of the counter so if the current counter is zero and you calculate the proof of the increment um it will create an account update which has a validity precondition for the proof that the onchain state counters commitment still has to be zero when the transaction is processed so you can have a valid proof but an invalid precondition because another transaction has changed the onchain state before you so that is what preconditions do um and then the rest of the code is equal to the first circuit that we saw Plus at the very bottom we set the onchain state again through the account update um if you have any questions like uh please feel free to

ask so if you want to run the contract it's pretty simple you just create a new instance of the class use a transaction API um invoke the the increment method as before then you proove the transaction and you send it um line number eight shows you how the account update will look like after you run the increment method so you're going to have an address of the contract that you're working with um you're going to have a set of preconditions that you have created during the execution plus you will have instructions to update the onchain state plus it will have an authorization of type proof once you are done running the line 11 um this diagram will hopefully clear up the whole account update thing and

the client of chain execution so we start at the very bottom where Alice is on her client executing the counter increment transaction which produces a list of account updates that are part of the proof that's the transaction that we will be sending um we will prove it and we will send it so we will send it to the network to our favorite RPC provider and the transaction will end up in a mle now what will happen so we are on the Chain itself we are producing a block and uh we have some initial state of the contract before the transaction has been processed so we try to apply the transaction to the account we use

the account updates provided by the client we authorize them by verifying the proof that is attached to the transaction and to the account update itself if the proof is valid or actually if it's not valid then we revert and the transaction fails if the proof is valid then we also check the preconditions and if they are not valid we revert the entered transaction if the preconditions and approve are valid we transition to the resulting state which would be the updated counter commitment all right so um I've previously mentioned that there's only eight Fields wor of onchain storage for every account or every contract um which means that it's not really possible to do key value storage

within the smart contracts natively what you can do however is tab into a simple primitive called merries um which have this unique properties that we will explain in the next slide but essentially what it means is that every mil tree has a root hash so there's a hash of all the data that is in the tree and you can store that within your smart contract and then within your uh circuit implementation you can create a proof that certain piece of data is part of the tree so that you can work with it so what's a Merle tree so Merle tree is a data structure that allows us to cryptographically verify that a piece of

data is part of a larger structure um we do it in the following way so you see the entire tree as it is at the very top we can see the tree of uh the root of the tree uh we see a bunch of nodes that that are there and the leaves at the bottom or the nodes at the bottom are the actual data that we are storing in the tree so this is a pretty small tree of height two or three I believe so we can store four pieces of data here which is not really interesting but in reality or in practice we use trees we use trees that are much higher so we can store I don't know billions of data entries there um and have the ability to like statically or quickly prove that that piece of data is still part of the

tree so um if you want to write a contract that is using merries to store data you would have to do a bunch of things yourself the first thing you have to do is you have to figure out how to read data from a Merle tree within your offchain executed smart contract so you literally have to prove that a piece of data is part of the tree and you do that by fetching a vitness and the data itself um from like an offchain storage service that you might have to implement yourself as well and then within the circuit you calculate the root hash by hashing uh your data with its siblings and the parent uh noes with it siblings

again until you arrive at the root hash and then you can compare that to the data commitment that you are storing in the in the the in the contract so if you look at it line by line so we are getting the commitment that's going to be um the root hash that we store on chain then we turn the key that we are using in our counter example to a field so that we can use it as a key forry then we get the witness and the actual value that we want to be using for our counter um then we calculate the root using the value that we have received and then if the calculated commitment to the ru hash matches the start commitment then we can trust the data that we have received and we can

continue with the counter logic as we would have before on the other hand if you want to write data to the tree it's essentially the same but you wouldn't be using the data that has originally been in the tree but you would calculate the root hash using the new data after incrementing it then you would arrive at the new root hash and store it in the contract again so um what are the design limitations of this approach right so we know that contracts are executed on the client side independently of other clients we know that if we rely on onchain state that creates preconditions and in our case we would have a precondition pointing to the root hedge of the tree

so that creates some interesting cases which we will describe here if we have two users transferring at the same time um their preconditions or the root hes they have used used to calculate the increment proof would be the same and once one increment transaction goes through it will change the root hash that's stored on chain um which will invalidate the second transaction here's a small diagram of how it roughly works so again we have Alice executed counter. increment using some arguments um she receives a witness and the data that she needs to work with the counter that gives her account updates which have a precondition to the root hash commitment and if there's also

Bob who's doing the same transaction at the same time they end up colliding on the precondition which ends up invalidating Bob's transaction which is not something that you want to do in practice because it would limit throughput of your apps okay so um um our team has basically sat down and we Tred to think about what can we do for zkf developers that would solve these inherent issues or these properties of the of the smart contract system and we arrived at the framework that allows you to build zik rollups on Mina because if you think about it even the original smart contracts are essentially like micro Z rops because you have to run them

offchain anyways so we thought why not take it to the next level and provide um an extra set of tools an extra set of infrastructure that leverages recursive proofs to allow you to build essentially ZK app chains so yeah um the name of the framework is protate you can install it like using mpm it's very simple it is also tapescript based it is based on OJs and it uses a bunch of like quite a few recursive circuits under the hood to allow you to not have to do all the merry operations that you saw before focus on your business logic ship your apps to our users and let us worry about the

infrastructure so what is protate it's a scrip based framework for building ZK chains it is not another zkm based framework um instead we use a ZK VM that we wrote ourselves we support a hybrid execution model so that means that like with smart contracts originally all the code runs offchain on your client with Brit what we do we sequence the user transactions so that you don't have the precondition collisions or anything like that but to be able to do that we had to sacrifice the Privacy property and take all the execution on chain that was the first step of the problem or the solution the second step of the solution

is introducing this hybrid execution model by allowing users to maintain the offchain computation and send proofs to the sequencer that they have done something correctly so that allows you to maintain your privacy and we call that the hybrid execution model so yeah of execution client at DK proves um the important thing to mention here since every every piece of code that you will write with proit is going to be a ZK circuit plus also the underlying VM is also a series of circuits it means that uh whoever is operating the ZK rollup running the sequencer has no room to lie about the execution because all parts of the execution are proven and then eventually

settled to mina to be verified there the enter framework is built from the ground out to be customizable and modular so that means that we provide a bunch of stuff out of the box that you can reuse yourself if you want to but if you feel like you don't like a part certain part of the framework you can throw it out replace it with your own module as long as it matches the interfaces that we ask for so let's say that you don't like our mle implementation you can write your own and plug it into the framework we were aiming for the learning curve to be as shallow I hope that's the right word as shallow as possible so it should be relatively straightforward for you to pick it up

during the weekend okay so um produ is meant to be use it used to build your own ZK rollup um as I've said before you know ZK rollups are a series of recursive circuits they can prove transaction execution and also rollup State transitions State transitions are essentially instructions to our protocol that tell the protocol um how should the merot Tre State be changing the L1 itself so the Mina L1 um offers sequence Sten actions to help you build ZK rollups but you still have to ride your own circuits like you've seen with the smart contracts um with Brit you can really easily build zero knowledge

interoperable and privacy preserving apps and you can build your own ZK rollups um essentially the zops that you build with Brit are in fact the super set of zaps or the smart contracts because we offer the same privacy features through offchain execution and recursive proofs but additionally we also offer onchain execution and sequencing so you don't have the precondition issues that you might run into if you try to write your own um offchain state so what is a product Z all made of you have U runtime which is where your application logic lives then you have the protocol which is where we take care

of the block production transaction fees account State nones State Transitions and everything else that you don't have to do anymore because we wrote it for you plus you have the sequencer where you have the menol the block production settlement of theop state to the L1 and a bunch of other features of course you also have the L1 settlement contract um which is a smart contract as you've seen before and the proof of the execution of your application change logic gets settled to the L1 okay so what kind of features do we offer um we offer a supercharge developer experience so if you know typescript or JavaScript should be very

easy for you to get started all the modules are usable and open source so you can just install them with mpm um you don't have any data race condition issues since we are sequencing all the transactions um the onchain storage is not limited like the one for smart contracts um you can essentially say that that that it's bottomless because of um the merry size that we use you can still preserve the offchain privacy because we have the hybrid execution model and you can just focus on shipping uh the business logic of your app we are also integrated pretty well with um all the ecosystem ballots that use the me assigner um standards so yeah one cheeky thing that

I wanted to mention originally was that if you wanted to you could reimplement even the smart contract protocol itself in your app chain prot it but you probably shouldn't do that uh yeah so some more things that we're working on um they're working on improving the interoperability between the zops that's an important topic and also data availability so if you were to build a zik r prod today um you would be essentially running a validium because there's no no data availability guarantees uh built in today but there will be so yeah so um here we have a diagram that shows you how a transaction life cycle

might look like if you build your chain with protate so we start on the client again and we have Ellis um creating a transaction that calls balances. transfer which is part of our R time she doesn't have to prove anything because there's no offchain computer privacy necessary at this point and she just has to sign the transaction and send it to the sequencer if she wanted to have optional privacy she could create a proof of a separate circuit execution build using the ZK programs we've seen before generate the proof and provide it as an argument to her sign transaction and send that to the sequencer now when the sequencer produces a block it will execute all the

transactions that righted from the users in a sequence that means that first transaction has has I don't know balance thousand then you do a transfer okay then the second transaction has a balance 500 so that's just sequencing or something that you would normally expect in an evm environment so once the runtime logic is executed we end up producing what we call a set of State Transitions and those are instructions for the underlying protocol that tell the protocol how the Merle tree State should be changing and we prove even the merry operations then we merge the proofs together recursively until we have a block proof which then goes back to mina um here's how you can write your

own business logic so it's very straightforward again you extend a class that we provide out of the box and you define some onchain state so is essentially unlimited because the tree is so huge um here we have a balanced storage between a public key and a un 64 that represents our balance and we have a single method on the r time that's callable by the users so it's called transfer and the are arents are from to and the amount so the first thing you do is you fetch the state then you do a bunch of assertions making sure that the from address has a sufficient balance um you do an assertion here then you subtract the balances and you set them so writing runtime code is really really

straightforward shouldn't be any different from let's say writing solidity code except it's typescript um you don't really need to be a ZK Wizard or a mathematician to write circuits using om JS and how to like also ship your app chains using broid so um if you want to write tests for a runtime that you have written so you write your runtime then you go write your test obviously or if you are feeling fancy write your test first and then you write the run time it's up to you um all you have to do is create an instance of a testing Gap chain within your uh unit testing framework we use just so you just specify the layout of your runtime

um for this example my runtime only consists of a single module called balances then you start the app chain and you set a signer for your transaction API for the test okay executing a transaction a very similar experience to the uh mini API so first of all you have to figure out which module you want to be calling then you forge a transaction you provide the sender then you specify what transaction you want to be calling which and what parameter you want to be using you sign it you send it to the sequencer and then in test of course you have to manually produce a block and then you can query the state of the chain through our query API which under the hood is just the

graphql call to the sequencer and that gives you the actual balance that is the result of your transaction and then block production okay so if you want to start hacking with protate um you won't find it in the minad do um you would only find Smart contracts and omz itself if you want to start hacking protate you have to go to protate doev and find our dogs which will lead you to the starter kit which you can clone install the deps and then just run a few commands and you have a full chain running locally with an example UI Vol integration notifications and everything else that you need for a hackaton to start working on your

idea this is what you would get if you install the starter kit it's a a simple example UI where you can connect the wallet and drip some tokens from a faet plus get notifications what are we working on now so um one of the things that I'm personally super excited about is working on open Treasury and governance for all the projects that use peret um we're also working on a privacy enabled Dex called copen and the things that I would like to see you guys work on are for example nft standards Ling Market stable coins name Services credential Solutions Z kyc or whatever else you would like to work on

um yeah thank you for listening um please check out our website um and let me know if you need help with building at our boots thank you any questions maybe no you have a question your is yeah you you mentioned that your merer tree is so big is basically like boundless so like how deep is it actually uh 256 that's the height I mean it leads to some unwanted side effects like um The Proving of the rootes calculations being relatively

slow but there are other designs that can solve that like having index trees which is something that we are currently in the process of adding into the framework that's going to help help us increase the throughput for example okay thanks thanks uh relatedly uh I'm curious uh right now you say data availability is coming uh how is the storage handled for all the state that you need to meriz um presently and are there any options can you repeat your question sorry uh so you're proving the offchain state uh with this Markle route right uh who is in charge of storing that before data availability is available as you were talking about um just the sequencer I mean uh by default it has to be the

sequencer because to produce the blocks to run the transactions you need to have the data somewhere so that start at the sequencer the step after that is of course making the data available for the rest of the world so that the sequencer is not the only one who can produce the blocks that's like the basic premise of Da okay thank you um see you at our boot thank [Applause]