 Okay, as was previously mentioned, I'm John. I'm also the CTO and a co-founder of Veridice. And so today I just want to ask you a question. Are your zero-knowledge proofs correct? Why don't we find out? So just a little bit about Veridice. All of us come from an academia background. So on the slide right here you can see some of the main members from Veridice. So on the top those are the co-founders including myself. And on the bottom you can see some of the other people that we have here at DevCon. So if you have any questions please come and find us, ask us questions. They're also right there in the third row. But yeah. So just a little bit more about us. We are a security company. So we provide audits and we also provide tooling in order to find vulnerabilities in all aspects of the blockchain. And so basically we build tools to help ourselves and other people find bugs in DeFi applications, blockchains and what we're going to be talking about today, zero-knowledge circuits. And yeah we're very interested in making our tooling available. And so today I'm going to talk about specifically our open-source tooling and not about the tooling that we're providing as in our security as a service suite. So to begin with I think it would be useful to talk about what sort of bugs we actually see in zero-knowledge circuits. And so the reason why we want to do this is because these circuits are extremely prevalent especially in L2 blockchains. And so that means that you can see these circuits integrated into smart contracts themselves like Tornado Cache and Sim4 and blockchains. And also there are a number of libraries for circuits as well like the Circumlib. And so one of the interesting things that we've seen is these circuits present new challenges especially for you know people who are interested in security. And the reason why we want to do this is because we've seen a number of exploits in all aspects of the blockchain from smart contracts to the blockchain itself. And these zero-knowledge circuits are a relatively new domain. And so while we haven't seen very many hacks in the circuits themselves other than luckily the ones that Tornado Cache found before it was exploited, we want to make sure that people are thinking about the security of these circuits as they develop them. And so the way that we want to do this is by, or for Veridice, we want to provide tools. And so I think that the best way to start is to talk about some general types of bugs that we see. And to do this I need to kind of establish like the model of the zero-knowledge circuit that we're assuming here just because these circuits can be fairly complex. And so here's a simplified model of the circuit. So basically what happens is there's going to be a witness generator. This could be a blockchain or a smart contract or something like that. And it's going to take in a set of public and private inputs. It's going to generate a witness. Then this witness is going to be provided to the prover along with some additional information which will generate a proof. And then finally that will go to the verifier and then the verifier is going to say, yeah it's correct or no it's not. And so when someone wants to create one of these circuits there's a couple of ways that they can do so. And the simplest model they simply write a single application. So this would be their circuit application written in something like CIRCON or Cairo. And then that will generate the witness. So your smart contract for example. And it will also generate a set of finite field constraints which is later used to generate the prover and the verifier. And so the interesting thing that we see here is you have a single application which is used to generate two different or two different applications which do slightly different things and more interestingly they can be out of sync. And so one type of bug we see here obviously is if someone doesn't implement their circuit correctly. So if that happens then we have a functional correctness violation and it basically just means you know someone needs to go back and rewrite their circuit in a way that's correct. The more interesting bugs though are when the witness and the constraints are not in sync with each other. And so this happens because generally these languages provide ways for you to put specific logic in your witness and put specific constraints in your finite field constraints. And so if it turns out that your constraints are less permissive than your generator you could reject valid interactions with the application. So for example if you have a blockchain it's possible that in this case you could have an dial of service where the blockchain wouldn't be able to process transactions which you know wouldn't be great. The worst case or the more dangerous case though and I should say under constrained is an under constrained circuit. So what happens with an under constrained circuit is the constraints are more permissive than the witness itself. And the reason why this is more dangerous is because while the witness might be correct and it might provide all of the interactions you want and they're all accepted by the constraints everything seems great. However an attacker could then come in and they could provide an alternative witness which might be used in order to get some alternative transaction or logic accepted by your constraints and then proven. And this would necessarily be bad because let's say one of the constraints which didn't end up in your finite field constraints was equivalent to the sender is equal to the admin. Right? Obviously if someone was able to bypass that then bad things could really happen. And so the way that we deal with this in Veridice is by using a field in computer science called formal methods. And so formal methods provide a way of automatically reasoning about software. And so it does this by understanding and understanding the semantics of the underlying program and iterating over that program in order to determine information about it. And so basically what we're going to do is we're going to use formal methods in order to find bugs and create proofs that the software is correct. And so within formal methods there's basically three different fields. So there's automated testing. So if those of you or for those of you who have heard about fuzzing that falls into this category. And so basically in that case someone is just going to run the program on a bunch of inputs. And so this is useful for finding bugs. Next we have static analysis. We'll see a static analyzer in this talk which is going to analyze your source code for a particular class of bugs. And the important thing about these static analyzers is if they can't find the bug, it has proven that the bug does not exist in your program. However, the more interesting case is if it does find a bug. And then last we have formal verification. Some of you might have heard about this. There are multiple ways to perform formal verification but at Veridice we're particularly interested in automated formal verification. And so basically what this is going to do is it's going to analyze your software and make sure that a specification, where a specification is essentially a way of expressing the intended behavior of your application, it'll make sure that that specification holds for the application. And so that means that after using a formal verifier, one, you can verify custom correctness properties. And two, you will have proven that that property holds or that your implementation is correct. And so as we go across the spectrum, it takes more effort in order to both build and run the tool but it also provides much stronger guarantees. And so we're going to be looking at some of the open source tools provided by Veridice that fall into these categories. And so first we're going to talk about a static analyzer which performs common vulnerability detection on CHIRO or SIRCOM programs. So basically what I'm going to describe, this is the type of bug that is found by a static analyzer that we created called PICUS. And so basically what it's going to look for is uniqueness bugs. And so what a uniqueness bug is is it happens or it occurs when a constraint allows a single input to map to multiple outputs. And so what I mean by this is if you look at this SIRCOM program over here, you don't have to understand what it does. But basically there's a bug that's circled in red. The reason why that's circled is because it constrains all of the outputs except for one very specific output. So Decoder is supposed to simply zero out all of the bits except for a single bit that is specified by imp. And so what happens here is if you look over on the right, you'll see that the constraint specifies that that multiplication has to be equal to zero. There's two ways that it could be equal to zero. It could be equal to zero because the output is equal to zero. Or it could be the case that imp minus i is equal to zero. And so all of the cases where imp and i are not equal to each other are constrained. But in the case where imp is equal to i, we find an unconstrained bit. And so what does this mean? This means that an attacker gets to decide what the value of out at imp is. So it could be the desired value, which is one, or it could be zero. And so why does this matter? Well, because of the reason that I told you about earlier, it means that depending on how this circuit is used, someone might be able to leverage this uncertainty in order to violate some assumptions that you have made and hack your protocol. So what can we do about this? Well, I already told you that we're going to be using static analysis. And so there's two types of ways that we can solve this problem. One is we can just perform straight static analysis. And so what this is going to do is basically we'll just look at the inputs and outputs, and we'll say that if the output is a linear combination of the inputs, then it must be constrained. So that works. However, it also produces a lot of false positives, which is undesirable because then you have to go and you have to determine is this real or is this not? So instead, you could use something called an SMT solver. So what an SMT solver is, is it's simply a program that can solve mathematical formulas. And so because you have constraints, you can encode this mathematical formula that states whether or not a particular output is constrained. The problem is normally there are many constraints. And so if you tried to send that to an SMT solver, it would quickly become, it would quickly bog down and it most likely would not be able to solve uniqueness for large circuits. So basically, we see that static analysis is scalable, but not precise. And we see that the SMT solver is precise, but not scalable. And so what we have done is we've developed PICUS. And so PICUS is going to take in your constraints, and it's going to output one of three things. So it's going to output, yes, this is, this particular circuit is entirely constrained. No, it's under constrained, or I don't know. But the way that it does this is by using a combination of static analysis and SMT. And so what I mean by that is we're going to have two phases that are going to iterate. And so basically in the first phase, what we're going to do is we're going to take those constraints and a set of signals which we've already proven are constrained, and then we're going to compute a new set of constrained signals. And so this will give us a set of constrained signals that we already know are constrained. And so if we end up finding that all of the output signals are constrained, great, we're done. Otherwise, if there are some output signals that we haven't been able to determine if they're constrained, we can go to the next step. And so the next step is the SMT phase. So what happens in the SMT phase is we take the signals which we have already proven are constrained, and then we again take the set of constraints, and we use an SMT solver in order to try to find additional output signals that are constrained. And so if we happen to find that all of the output signals are constrained again, great, we've solved a problem. If we find an output signal that is unconstrained, because we can prove that in this case, then we say, all right, you have an under-constrained signal and it's specifically this one. And then otherwise, if the SMT solver fails for some reason, so basically that happens if k'' is equal to k, then that means that we've basically stalled and we'll instead just return, I'm not sure. So I mean, the last case is if we ended up making progress, then the important part is we can go back to the static analysis case, because we have now proved more cases are constrained. And so that means that the static analysis phase might be able to make further progress. And so basically we'll do this in a loop. And we find that in this case, Pycus is able to scale much better than the previous attempts that we saw. So basically we evaluated this on a set of circuits and Pycus was able to solve, I believe that comes out to 98% of the circuits that we threw it at. Whereas if we look at static analysis and SMT, static analysis returned a bunch of false positives and SMT timed out on a lot of the benchmarks. And so this is the first tool that we look at. So the important part is you provided a circuit and then Pycus does all of the work for you. You don't have to provide it any additional inputs, and it will give you a list of possible problems. The next thing that we're going to look at is a verifier. So this performs automated verification. So what I'm going to talk about next is a tool called Mejai. So what Mejai does is it performs automated verification on Cairo programs. And so the way that it works is you will provide Mejai with your source code and a specification. And as I stated before, the specification just specifies the intended behavior of your Cairo contract. And then Mejai will spit out either yes or, more interestingly, it will spit out no and it will tell you why. And what I mean by why is it will provide you concrete evidence of why your specification does not hold. And so the way that it does this is it's going to take your specification, your program. It's first going to translate it into a symbolic representation. And so what I mean by a symbolic representation is rather than having, you can think of a symbolic representation as being unconstrained. So basically any value in your program could map to any other value in your program. And so then once it does that, it's just going to run this through, it's going to run your program symbolically. And I'll show what that means in a little bit. And more importantly, it's going to interact with an SMT solver in order to generate proofs that your any, or as it runs your symbolic program, it's going to use the SMT solver to prove that your behaviors are consistent with the specification. So let me get a little bit more concrete about this. In order to do this, you need a little bit of information about symbolic execution. So basically the way symbolic execution works is looking at the very top, you can see just a set of variables. And so we start with these variables being mapped to any possible value. So unlike testing where you say like in this case u is equal to five, we say u is equal to some symbolic value. So it's going to map to any value within the range. And then when we symbolically execute the program, what we're going to do is we're going to essentially just run through your code and generate case statements. So for those of you who remember your proofs class, basically we do case splits whenever we come to conditionals. And so here what you can see is starting at the very top, we have everything unconstrained. But then once we enter that first if statement that you see, you'll see on the left hand side that u is equal to five. But then we also need to consider what happens if you don't enter that if statement. And in that case, u is still some unconstrained value. And the important part is at the very end of this, after executing the entire function, we end up getting a set of symbolic equations, which we can then send to an SMT solver to make sure that it's consistent with your specification. So some of you may have noticed that it seems like there's a fairly large spread. So if we look back at this, you kind of get this tree-like structure with a lot of breadth. And so in order to make this actually scale, what we do is we have to perform a basically intelligent merging. So essentially what you can consider is after we execute an if statement, once control flow joins back, we can also join those states. And so if you don't understand what this means, that that's OK. Basically, all I'm saying here is that in order to improve the scalability of this tool, we push some of the work off onto the SMT solver. And we, you basically have to balance the work that your symbolic execution engine is doing and the work that the SMT solver is doing. So let's look at an example of how this actually works. So here you can see the move function in MakerDAO's Cairo implementation. And so the specification that we're trying to check is basically that move transfers funds correctly. And so the specification that you see on the right-hand side basically says that after the move function has executed, the die balance of the destination should be greater than or equal to the die balance at the beginning. And then the die balance of the source should be decreased by an appropriate amount. And so the interesting thing here is Magi reports that this does not, that this is not sound, this specification does not hold for the code on the right-hand side. And specifically, it says that it does not hold when rad, which is, you know, the amount that's being transferred, is equal to some specific value. And so the reason why it reports this is because in Cairo you have this interesting case where because of the size of the large primes, they are going to be less than the size of a Uint256. And so Cairo ends up splitting your Uint256 into two parts, the lower 128 bits and the upper 128 bits. And so importantly, the reason why Magi reports there's a violation is because the MakerDAO developers here forgot to constrain the value of rad. And so the way that you do this is in Cairo they provide a specific function in order to make sure a Uint256 is properly constrained. And if you leave that out, that means that your Uint256 is not well formed. And so once we add that check back in and then we send this back to Magi, Magi will report, yeah, everything's right. The specification holds. And so in this case we have properly guaranteed that this move function transfers funds correctly. And so this was just a very simple case. However, we've been working with the MakerDAO people in order to verify their Cairo implementation of their protocol. And so as we've done this, we've ended up finding, I think, at least one bug that the MakerDAO people have merged into their implementation. So that's all I have for you today. If you're interested in learning more or seeing demos of these tools in about an hour or so at three o'clock, we will be down at the ZK Community Hub just giving a demo of these tools and a couple of other ones. Also, on the left and right you can find links to the repositories for Pykis and Magi. So if you're interested in trying them out, please do. And there's also a Twitter link. So if you want to follow us on Twitter, that would be fantastic. Hi, thanks for your talk. Just a question. When you're talking about these Cairo contracts and could you say, if I didn't have access to the source code and I just had this circuit, how easy is it in practice to exploit these arithmetic errors or constraints? So if you're an attacker and you don't have access to the source code, how easy is it to figure this out? Yeah, that's a good question. Attackers are very good at looking at low level code and figuring out how to exploit it. So I would say that if you were trying to get security through obscurity, it's really not going to work for you here because generally your code is going to be public somehow. And so if someone is able to gain access to even the low level implementation of your code, then they could possibly figure out how to exploit it. And on top of that, we've seen cases where even if someone doesn't have access to your code but they're able to query it, they can also find potential exploits. I want to know what kind of bugs or issues cannot be found by those two tools. So what kind of bugs cannot be found by these two tools? Okay, so for Pykis, because it's not performing functional correctness checking, it can only find under constrained bugs. And so that means that it won't be able to find like if your program is written in correctly. For Magi, because it is verifying functional correctness, it relies on the quality of your specification. However, if we assume that you have specified the behavior of your program properly, it should be able to find any potential bug in your program. So for example, one thing that we see is if you write a specification that says, let's say the die is transferred correctly, but then it turns out that due to an under constrained bug or an under constrained bug or a uniqueness bug, it doesn't end up being transferred correctly. Magi will point that out. However, it depends on the quality of your specification. And yeah, it also won't find bugs in anything other than your Cairo contract. So if you perform interactions, let's say with other applications that Magi doesn't have access to their source code, Magi won't necessarily be able to check and make sure that everything is correct. It has to make simplifying assumptions basically.