 Hello everyone to the OpenZKD workshop where I'll be showing the Stark implementation that we recently announced. So it's built in Rust and anyone who wants to code along, I really recommend that you get through these commands right now because it will download the relevant libraries. And given the conference Wi-Fi, you might want to do this now. I'll keep this text up in every single slide so you don't have to remember it or jot it down. Just do it while I'm talking. So first of all, come onto the upper right corner. Do it. Also, I'm assuming that if you're coding along, you're familiar with Rust. If not, you'll be fine just listening. So what is a zero-knowledge protocol? Essentially, you have a thing that is a claim which you can prove given some additional information called a witness or the private input or the secret. And that results in a proof that is usually much smaller than the actual thing that you're proving, so smaller than the witness. And you can verify this thing. So if you have a claim and you have a proof, you can verify it. You can make sure that, indeed, a witness exists that satisfies this claim. I'll give you an example soon. Now, the particular kind of proof that we've implemented here is a start. This is a protocol family developed by Eli Ben-Sassan, Michael Leabseth near Goldberg and others. In particular, Eli Ben-Sassan has been a hero in this field. He has been working on this for a long while. And high-quality protocol variants and implementations of this particular protocol family are made by Starkware. So a huge shout-out to them for all the amazing work they've done in this field. Open Stark is an independent and partial implementation of the specific Stark variant used in the Stark test demo. So why Stark? Starks have some very interesting and unique qualities. They have no trusted setup. They require only elementary mark. Because of their simplicity, they have very good proofer and verifier performance. They have small proofs, not as tiny as the proof you can get using bearing cryptography constructions. But this comes with a caveat. They have a unique but completely different constraint system from everything you're used to. And this makes them so interesting to study and play around with. So, quick overview of how these things work. To prove that a program has executed on some input data, you create a computational trace of the program's execution. And you lay out this trace in a big table. Now, this trace also contains your private input. So this table is sort of secret. But we want to have a proof that binds you to this particular trace. And in Starks, we do this by creating a set of equations which you will evaluate over the trace table to prove that it is correct. We'll give an explicit example of this soon. But it will allow you to do that, hey, every next row needs to contain the sum of previous two rows or so on. Or you can say that it needs to be equal to a particular value. So, big slide, big scary slide. Don't worry, we'll go through it by then. What we're going to do is Fibonacci. So, Python has connotation Fibonacci is you have A and B. We initialize A with 1. We initialize B with, in this case not 1, but some secret value. This is going to be our private input. And then we just iterate. We update A and B with the previous B value and A plus B. So this is a standard Fibonacci sequence. And then we return A for some index. So if we have this function and we run it, we can do Fibonacci the 200 value starting with secret value 42. And you'll end up with a large number that starts with 1, 1, 9. Now we want to create a zero-knowledge claim on this. By the way, I'm saying zero-knowledge, but actually the implementation we have is rather as a single proof. Not zero-knowledge. Kind of a detail. So we want to claim that we know some number, and I'm not going to tell you that it's 42, such that if we compute the Fibonacci sequence on this and take the 200 value, we get this big number. So this is going to be our public claim. All right, so let's go through the steps. The first thing we need to do is create a computational trace. So we run this Fibonacci program, and we write down at each step what the values of A and B are. So in the first row, A is 1, and B is our secret value. Because we're calling it with index 200 and secret 42. So A is 1, B is 42. And then we go into the little number. So for I in 0, 2 index, we get A equals B. So the next A is our previous B. And you see that this happens everywhere. And the new B is the sum of the previous two. So this value here is 1 plus 42. 43. Same here. And this goes on until we hit row 200. And then we get our big number. The outcome that we want to demonstrate. Now, if you look at this trace, you can sort of see that there are four simple rules that capture the entire nature of the problem that we're trying to prove. And these rules are written down here. They are the so-called constraints. They come in two basic kinds called boundary constraints and repeating constraints. Our boundary constraints are the first two. We need to have the first column be 1 for row 0. So this value here needs to be 1. Then we have another boundary constraint similar to that one. That says that the first column needs to be our big outcome, but only on row 200. So this value needs to be our big M. And then we need to demonstrate that it actually executed this little loop here. So we need that for each row, the first column is equal to the previous row, the second column. And this should have been a 1 here. Sorry about that. And this holds for each row in this table. So this is a routine constraint. Similarly, the second column for the next row is the sum of the two columns in the previous row. And this value is the sum of these two values. Now, these are four simple rules, but if you think about it, this captures everything that is intended with this claim. So if you have a trace table that satisfies these constraints, you've shown the disclaimer. Now we've reduced our problem from this thing here to this thing here. We still haven't made our trace table secret, but that's part of the start protocol. But in order to do that, we need to do a couple more iterations on what these constraints look like. And the next thing we need to do is we need to turn them into multi-prime constraints. So instead of having equalities, we need to create expressions that are 0 whenever they hold. How easy. Whenever you have an equality sign, just replace it with a minus. So this thing needs to be 0 on row 0. It's only 0 when this thing is 1. Perfect. Same here. This thing needs to be 0 on row 200. You just subtract the value and then it will be 0. Same thing for us needing to have these two values equal. You just subtract it. And again, when we need to have the sum, we just subtract the left-hand expression from the right-hand expression. Okay, so now we've done that. We've changed our constraints such that they are expressions that are 0 whenever the constraint holds. Now we need to do an x-trick. The reasons for this trick are kind of mathematical and I don't have the time to go into the details of exactly why it is the way it is. But if we have a constraint where we want i equals some value, we need to multiply this by a thing 1 over x minus omega to the power i. Omega is some constant. And similarly, if we want a repeating constraint, we need to multiply this thing with this expression here. When you do that, you get this system. It looks complicated, but it's really just these things multiplied by these things. And that's pretty much it, what you need to do in order to create a constraint system for the few moments you start. So let's show you now what it looks like if you implement this using our library. So is anyone trying to code along? Yes, at least one. Now this makes sense. So yeah, I'm hoping you got it to the point where you can do the build release and you managed to download the things. I did the build release, but I got a bunch of build errors. It should just be warnings. I'll just link here now. So what we do is we take the ZKP start packets and we just import everything. We also import the prime field that it uses. For the current example, it doesn't really matter because our value is small enough. But if you're working with these systems in practical applications, you need to know that every operation happens in some large prime field, which has a lot of applications. So the first thing we do is we create a struct that represents our claim. In this case, we created a struct for the matching claim. It has the index, which in the example is 200. It has the secret value, which was this very large number that we had. So it has the public value that we want to claim, the largest number. And now we implement two traits on it. Proofable and verified. Let's start with proofable. So what do we need to do? We need to create a trace table for this thing like we said. So what I already have here is we have the struct itself, which is here. We have our secret value, which is here. We need to output a trace table. So for mathematical reasons, the trace table always needs to have a power of two size. So we'll just round index up to the next power of two. Don't worry. We'll still get row 200 out of it. We just need to compute 256 rows. And we create a trace table with this size and two columns. So let's initialize it. We start with adding our leftmost thing like the one. We need to have the second column be our secret value. And then we have our for load where we update each value. So the first one was a copy of the time minus one comma one. The second value, or V value, was equal to the sum of the pre-proofable talk louder. Oh, yes. So I'm adding it. So if you're not familiar with brush, you can ignore the understandings in the clones. They're just a satisfied borrow checker. So let's see if this works. In fact, let's add a little line where we say that the value equals, let's print the value that we want to claim. Can you make it a little larger? A little larger? Yeah. Still. Oh, man. Yeah. Here. I'm not used to programming like this. That's interesting. So yes, let's print the value that we're trying to claim. There we go. Okay. Let's run it. See if it still works. Claim here. So let's go to the main function. So we created our claim, index 200, and we claim that the value is, this is just a hexadecimal of the large number. We claim that our secret is 42. We print our claim start, which you can see here. And let's just, for tests, do claim.trace and see if indeed on row 200, we get the value. And yes, we get the value that is the same as this one. So we implemented our trace table correctly. So that's the trace table. Now, the second thing we need to do is we need to implement the constrained expressions. For that, we have a little genome compression expression that we import. We need to, again, have a number of rows that is a power of two. So we round it up to the next one. Here, I've prepared those two expressions that do one off the repeated constraints. So we can just create a new constraint, object from expressions. We give the size of our trace table, which is n by two. We give a seed, which in this case we're not really using. And now I have a factor where I can just enter these expressions. So does anyone remember our constraints? Here we go. The first one was the first row and the first column needs to be a constant value one. And this needs to hold for the first row. So we say home row zero. Cool. Second constraint, very similar to the first one, except that we now need to have it match for a claimed value. And we need to have it on the row that we claim. So like this. This changes itself. So this is actually i plus zero. So it means that on the row offset zero versus row offset. So this should work like this. Now we have the repeating constraint. Before you go ahead, the on row and every row functions in omega. Are those just for every table or are they specific to this example? They would work for every table. The technical thing that happens is that omega is a root of unity of size of the trace table. And the particular prime field that we have has roots of unity only of powers of two, which is why we need to have a power of two trace table length. But these three lines here, you can essentially copy based in any constraint system and they should work. Should that say trace two hundred zero? No, because if you go to the next sample, you see here that we do i and i plus one. So the i plus is sort of implicit here. In this case, we have i plus zero, i plus zero, which matches what we do here. This is like i plus zero and this is i plus zero. The number two hundred itself comes into this on row expression. That's where the two hundred is implied. It becomes a little bit more clearer with the repeating constraints. So the first one we'll do is the copy constraint. We'll do trace and here we want i plus one. So we say one here and zero and we need to subtract trace zero from up to one. So one row down and column zero minus same row column one. And we want this on every row. And then there is one more repeating constraint that we have. And in fact this is the constraint where it's all about because this is the one that actually implements the Fibonacci. The rest is sort of like boilerplate and plumbing. And this is the one that says that it is the sum of, the next row first column is the sum of the previous row both columns. Here we go. You need to make it trace one to one. You're right, you're right, yes. So next row, second column, sum of previous row both columns. Okay, let's see if it runs now. Successful Fibonacci proof in only 32 kilobytes. It gets, the proof size grows logarithmically. So for larger and larger examples it's significantly better. Now I'll show you what exactly happened here. So we're creating now an instance of this Fibonacci claim of yet with the values that we want to prove. We have our secret value that we want to hide. We create a proof by saying claim object dot proof with our secret and unwrap as a technicality here for trust error handling. It basically throws an error in anything else. Then we can convert this proof to its binary representation. We can show that it's about 32 kilobytes. And what we can also do is we can do claim dot verify and give it the proof and make sure it's okay. Print valid, which you see here on the line. What we can also do is create an incorrect claim. So let's take the same index and let's now change the value to something completely different. And if we take a wrong claim and try to verify it using this proof, it will actually print invalid. So much for the life coding part. In conclusion, so we started this OpenCKP project to experiment and learn and study different proof systems. Like I said, we're currently having only a very partial implementation. One of the things we would like to implement next from the start proof protocol is having constraints that span offsets different from one. Right now we have constraints between one row and the next. If you want to do interesting things, you really need to have offsets that are more complicated than one row and the next. You want to be able to reference multiple rows back, for example. We want to add many more examples to this. We have, for example, an implementation of the MIMC hash function. We are working on Vitalik's proposal for proof of data availability. We are working on the example from Matto Labs where they do a verifiable delay function using MIMC. So that's a lot of fun. We're working on... We're working on EVN verifier contracts so that you can not only... Like right now, we can verify or prove in Rust. Our Rust code base compiles to WebAssembly. So we already have verifiers in the browser. We also managed to verify it inside of a substrate node. So any WebAssembly-enabled 2.0 blockchains will be able to verify these proofs. One thing you notice in this language right now is that it feels really low-level. Like we're literally writing the assembly language of the constraint systems. And what you want is a higher-level language where you just write the program in more or less the Python-esque language that I showed and then it will automatically generate the logic for the trace table and the constraint system for you. That's an interesting field to explore. Performance is pretty decent but still has room for improvement. So that's something we can work on. And we're interested in implementing all sorts of crazy new zero-knowledge-proof protocols that have been released over the past couple of months. A lot of them have a meaningful equivalent that is very similar to Starks that you have transparent. You have no trusted setup. You use polynomial commitment schemes. So you can look into Aurora, Marlin, Plunk, Fractal, etc. And we're trying to... There is a nice community within the Ethereum blockchain of people researching this and our hope is that by contributing this and inviting others to contribute to us we will get an exchange of ideas and help each other move this field forward. All right. Thank you very much. So what guarantee do we have that the secret has not leaked into your proof? In this case? Yeah. Absolutely none because you can just compute human as you reversed. The other thing I've really mentioned is one of the things we don't have right now is perfect zero-knowledge. We need to implement a couple more things to get that. So right now we don't even have zero-knowledge. We just have succinctness and it seems that your proof only grows logarithmically in the size of the private neighborhood instead of like... And check the proof. So how efficient do you think you can check the proof? I mean, can I just rewrite the algorithm and check it out that way? No, it's more efficient. So it's logarithmic in the original. Logarithmic in the number of steps. Yes. Yeah. What do you think in here? For example, you have like this seed string somewhere in there. What was that for? Yes. So what you want is to initialize the proof with the shared information between the proof and the verifier. So if I'm creating a proof and I'm giving it to you, it will improve security if all our knowledge we have in common was encoded in there. So what you could do is take a hash of your claim, put it in there, and then the proof is more constrained cryptographically to be for that particular public input. So it adds to security by committing you to this particular instance of the problem. I'll get back to my original question. Sure. So, supported to people, actually, it's something else. Yes. So, again, what guarantee do you think will provide that the secret is not leaked? So, supported to people, sometimes it's not necessary, but they can get the input from the output and the other input. So there's two things to this question. The first is... I mean, what should I trust your proof basically that doesn't leak to my secret? Yes, it's okay. So the guarantee that you get is that the verifier will not be able to learn anything more than can be learned from the public input. And that is a much, much more proof of paper by somebody else? Absolutely, yes. That is a proof done by Ali Ben-Sassan at all. I know. But what if you, in your proof, you simply put on the input, you know, that we did it for somebody else? Yes, which is essentially what happens in the Fibonacci example, because, you know, the output is just defined by the secret value, so you can just compute it backwards. It's not hard, algebraically, to do this. What you could also do is just, like, iterate through all the values if you know that there's only a small set of them. And this is why it's really important to know what the exact proof is. And the exact thing is you cannot learn anything more than you can already learn from the claim itself. If the claim contains your private input or something that allows you to derive the private input, then you have no zero-knowledge guarantees. So this is something to keep in mind if you want to use these things for actual zero-knowledge and privacy. Be careful about what you put in the public input, because you might be able to derive the private input from this if you're not careful. Another big remark I need to make is that right now we don't have perfect zero-knowledge. We only have succinctness in the system that we have. So, like, the system we have is not suitable at the moment for any hard privacy requirements. But again, contributions are welcome. So something that you could maybe prove with this is that given the output being a hash function you can verify that there was an input that did generate that hash. Yeah, actually we have an example of a MIMC hash pre-image. You can find it in the repo. I'm out of time.