FABRKNT
Reth Expert — Production Engineering
Production Engineering
Lesson 10 of 25·CONTENT15 min35 XP

Treat this page as a workbench, not a blog post. The goal is to extract a reusable mental model from the source and carry it into the rest of the Fabrknt stack.

Course
Reth Expert — Production Engineering
Lesson role
CONTENT
Sequence
10 / 25

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:

SystemApprox. proving time (single block)Hardware
Risc0seconds–minutesGPU
SP1secondsGPU + recursion
Custom zkEVM (Linea, Scroll)sub-second per blockdedicated 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:

  1. Guest reads two integers, returns their sum
  2. Host generates and verifies the proof
  3. Modify the guest to call Revm on a 1-tx block
  4. 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.