Lesson 10 — zkEVM with Revm
Question
zkEVMs prove EVM execution via ZK SNARKs. Revm runs the execution; ZK circuit verifies the trace. ~1 minute proof generation per block.
Principle (minimum model)
- zkEVM architecture. Revm executes (untrusted) + ZK circuit verifies the execution trace (trusted). Output: proof that EVM ran correctly.
- Trace capture. Revm with custom tracer that records every opcode + state read/write. Trace is gigabytes.
- Witness generation. Circuit consumes the trace + computes proof. ~10 minutes on dedicated hardware.
- Verification. ~50 ms on consumer hardware. Asymmetric: cheap to verify, expensive to prove.
- Production zkEVMs. Polygon zkEVM + Scroll + Linea + ZKsync + Taiko. Each ~$100M+ TVL.
- Trade-offs. ZK = trustless + cheap verification; opt-rollup = trust but cheaper. Different use cases.
- Risc0 / Succinct. ZK VMs that run any Rust code (not just EVM). Future direction.
- zkEVM with revm. Custom revm Database + custom precompiles for ZK-friendly cryptography. Production trend.
Worked example + steps
zkEVM with Revm
Linea, zkSync, Scroll, Polygon zkEVM — every production zkEVM rollup makes the same claim: "the verifier doesn't trust us, the verifier checks a 250-byte proof and that's it." No re-execution. No "trust the operator." A 32-byte commitment, a SNARK or STARK, and a smart contract that says yes or no.
The thing producing the proof is called a prover. The prover doesn't run geth, doesn't run nethermind — it runs Revm, the Rust EVM, compiled to RISC-V (a clean reduced-instruction-set CPU architecture that zkVMs can model cheaply) and executed inside a zkVM. This lesson is why Revm specifically, and what the prover-side code actually looks like.
1. The proving stack
flowchart TB
subgraph Host
RPC[Ethereum RPC] --> Pre[preflight: collect witness]
end
Pre -->|Input: header + witness + call| Guest
subgraph Guest [zkVM guest]
Verify[verify witness vs stateRoot]
Verify --> RevmRun[Revm runs the EVM call]
RevmRun --> Journal[commit block hash + result]
end
Guest --> Prover[Proving system<br/>STARK / SNARK]
Prover --> Proof[Proof + Journal]
Proof --> Verifier[on-chain verifier contract]
You compile a normal Rust program (which calls Revm) to RISC-V, run it inside a zkVM, and the zkVM emits a proof of correct execution.
2. A real guest — Steel + Risc0
This is the entire guest/src/main.rs from boundless-xyz/steel/examples/erc20-counter:
use alloy_primitives::U256;
use erc20_counter_core::{IERC20, Input, Journal};
use risc0_steel::{Contract, ethereum::EthChainSpec};
use risc0_zkvm::guest::env;
fn main() {
// Read the input from the guest environment.
let input: Input = env::read();
// Derive the chain spec from the chain ID.
let chain_spec = EthChainSpec::from_chain_id(input.chain_id).unwrap();
// Converts the input into a `EvmEnv` for execution. It checks that the state matches the state
// root in the header provided in the input.
let env = input.evm_input.into_env(chain_spec);
// Execute the view call; it returns the result in the type generated by the `sol!` macro.
let call = IERC20::balanceOfCall {
account: input.account,
};
let returns = Contract::new(input.erc20_contract, &env)
.call_builder(&call)
.call();
// Check that the given account holds at least 1 token.
assert!(returns >= U256::from(1));
// Commit the block hash and number used when deriving `view_call_env` to the journal.
let journal = Journal {
commitment: env.into_commitment(),
contract: input.erc20_contract,
};
env::commit_slice(&journal.abi_encode());
}
That's the entire zkVM guest. ~25 lines. Read it carefully.
env::read()
The guest reads its inputs from the host through a serialized stream. The Input struct holds: chain ID, target contract address, the EVM input (state proofs the guest will need), and the account to query.
input.evm_input.into_env(chain_spec)
This is where the magic is. evm_input contains a block header and a state witness (every storage slot the call will touch, with their MPT proofs). .into_env(...) verifies the witness against the header's stateRoot — if a single byte is wrong, this fails. This is what guarantees the prover can't lie about state.
IERC20::balanceOfCall (sol!)
The same sol! macro you saw in MEV — generates the typed call. The same code that talks to a node over RPC also runs inside the zkVM. That's the unification: ABI, encoding, type system — all shared between the off-chain world and the in-prover world.
Contract::new(...).call_builder(&call).call()
Executes the EVM view call inside Revm, against the verified state. Returns a typed U256. The Revm instance reads through the witness; if it ever asks for a slot that wasn't witnessed, the proof fails.
env::commit_slice(&journal.abi_encode())
The "public output" — what the verifier will see. Here, an ABI-encoded Journal containing a commitment (block hash, block number, stateRoot) and the contract address. Anyone with the proof and this journal can verify: "On Ethereum block N, contract X had user Y holding at least 1 token."
3. The host side (preflight)
You don't see the host in that file, but it's the mirror: it talks to a real Ethereum node (via Alloy + a Reth RPC), simulates the call, collects the witness, then ships it as Input to the prover. Steel's host helpers handle the RPC + witness collection so your binary just calls something like:
let input = builder.preflight(&provider, contract, &call).await?;
let env = ExecutorEnv::builder().write(&input)?.build()?;
let receipt = default_prover().prove(env, ERC20_COUNTER_GUEST_ELF)?;
4. Why Revm specifically?
- It's modular (Database trait makes the witness/oracle pattern clean)
- It's deterministic — every run with the same inputs produces the same outputs
- It's fast in CPU, which translates to fewer cycles, which translates to smaller proofs
Geth in Go would be a nightmare to compile and minimize for a zkVM. Revm just works.
5. The witness pattern
Inside the prover you can't "read state from disk." Instead, before proving you assemble a witness: every state value the block touched. Then your in-zkVM Database impl looks like:
struct WitnessDB {
accounts: HashMap<Address, AccountInfo>,
storage: HashMap<(Address, U256), U256>,
// ...
}
impl Database for WitnessDB {
fn basic(&mut self, addr: Address) -> ... {
Ok(self.accounts.get(&addr).cloned())
}
// ...
}
If the block reads something not in the witness, the proof fails. The witness producer (your indexer / Reth ExEx) is therefore part of the security model.
6. Performance reality
Proving a single Ethereum block in 2026:
| System | Approx. proving time (single block) | Hardware |
|---|---|---|
| Risc0 | seconds–minutes | GPU |
| SP1 | seconds | GPU + recursion |
| Custom zkEVM (Linea, Scroll) | sub-second per block | dedicated infra |
Generic zkVMs (Risc0/SP1) trade some prover speed for flexibility — they can prove any Rust program, not just EVM. Custom zkEVMs are faster but rebuild the whole stack from scratch.
7. Why this matters
- L2s using zkEVM rely on this pipeline (Linea, zkSync, Scroll, Polygon zkEVM)
- Optimistic rollups are migrating toward "validity proofs as fast finality"
- Stateless clients could let you sync without holding state — relying on witnesses + proofs
Reading risc0/risc0-ethereum is the most direct path to understanding zk + Revm in production.
8. Practice
Before claiming familiarity, write the smallest possible host/guest pair:
- Guest reads two integers, returns their sum
- Host generates and verifies the proof
- Modify the guest to call Revm on a 1-tx block
- Compare guest cycle counts before/after — that's where the perf engineering lives
Now you know what "L2 prover" actually does.
Final check: in two sentences, explain what makes a zk proof of EVM execution trustless — versus a node operator just claiming "I ran the block, here's the result." If your answer doesn't reference the verifier-side check (commitment + recomputation in the verifier contract), the lesson isn't done with you.
Summary (3 lines)
- zkEVM = revm executes + ZK circuit verifies trace. Trace gigabytes; proof gen ~10 minutes; verify ~50 ms.
- Production zkEVMs: Polygon / Scroll / Linea / ZKsync / Taiko. Each $100M+ TVL.
- Trade-off: ZK trustless+cheap-verify vs opt-rollup cheap-prove. Risc0 / Succinct = future ZK VMs. Revm + custom precompiles enables zkEVM.