Part 3 covered the cryptographic foundations: SNARKs and STARKs, their properties, and the mathematical constraints that make them work. Part 4 surveyed what people are actually building: privacy protocols, rollups, bridges, identity systems, and the expanding category of verifiable computation.
Now we need to understand how these pieces connect. You know the cryptographic primitives, you know the applications, but how does a developer actually use ZK proofs to build something? How do you take a program you want to prove and turn it into something that SNARKs or STARKs can work with?
This is where zkVMs come in. Understanding how they work, and more importantly understanding the choices different teams made when building them, helps clarify why the ZK landscape looks the way it does. The architectural decisions determine what’s practically possible and where the performance boundaries are.
The Problem: Cryptography Isn’t Made for Developers
Here’s the issue. In Part 3, we covered SNARKs and STARKs from the outside: their properties, their trade-offs, when to use which one. But we didn’t talk much about what they’re actually doing under the hood when they verify a proof.
At their core, proof systems work by checking mathematical relationships. Think back to school algebra: if I tell you “x + 5 = 12,” you can verify I’m telling the truth by checking whether x really equals 7. The equation is a constraint that must hold.
Proof systems work similarly, but with much more complex math. When you generate a ZK proof, you’re essentially saying “these values satisfy this set of mathematical equations.” The proof is cryptographic evidence that you know values that make all the equations work out. The verifier doesn’t see your actual values (that’s the zero knowledge part), but they can check that the equations are satisfied.
For example, say you want to prove you know three numbers a, b, and c where a × b + c = 100, without revealing what a, b, or c actually are.
In a proof system, you don’t just claim this is true. You break down the calculation into steps and prove each step:
Step 1: You multiply a times b. Call the answer “intermediate” (just a temporary result you’re tracking).
Step 2: You add c to that intermediate result. Call this “result.”
Step 3: You check that result equals 100.
Now here’s the key part: the proof system lets you prove that you performed these steps correctly with some actual numbers, without revealing what those numbers were. The verifier sees “yes, someone did this calculation correctly and got 100” but never sees your values for a, b, c, or even that intermediate result.
This is what we mean by “proof systems work with mathematical constraints.” Every step of a calculation becomes a constraint that must be satisfied. String enough constraints together, and you can prove arbitrarily complex computations were done correctly.
But developers don’t write code in polynomial equations, they write programs in languages like Rust, C, JavaScript, Solidity. These programs do normal things: add numbers, compare values, read from memory, branch based on conditions, call functions.
There’s a huge gap between “I have a Rust program that processes blockchain data” and “I have a set of polynomial constraints that a SNARK can verify.” Somehow you need to bridge that gap.
The early approach was to close this gap manually. If you wanted to prove something with ZK, you wrote it directly as an arithmetic circuit like the one we just went through. Let’s illustrate what that actually means with a concrete example.
What Writing Circuits Actually Looks Like
Let’s see what writing that circuit we just discussed actually looks like compared to normal code.
In a normal programming language, you’d write:
result = a * b + c
Simple, direct, one line. But in an arithmetic circuit for a ZK proof system, you need to break it down into those explicit constraint steps:
constraint 1: intermediate = a * b
constraint 2: result = intermediate + c
constraint 3: result = 100
Each constraint is a mathematical equation that must hold for the proof to verify. The circuit is literally a graph of these mathematical relationships, where values flow through operations that the proof system knows how to handle.
For this simple example, the difference seems manageable. But real applications need to do much more complex things. Want to check if two values are equal? There’s no “equals” operator in arithmetic circuits. You have to subtract one from the other and verify the result is zero using additional constraints. Want conditional logic (if this then that)? You can’t have branching in a circuit. You need to include both possible paths and use constraint tricks to ensure only the correct path executes.
Want to read from memory? Hash something? Do bitwise operations? Every single operation requires careful translation into arithmetic constraints. And once you compile the circuit, the logic is fixed. You can’t change control flow at runtime.
Writing circuits requires specialized knowledge of finite field arithmetic, constraint optimization, and the specific quirks of whichever proof system you’re using. Most developers don’t have this background. More importantly, most developers shouldn’t need to learn an entirely new computational model just to use ZK proofs.
The circuit approach works for specialized applications where you can invest the time to handcraft optimized constraints. But it doesn’t scale to general-purpose proving.
The Solution: Virtual Machines as an Abstraction Layer
This is where zkVMs change the game.
A zkVM is a proving system built around a virtual machine architecture. Instead of writing circuits directly, you write normal programs in familiar languages. The zkVM handles all the circuit translation automatically.
Think of it like a compiler. When you write C code, you don’t write assembly instructions by hand. The compiler translates your high-level code into machine instructions for whatever processor you’re targeting. Similarly, when you write a program for a zkVM, you don’t write constraints by hand. The zkVM translates your program into the constraints that a proof system needs.
Another parallel is how AI development evolved. In the early days of deep learning, building neural networks required writing CUDA code, which is low-level GPU programming. Only developers with specialized expertise in GPU architecture could build AI applications. Then frameworks like TensorFlow and PyTorch emerged. These tools abstracted away the GPU complexity. Developers could write in Python, describe their neural network architecture in high-level terms, and the framework handled all the GPU operations automatically. This abstraction enabled mass adoption. Suddenly millions of developers could build AI applications without understanding the underlying hardware.
zkVMs are following the same pattern for zero knowledge proofs. The circuits are like CUDA code: powerful but requiring specialized expertise. zkVMs are like TensorFlow: they let you work at a higher level while handling the complex stuff automatically. Write normal code, get proofs. The abstraction makes ZK accessible.
How do zkVMs actually achieve this abstraction? Most use standard instruction sets like RISC-V. If you’re not familiar with instruction sets, here’s the quick version: your high-level code (Rust, C, etc.) compiles down to a standardized set of basic operations that a processor understands. Things like “add these two numbers,” “load this value from memory,” “jump to this instruction if this condition is true.” RISC-V is just a particular specification for what those basic operations look like and how they work.
The beauty of using a standard instruction set is that all the existing compiler tooling just works. Your Rust compiler already knows how to generate RISC-V instructions. The zkVM then takes those instructions and proves they executed correctly.
You write code, the zkVM makes it provable, and you don’t touch any circuits.
How zkVMs Actually Turn Code Into Proofs
So what actually happens when you run a program in a zkVM? There are several distinct steps, and understanding them helps clarify what different zkVM architectures are optimizing for.
Step 1: Execution
First, the zkVM just runs your program normally. It processes each RISC-V instruction in sequence, maintains registers, manages memory, handles function calls, exactly like a regular processor would. This execution phase is completely deterministic. If you run the same program with the same inputs, you’ll get the same outputs and the same sequence of operations.
Step 2: Trace Generation
As the program executes, the zkVM is also recording everything that happens. Every instruction that runs. Every value that gets read from or written to memory. Every register that gets updated. This complete record is called an execution trace.
The trace is literally a step-by-step log of the entire computation. If your program runs 10,000 instructions, the trace captures all 10,000 operations with their inputs, outputs, and state changes.
Step 3: Arithmetization
Now comes the bridge to circuits. The zkVM takes that execution trace and translates it into arithmetic constraints that a proof system can verify.
Remember earlier when we showed how a simple calculation gets broken down into constraint steps? The zkVM does this automatically for every instruction. An ADD instruction becomes a constraint that the result equals input A plus input B. A LOAD instruction becomes constraints proving “the value at this memory address is X”, capturing both which address was accessed and what value was found there.
This translation from execution trace to constraints is called arithmetization. It’s the layer that connects normal program execution to the kind of mathematical constraints we discussed at the start of this part.
Step 4: Witness Generation
The constraints from Step 3 describe the rules that must hold, but the proof system also needs the actual values from this specific program run. This concrete data is called the witness.
Think of it like filling in a form. The constraints are the blank form with rules (“this field must be a number,” “this total must equal the sum of these values”). The witness is the form filled out with actual values.
Here’s how it works. You (the prover) just ran the program. You know all the actual values: when that ADD instruction executed, you know it was 5 + 7 = 12. When that LOAD instruction ran, you know it read a specific address and found a specific value. The execution trace captured all of this.
The zkVM generates the witness by extracting all those concrete values from the trace: every register value, every memory read, every intermediate calculation. You use the witness together with the constraints to generate the proof.
The key is that the witness stays with you. You never send it to the verifier. The proof is cryptographic evidence that you have a valid witness, without revealing what the witness actually contains. That’s the zero knowledge part. The verifier just checks the proof against the constraints, never seeing your actual values.
Step 5: Proof Generation
Finally, we get to the actual cryptographic proof generation. This is where SNARKs or STARKs, the systems we learned about in Part 3, actually do their work.
You might be wondering: didn’t we already do the math stuff back in arithmetization? Why do we need another system?
Here’s the distinction. The arithmetization step converts your program into mathematical constraints, equations that describe what the computation should do. But those constraints by themselves aren’t a proof. They’re just equations. Anyone could write down “result = a + b” without actually having the correct values.
What the SNARK or STARK system does is use cryptography to create unforgeable evidence that you have values satisfying those equations, without revealing what the values are.
The math gets quite complex here, beyond the scope of this series, but here’s what’s essentially happening:
The system takes your witness (the actual values like a=5, b=7, result=12) and your constraints (the equations like “result = a + b”) and converts them into a cryptographic form where a verifier can check that the equations hold without ever seeing or learning your actual values.
SNARKs do this using elliptic curve mathematics while STARKs use hash functions. Both achieve the same goal through different mathematical approaches: they create a proof where the verifier can confirm “yes, someone has values that make these equations work” but cannot figure out what those values are, and crucially, cannot fake a valid proof without actually having correct values.
The output is a small cryptographic proof, typically just a few hundred bytes. A verifier checks this proof against the constraints (which are public), and if it passes, they know the computation was performed correctly without needing to see the witness or re-execute anything.
The five steps we just walked through show how zkVMs bridge from code to cryptography: execute the program, record what happened, convert to equations, package the values, create unforgeable cryptographic proof. Essentially, this system allows a developer to write normal code, and the zkVM handles everything else.
Why Different zkVMs Make Different Choices
The process we just went through was highly simplified. In reality, each of those steps involves architectural decisions that determine what the system does well and what it struggles with.
Risc Zero provides a good example. They built one of the first general-purpose zkVMs, focused on making zero-knowledge proving accessible to developers without specialized cryptography knowledge. Their goal was a system where any Rust developer could write normal code and get proofs, prioritizing ease of use and transparency over optimization for specific workloads.
Given these priorities, their architectural choices make sense. They chose STARKs as the proof system because STARKs don’t require trusted setups. The parameters can be generated publicly with no secrets to manage or trust. For a general-purpose system where transparency and developer trust matter, this was more important than having the smallest possible proofs. They also chose RISC-V as the instruction set because existing Rust tooling already compiles to it, meaning developers can use familiar tools without learning new languages.
The tradeoff is that STARK proofs are larger and cost more to verify on-chain. For applications verifying proofs frequently on Ethereum, this gets expensive. But Risc Zero’s target users value transparency and developer experience over minimizing verification costs.
At the opposing end, consider systems built specifically for scaling Ethereum through rollups. These systems exist to verify massive numbers of transactions on-chain as cheaply as possible. Their entire model depends on minimizing the cost of posting proofs to mainnet.
For these systems, the architectural choices flip. They use SNARKs despite the trusted setup complexity, because SNARK proofs are tiny and verification is extremely cheap. They optimize every step of the proving pipeline for verification cost, accepting slower proving times or more expensive hardware. For a rollup processing thousands of transactions, saving even a few hundred gas per verification adds up to millions in cost savings.
Between these extremes sit many other implementations, each optimizing for different priorities. SP1 focuses on developer experience and on-chain verification efficiency with its precompile-centric SNARK architecture. Zisk prioritizes low-latency proving for real-time applications. The diversity mirrors what we saw in Part 4: ZK proofs serve privacy, scaling, bridges, identity, and verifiable computation. Each domain has different requirements.
Architectural decisions don’t just apply to these granular choices about instruction sets or proof systems. Even how the entire system comes together as a whole offers different approaches.
Monolithic vs Modular Architectures
We’ve been zooming in on specific choices like STARKs versus SNARKs or RISC-V versus other instruction sets. Now let’s zoom out to system-level design philosophy.
A monolithic zkVM integrates everything tightly. Execution, arithmetization, and proof generation are all designed to work together as one system. This enables aggressive optimization. When every piece is built specifically to work with every other piece, you can make assumptions and shortcuts that wouldn’t be possible in a more loosely coupled design.
The advantage is performance. The disadvantage is flexibility. If you want to swap the proof system (maybe use STARKs for one workload and SNARKs for another), you can’t just switch components. If you need specialized handling for certain operations, you have to modify the core system.
A modular zkVM separates components more cleanly. The execution layer is one piece, arithmetization is another, proof generation is a third. They communicate through well-defined interfaces. This makes it possible to mix and match implementations.
Want to use a different proof system for different use cases? A modular design makes that feasible. Want to optimize specific operations with specialized accelerators? You can plug them in without rewriting the whole stack.
The tradeoff is integration overhead. More abstraction boundaries often mean less performance. Optimizations that would be straightforward in a tightly integrated system require coordination across components in a modular architecture.
Most zkVMs in production today lean toward monolithic designs. For teams building for specific use cases, the performance wins outweigh flexibility. But this creates issues when your applications don’t fit the assumptions the zkVM optimized for.
Understanding the Real Constraints
The choices we’ve discussed span multiple levels. At the granular level, teams pick instruction sets and proof systems based on their priorities. At the system level, they decide whether to build monolithic or modular architectures. Each decision shapes what the zkVM does well and what it struggles with.
The fundamental challenge is that you can’t optimize for everything. A system built for transparency will sacrifice proof size. A system optimized for on-chain verification costs will accept expensive proving. A tightly integrated monolithic design will perform better but lose flexibility.
This is important because the domains where ZK proofs are used have vastly different requirements. Remember from Part 4 the range of applications: privacy protocols need zero knowledge actively, rollups care about verification costs, bridges need cross-chain finality, identity systems need selective disclosure, and verifiable computation spans everything from historical data queries to AI inference verification.
Most zkVMs handle this diversity by specializing. Pick your target domain, optimize for those specific needs, accept the tradeoffs. If your applications fit those assumptions, the system works beautifully. If they don’t, you’re fighting against the architecture.
Brevis chose to build infrastructure for verifiable computation. This meant serving applications with fundamentally different workloads from day one. PancakeSwap needs sub-second proving for real-time trading fees. Euler processes 100,000 addresses in multi-hour epochs. Kaito needs privacy-preserving verification. These are different use cases with different optimization targets that would normally require different systems entirely.
How do you build a system that handles all of them? Part 6 explores the architecture Brevis developed to address this problem.

