RISC Zero Security Deep Dive: Architecture, Risks, and Review Methodology

DeFi Security Summit at Devconnect Argentina 2025 · November 2025 · 19:47

Kirk Baird, executive director at Sigma Prime, goes through what RISC Zero is, how it works and what a security engineer should look for when reviewing code that runs on it. RISC Zero is a zero-knowledge virtual machine that proves program execution, and the talk covers its architecture, the risks it introduces and a methodology for reviewing it.

Speakers

Topics

From DeFi Security Summit at Devconnect Argentina 2025, hosted by DeFi Security Summit.

Transcript

Beautiful. Thanks, Pablo. This is the last talk of the session, then we can all go out for a little sweet treat or some little snack in the break. Um, so first of all, uh, you guys heard Jeffrey earlier on in the session. Uh, this quote is from him, but he was quoting someone else. And so I have absolutely no idea who originally said it, but if crypto goes to zero, at least it gave us zero knowledge proofs. Uh, zero knowledge proofs are great. Uh they're they're absolutely unreal technology and um risk zero is what brings us to everyday people without needing to do maths. Uh if the last talk was too math heavy for you all uh I'm sorry it was great. Uh it was the right amount of maths but this talk won't have any maths at all. So you'll feel relieved.

Now uh what is risk zero? Uh how does it work? That's what we're going to be looking into. But really going to focus on applications as in people who have written code for riser rather than diving really deeply into the riser VM. So the type of architecture and the theory that we're going to be looking at is what people need to review and write uh ris. Second part will be a coded example. Run you through uh how bad I am at a developer and how many bugs I can write. You can hopefully spot them all out pretty quickly and then we'll go through them together. And then at the end we're going to be running through all the security tips and tricks and everything else you need to know. So theory basic risk zero architecture. Um what are we actually doing? The risk uh the virtual

machines any sort of ZK virtual machine is going to be proving that a certain part of execution happens. And think of it as a smart contract which is going to be a really good analogy through this thing. you can prove that the smart contract executed and there is an added benefit that any of the function parameters the inputs can be private and can be hidden. Um why is this so good? The the biggest why is actually the compression cuz we can do an entire program like huge I'm talking about hundreds of thousands of lines and we can output that into a single verify line which is a little bit of maths BLS and a few other things under the hood and can run very comfortably on a smart contract in about 100,000k gas 100,000

gas. Nice. So how do we actually do this? Uh what you got to do you write code and the best thing about riso it's in Rust. Uh we all know and love Rust. Well, most of us do. It's an excellent smart contracting language. Um and what else you could do is you can compile it into VM code. Uh they use ELF and risk 5 for for this. And with that code, you could send it over to a prover. Prover runs it and gives you output this little receipt. So I'm kind of going to break these into like two main types. Uh we got those that are general purpose which is riser written rust happy days uh you can write your code in rust then we got the other ones like domain specific languages um

that's cyros VM Aztec uh miden and aloe they all use domain specific uh zkms which uh the trade-offs here are that you have to learn a specific language for example you need to learn cyro to write on starknet and circuit-based languages they're challenging They're they're actually really hard to write good code. You need to know a little bit more about the maths. You need to know about finite fields. And you really got to be strict on writing circuits and constraints or else you're going to put some bugs in your system. That does not happen in Ris or SP1. You write Rust and some other really smart guy has gone and done all the circuits for you. So it's happy days. Now uh let's get into the architecture of provers versus

verifiers. So provers are the people who are generating proofs. Oh wow, what a surprise. Uh well named actually thank you. Uh these run the programs. They need to have full access to all the inputs. That includes the private inputs. If if you need private inputs, your prover will know what they are. Um and then they can be split. The proof gets split into two components. The guest code which is actually what's proven and then the host. And of course they output a receipt which is a proof uh mathematical proof. Small little thing. The verify, on the other hand, all they do is check receipts. Yeah.

Yeah, I know. It's your accountant. Uh, all they're doing there is running through a receipt, making sure they work. They do not actually execute any of their code. The code is assumed to be correct if the proof is correct. Uh, real important here, they should never trust the prover. Uh, if you're you're relying on the prover and any sort of thing from the verify, then you're probably going to have a very centralized uh broken system. Cool. So let's look at this nice pretty diagram. Uh host and guest they're inside the prover uh yellow and then we got the verifier in blue that does the maths. The host is a normal program. It has access to the internet. It has access to your database, the real world,

whatever you want it to do. It's just a regular old program and it can call the guest. Uh the main function of the host is to prepare all of the variables that need to be given to the guest and then the guest is the code that actually gets proven and this is taking inputs. It's think about the EVM. It doesn't have any access to the outside world when you're in a smart contract. That's the same as a guest code. All it has access to is what the proof gives us. Uh sorry, the host gives us. Nice. And then the guest outputs a little receipt. So uh some of the things that pop up a lot in this space are receipt, journal and image ID.

The receipt is the proof. I've already told you about four or five times. Uh the journal is uh what we call the public outputs. So as I said, uh the guest can have some private inputs. It can also have some public outputs. So these are things that it really wants to share for the real world. And we'll find out very shortly why that's helpful. Next up is the image ID. The image ID is unique to every single wrist zero program. Why do we need this? Uh because if you write a program, I write a program and some hacker over there writes a program, we want to know if the receipt is verified for my program or it's for your program or it's for his program over there. Um so if you don't check the image ID, someone is going to

make a fake program and and wreck you real quick. Cool. So jump into it. Little bit of code. Fun fun. Um, these are the main functions that you'll use in the risk zero crate. Like this, this is literally all you really need to know to audit risk zero code. These six little pieces of information. Super easy. First one, entry point. That is just defining where you start your guest code. All right. Uh, yeah, guest code starts at a main function. What a surprise. Nice spot. Next up, we have the read. This is what I talked about. Private inputs only. Uh, EVM commit.

This is putting to the journal. Anything you put in that commit is going to be public for the world to see it when they receive your receipt because it's written to the journal. Uh those top three are all for the guests. Next up, this builder uh executor environment. This is what we actually use to uh build our guest programs and so that the prover next up can call prove over the environment. And then finally, the very last point is the verifier. All they really do is check this proof. Um, under the hood is going to be some nice maths.

Woohoo. Made it to the coded example. This is where things actually get interesting. Um, this is what we're trying to do. Prove an Ethereum address has more than one ETH. Uh, little bit of criteria. Fetch it via an RPC. Check that it is my address and the verifier has a trusted block. So, let's start off with this. This is full of bugs. uh if you write this, I'm going to be really mad at you and and give you about four or five critical bugs. But let's look what it does. The first uh piece of code that actually executes is going to be the host.

Sequentially, the host is always first because they need to gather all of the input variables that we put into the guest. And as you can see here, uh first thing they do is they do this RPC call for some address and they get the balance. Next up, they write that balance into the environment, hit the build, and then the prover can prove. That prover.prove step is when the guest code starts executing. So, it will specifically happen after the host, which makes it a little bit hard for the host because the host needs to get all of the environment variables in order before they actually call the guest. And then finally after the guess is finished executing it's going to return a receipt and that receipt can be verified by the

var uh the verifier. So what is the first bug that we we see here? Uh the first bug is that the RPC call is done in the host and that's where we check the address to the balance match. This is not done in the guest and therefore the verifier cannot verify in any way which address this balance is for. This d this balance was meant to be for my address, but I could use italics because I know he's got the bags. So, let's touch that up. Let's also write the address in here. As you can see, that second line, the host is now uh putting the right of the address. The guest now reads uh the address in the same order. The address came first and

then the balance. And it can now commit the address to the journal. The verifier is able to extract the address out of the receipt and now it can do two things. The verifier will now check the address and it will also verify the receipt. So we are using the public journal here to do things that we cannot do in the guest. Um the guest couldn't check this address. That could only be done in the verifier. So they put it to the journal and said all right verify this is your job. I don't know how to verify this thing. You verify it. However, there's still more bugs in here. Is anyone seeing some bugs in the guest code?

>> Uh, in in the guest code, >> the the host has access to the internet. The host is just a regular Rust program that's that's allowed to see anything. So, it's it's allowed to hit this RPC. >> Yeah. Yeah. We we we don't have any checks between the balance and the address. The address just comes in and there's balance there. So next up, we need to do a proof inside the guest program that ties those two together. Bam. So to do that, we actually need to do a bunch more things. Uh we need to get a balance and a proof from the RPC.

Uh let's let's just assume that happens nicely. And then we need to pass all those things into the guest. To do a proof, uh you you need quite a few things. Uh you need the balance, you need the address, you need the block hash, and you need some sparse Merkel tree proof. If you've ever looked into Ethereum and how how to prove an accounts balance before, it's it's just basically a giant Merkel tree. A little bit more complicated, but really not that much. Um, so inside the guest again, we read everything in the same order because or else our program won't work. And then we do verify account proof. We do that whole Merkel tree. And now we have very happily tied the address to the balance. So those two are tied together. Happy days. We can commit

the address. And now the verifier is like, okay, thank you. That that address does have that balance. We're all good. Now, this this is looking pretty tidy code at this stage. However, there's still one very subtle bug. Uh the subtle bug is that block hash. Uh we have absolutely no way to tell if that block hash is real. the guest the the host could have lied and just put some arbitrary block hash from FC classic any other chain they wanted to and this proof would have gone through and like yeah yeah that's that's what the account balance is for this block hash. So what we need to do bam is actually commit that block hash at the end of the guest because this is something that the guest

cannot easily verify. How does a guest which is just a small little smart contract know if a block hash is valid? It's the tip of the chain. and it's the canonical chain. It hasn't been forked. These things are all extremely complicated to do, but for a verifier, um, maybe they could do them quite easily with a light client proof, something like that, finality proof, something along those lines. Happy days. Uh, the verifier has done this. And now we have the most secure ever balance check. And so to highlight what the benefits actually are, as you can see, this this guest code uh takes in a lot of inputs and it also does a very big account proof which is going to be a whole Merkel proof. And

the verifier on the other hand is just what like four lines and some little BLS logic in there. That's the compression. That's the main reason that we use uh onchain proofs is because you're able to take a really big guess code and put it into a tiny little line in the verifier. So if you think about it in the context of an L2 uh ZK roll-ups, ZK rollups, the actual node implementation is massive, hundreds of thousands of lines, their guess code would be absurdly big. However, onchain they can just have a verifier that's just a handful of lines that does that. So this is the main gains uh that we get to see. The other gains are if you want to optionally hide something like I don't know the claim

balance amount or something like that, you can have some private inputs as well. Cool. Last section almost for time for a sweet treat. All right, so the host rehashing untrusted by everyone else. No one should trust the host. Uh it must put everything in the same order and it reads it. The main security bugs that we're going to see in the host are chain halts. It's going to be DOSS vectors. Uh there's going to be things like panics, error handling, what happens if the guest code is fail, writing things to the guest in the wrong order, which is just going to cause everything to fail and revert hopefully.

And then in the guest code, it's really about input validation here. Uh I've called it under constraint circuits. That's a little bit of a bastardization. I'm sorry. Uh it's just input validation. Any input that's not happening in the guest code must happen in the verifier or it doesn't happen at all. Write anything to the public journal that you can't verify. So the verifier can do it. Uh the verifier has three important things. It must read the receipt. It must validate the journal entries. And it must check the image ID. Uh it verifies anything the guest can't.

So if I go back to this line, you could see that in the receipt. There's balance ID. This is the image ID. I've set it as a constant as you could tell by the big writing and it's fixed. If that image ID was passed in by attacker, they could actually make a proof for any program at all. So, it's really important to do a lot of verification of your image ID as and so it should be what you need it to be. All right, tips and tricks. Guest code is where most of the big scary critical vulnerabilities you got to find. Guess code is where the juicy things are because that's what the maths is actually covering. Uh, and in risk zero, it is a 32-bit OS. And don't forget, if you're in Rust, everything is going to wrap by default arithmetically

when you have that. So, you really got to look out for 32-bit. Uh, 32 bits is only 4 billion, by the way. 2 to the 32. It's tiny. You can brute force that. You can do anything you want to it. Turn on overflow checks equals true on your cargo. Toml because panicking in the guest code is happy days. Uh, I'm not sure if many of you have looked at Salana or near similar principle. Panicking is a valid form of error handling only in the guest. If your host panics, well, you're going to get a chain hole. Um, next up is library dependencies. If you're in a guest code, as we said, you don't have access to a lot of things. That includes randomness, files, all of that things. And although you might not have written it yourself, if you use a third party dependency, they probably have. And that includes

things like threading. A lot of cryptographic libraries use threading to make the cryptography runs faster. And if you don't realize they're threadling threading, you're going to get probably compile time errors when you uh try and compile your guess code. And you'll be like, why is this library not working? Oh, sad face. Cool. Resource exhaustion. Uh the prover is running the guest code. The provers's a lot of the times not going to be you. There's going to be some beefy machine. Proving is really expensive. You need big beefy machines and they need to do a lot of work. So if you have any sort of unbounded loops, really unrestricted string lengths, arbitrary uh data inputs that use a lot of resource exhaustion, a lot of resources, it's going to be super

expensive for whoever's running the prover, which is probably not you. Unconstrained circuits, we've already talked on this. This is input validation. Uh make sure you do everything. And then again, anything that can't be validated inside the guest code, we send to the verifier. All right, this is your checklist. Uh, this has pretty much everything you need to know to review a riser piece of code application. Start out at the top. Critical logic all inside the guest code. Um, we've covered this. All inputs validated inside the guest code or written the journal. No private information written to the journal. Don't want to leak secrets. 32 bits. Never forget 32 bits.

Computer is expensive from user input. Uh, the verifier has those three main points. We talked about them recently. And finally, the host must write to things in the same order as the guest. I'm going to highlight this one for you. Um, right here we we have four pieces of information that we're writing. But if you have um say 100,000 lines of code, how do you actually get your host to write all 100,000 plus input variables in the same order that the guest's going to read them? Uh, in Rust, you're going to do this by generics. Uh the the best way to do this is to have one generic and you actually run the host code exactly the same as the guest code, but the host code

generic will write things into the environment variable and the guest code will read from the environment variable. So it's like a a little bit of a switch that happens at build time and that's pretty much the only way you can do it for large projects. Cool. All right. Uh handle the case when the prover fails. When the prover fails, you're going to get an error. uh in your host code which is running the prover uh that's fine. We can handle the error case uh errors for a whole b bunch of reasons but you don't want to panic. DOS vectors error handling redundancy and proofs are inherently replayable. Uh that means one proof if it's valid once it's going to be valid forever. There's no nons checks or anything like that.

That's all up to the verifier to handle if they want to do uh replay protection. Cool. There are some good resources. Uh if you want to see these slides, there's Google Docs. Some other sources uh there's a blog that I've written not too long ago on SP1. It's essentially covers a lot of the similar things from Riser, but it's on SP1. Um ne next up is the rare skills boot camp. Uh if you heard Jeffrey talking earlier, it was at the start of this session. Uh he's a great educator in the space. Uh he runs a boot camp on all of zero knowledge. It's an excellent entry point. There's also a ZK book if you don't want to sign up for a boot camp and you'd rather do it on your own time. Uh the book is free as well.

Lovely. Uh siphon updraft courses. They've got some great zero knowledge stuff in there. Uh lastly, if you want to hit me up on Twitter, I'll probably respond to you probably. Cool. >> Thank you, Kirk. [applause]