Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/risc0/risc0/llms.txt

Use this file to discover all available pages before exploring further.

Verify Groth16 proofs generated by Circom/SnarkJS inside the RISC Zero zkVM, enabling proof recursion and cross-system verification.

What You’ll Learn

  • Verifying Groth16 proofs in the zkVM
  • Working with Circom-generated proofs
  • Proof system composition
  • Performance optimization with DEV_MODE
This is an experimental example demonstrating proof system interoperability.

Overview

This example verifies a Groth16 proof (from another zkSNARK system) inside the RISC Zero zkVM, producing a RISC Zero receipt that proves the Groth16 proof is valid.

Why Verify Proofs in zkVM?

  1. Proof Recursion: Compress multiple proofs into one
  2. Cross-System Verification: Bridge different proof systems
  3. Flexible Verification: Add custom logic around proof checking
  4. Standardization: Use RISC Zero for all verifications

Example Use Case

The example uses a pre-generated Groth16 proof of a Sudoku solution from zkSudoku.
Circom Circuit (Sudoku) → Groth16 Proof → Verify in zkVM → RISC Zero Receipt

How It Works

1
Guest Program
2
The guest verifies the Groth16 proof and commits verification key and public input hashes:
3
use risc0_groth16::{ProofJson, PublicInputsJson, Verifier, VerifyingKeyJson};
use risc0_zkvm::{guest::env, sha::Digestible};

pub fn main() {
    // Read proof components
    let (proof_json, public_inputs_json, verifying_key_json): (
        ProofJson,
        PublicInputsJson,
        VerifyingKeyJson,
    ) = env::read();

    // Verify the Groth16 proof
    Verifier::from_json(
        proof_json,
        public_inputs_json.clone(),
        verifying_key_json.clone(),
    )
    .unwrap()
    .verify()
    .unwrap();

    // Commit hashes to journal
    let verifying_key_digest = verifying_key_json
        .verifying_key()
        .unwrap()
        .digest();
    let public_inputs_digest = public_inputs_json
        .to_scalar()
        .unwrap()
        .digest();
    
    env::commit(&(verifying_key_digest, public_inputs_digest));
}
4
Host Program
5
The host loads the Groth16 proof artifacts and generates a RISC Zero receipt:
6
use groth16_verifier_methods::{GROTH16_VERIFIER_ELF, GROTH16_VERIFIER_ID};
use risc0_groth16::{load_proof_from_file, load_public_inputs, load_verifying_key};
use risc0_zkvm::{ExecutorEnv, default_prover};

fn main() {
    // Load Groth16 proof artifacts
    let proof_json = load_proof_from_file("proof.json").unwrap();
    let public_inputs_json = load_public_inputs("public.json").unwrap();
    let verifying_key_json = load_verifying_key("verification_key.json").unwrap();

    // Create zkVM environment
    let env = ExecutorEnv::builder()
        .write(&(proof_json, public_inputs_json, verifying_key_json))
        .unwrap()
        .build()
        .unwrap();

    // Generate RISC Zero proof
    let receipt = default_prover()
        .prove(env, GROTH16_VERIFIER_ELF)
        .unwrap()
        .receipt;

    // Verify the receipt
    receipt.verify(GROTH16_VERIFIER_ID).unwrap();

    println!("Successfully verified Groth16 proof in zkVM!");
}

Running the Example

This example requires ~500M cycles and will take significant time to prove locally. Use DEV_MODE for testing.

With DEV_MODE (Fast)

RISC0_DEV_MODE=1 cargo run --release

Full Proving (Slow)

cargo run --release
For production, use Bonsai for remote proving:
export BONSAI_API_KEY="your-api-key"
export BONSAI_API_URL="https://api.bonsai.xyz"
cargo run --release

What Gets Proven?

The RISC Zero receipt proves:
  1. Valid Groth16 Proof: The Groth16 proof verifies correctly
  2. Correct Circuit: Verification key hash identifies the circuit
  3. Public Inputs: Public input hash commits to the statement
  4. Verification Integrity: All checks passed in the zkVM
The journal contains:
(verifying_key_digest, public_inputs_digest)
This binds the proof to a specific circuit and statement.

Groth16 Background

What is Groth16?

Groth16 is a zkSNARK proof system with:
  • Constant Size: ~128 bytes regardless of circuit
  • Fast Verification: Milliseconds onchain
  • Trusted Setup: Requires ceremony per circuit
  • Pairing-Based: Uses elliptic curve pairings

Verification Equation

Groth16 verifies by checking:
e(A, B) = e(α, β) · e(L, γ) · e(C, δ)
Where:
  • e: Pairing function on BN254 curve
  • A, C: Proof elements in G1
  • B: Proof element in G2
  • α, β, γ, δ: Verification key parameters
  • L: Linear combination of public inputs

Working with Circom

Circom Circuit

Example Sudoku circuit:
template Sudoku() {
    signal input puzzle[9][9];
    signal input solution[9][9];
    
    // Verify solution is valid
    component checker = SudokuChecker();
    checker.puzzle <== puzzle;
    checker.solution <== solution;
}

Generate Groth16 Proof

Using Circom and SnarkJS:
# Compile circuit
circom sudoku.circom --r1cs --wasm

# Trusted setup (or use existing ceremony)
snarkjs groth16 setup sudoku.r1cs pot12_final.ptau sudoku_0000.zkey

# Generate witness
node generate_witness.js sudoku.wasm input.json witness.wtns

# Create proof
snarkjs groth16 prove sudoku_0000.zkey witness.wtns proof.json public.json

# Extract verification key
snarkjs zkey export verificationkey sudoku_0000.zkey verification_key.json
See the Circom documentation for details.

Proof Artifacts

proof.json

Contains the Groth16 proof:
{
  "pi_a": ["...", "...", "1"],
  "pi_b": [["...", "..."], ["...", "..."], ["1", "0"]],
  "pi_c": ["...", "...", "1"],
  "protocol": "groth16",
  "curve": "bn128"
}

verification_key.json

Contains the verification key:
{
  "protocol": "groth16",
  "curve": "bn128",
  "nPublic": 81,
  "vk_alpha_1": ["...", "...", "1"],
  "vk_beta_2": [["...", "..."], ["...", "..."], ["1", "0"]],
  "vk_gamma_2": [["...", "..."], ["...", "..."], ["1", "0"]],
  "vk_delta_2": [["...", "..."], ["...", "..."], ["1", "0"]],
  "IC": [["...", "...", "1"], ...]
}

public.json

Contains public inputs:
[
  "123456789",
  "234567891",
  ...
]

Performance

MetricValue
Cycles~500M
Proving time (local)10-30 minutes
Proving time (Bonsai)1-2 minutes
Memory usage~8 GB
Receipt size~128 KB

Optimization Tips

  1. Use DEV_MODE: For development and testing
  2. Bonsai: For production proving
  3. Batch Verification: Verify multiple proofs in one zkVM session
  4. Precomputation: Cache verification keys

Use Cases

Proof Aggregation

Combine multiple Groth16 proofs:
Proof1 (Circom) ─┐
                ├─ Verify All in zkVM → Single Receipt
Proof2 (Circom) ─┤
Proof3 (Circom) ─┘
Benefits:
  • Constant verification cost onchain
  • Batch processing efficiency
  • Simplified integration

Cross-Chain Bridges

Verify proofs from other chains:
Ethereum zkSNARK → Verify in zkVM → Submit to Solana

Privacy-Preserving Audits

Verify compliance proofs:
Circom KYC Proof → Verify in zkVM → Onchain Credential

ZK Rollup Verification

Verify rollup state transitions:
Rollup Proof (Groth16) → zkVM Verification → L1 Settlement

Comparison to Native Verification

ApproachGas Cost (ETH)LatencyFlexibility
Native Groth16~300K gasInstantFixed
zkVM Verification~300K gas*AsyncCustomizable
*Via Bonsai callback

When to Use zkVM Verification?

Use zkVM when:
  • Aggregating multiple proofs
  • Adding custom verification logic
  • Bridging to chains without Groth16 precompiles
  • Standardizing on RISC Zero infrastructure
Use native when:
  • Single proof verification
  • Minimizing latency
  • Ethereum mainnet (has precompiles)

Integration with Other Examples

This example uses BN254 pairings (see BN254 example):
use substrate_bn::{G1, G2, pairing_batch};

// Groth16 verification uses pairings
let valid = pairing_batch(&[
    (proof.a, proof.b),
    (vk.alpha, vk.beta),
    (linear_combination, vk.gamma),
    (proof.c, vk.delta),
]) == Gt::one();

Trying Other Circuits

You can verify any Circom circuit:

ECDSA Circuit

From 0xPARC circom-ecdsa:
git clone https://github.com/0xPARC/circom-ecdsa
cd circom-ecdsa
# Follow their README to generate proofs
# Use those proof files with this example

Custom Circuits

  1. Write your Circom circuit
  2. Generate proof with SnarkJS
  3. Load proof artifacts in this example
  4. Verify in zkVM

Security Considerations

Trusted Setup

Groth16 requires a trusted setup ceremony:
  • Circuit-specific: Each circuit needs its own setup
  • Toxic waste: Setup parameters must be destroyed
  • Powers of Tau: Can reuse universal setup then specialize
Always use proofs from trusted ceremonies.

Verification Key Binding

The example commits verifying_key_digest to prevent:
  • Proof substitution attacks
  • Circuit confusion
  • Parameter tampering
Always check the verification key hash.

Next Steps