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.

On-Chain Proof Verification

Verify zkVM proofs on-chain to ensure the integrity of off-chain computations in your smart contracts.

Verification Overview

To verify a RISC Zero proof on-chain, you need:
  1. Seal (Proof): The cryptographic proof bytes
  2. Image ID: The identifier for the guest program
  3. Journal Digest: SHA-256 hash of the program’s output
Verification ensures that a specific guest program (identified by imageId) was executed correctly and produced the committed journal output.

The Verification Interface

All RISC Zero verifier contracts implement IRiscZeroVerifier:
interface IRiscZeroVerifier {
    /// @notice Verifies a RISC Zero proof
    /// @param seal The encoded cryptographic proof (Groth16)
    /// @param imageId The unique identifier for the guest program  
    /// @param journalDigest SHA-256 hash of the journal
    function verify(
        bytes calldata seal,
        bytes32 imageId,
        bytes32 journalDigest
    ) external view;
}
The verify() function reverts if verification fails. It does not return a boolean.

Basic Verification Pattern

Here’s a complete example showing the standard verification pattern:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IRiscZeroVerifier} from "risc0-ethereum/contracts/src/IRiscZeroVerifier.sol";

contract EvenNumber {
    /// @notice The RISC Zero verifier contract
    IRiscZeroVerifier public immutable verifier;
    
    /// @notice Image ID of the only function we accept proofs from
    bytes32 public constant IS_EVEN_ID = /* your image ID here */;
    
    /// @notice The verified even number
    uint256 public number;
    
    constructor(IRiscZeroVerifier _verifier) {
        verifier = _verifier;
    }
    
    /// @notice Set the even number with proof verification
    /// @param x The number to set (must be even)
    /// @param seal The Groth16 proof
    function set(uint256 x, bytes calldata seal) public {
        // Step 1: Construct the expected journal
        bytes memory journal = abi.encode(x);
        
        // Step 2: Verify the proof
        // This will revert if the proof is invalid
        verifier.verify(
            seal,
            IS_EVEN_ID,
            sha256(journal)
        );
        
        // Step 3: Update state (only reached if proof is valid)
        number = x;
    }
}

How It Works

1

Construct Expected Journal

Build the journal data that should have been committed by the guest program
bytes memory journal = abi.encode(x);
2

Call Verify

The verifier checks:
  • The proof is cryptographically valid
  • The proof was generated by the program with imageId
  • The journal hash matches sha256(journal)
verifier.verify(seal, IS_EVEN_ID, sha256(journal));
3

Update State

Only executed if verification succeeds. If verification fails, the transaction reverts.
number = x;

Groth16 Setup

To generate Groth16 proofs suitable for on-chain verification, you need to set up the Groth16 prover.

Prerequisites

  • Architecture: x86-64 (required for Docker setup)
  • Docker: Installed and running
  • rzup: RISC Zero toolchain manager

Installation

1

Install rzup Component

# Ensure rzup is version >= 0.5.0
rzup --version

# Install Groth16 component
rzup install risc0-groth16
2

Verify Installation

# Check that the component is installed
rzup list

Manual Docker Setup (Alternative)

If you need more control, you can manually set up the Groth16 prover:
# From the groth16_proof directory
cd groth16_proof
./scripts/install_prover.sh
The Docker setup downloads a proving key from an existing trusted setup ceremony.

Generating Groth16 Proofs

The simplest way to generate Groth16 proofs:
use risc0_zkvm::{default_prover, ExecutorEnv, ProverOpts, VerifierContext};
use methods::{IS_EVEN_ELF, IS_EVEN_ID};

fn prove_even(number: u64) -> anyhow::Result<Vec<u8>> {
    // Build execution environment
    let env = ExecutorEnv::builder()
        .write(&number)?
        .build()?;
    
    // Generate Groth16 proof
    let prover = default_prover();
    let prove_info = prover.prove_with_ctx(
        env,
        &VerifierContext::default(),
        IS_EVEN_ELF,
        &ProverOpts::groth16(),  // Request Groth16
    )?;
    
    // Extract the seal (proof bytes)
    let receipt = prove_info.receipt;
    let seal = receipt.inner.groth16()?.seal.clone();
    
    Ok(seal)
}

Extracting Seal from Receipt

For Groth16 receipts:
use risc0_zkvm::Receipt;

fn extract_seal(receipt: &Receipt) -> Vec<u8> {
    receipt
        .inner
        .groth16()
        .expect("Expected Groth16 receipt")
        .seal
        .clone()
}

The Groth16 Verifier Contract

The Groth16 verifier is a Solidity contract generated by snarkJS:
// Simplified from verifier.sol
contract Groth16Verifier {
    // Scalar field size
    uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
    
    // Verification key constants (from trusted setup)
    uint256 constant alphax = /* ... */;
    uint256 constant alphay = /* ... */;
    // ... more verification key parameters
    
    function verifyProof(
        uint256[2] calldata _pA,
        uint256[2][2] calldata _pB,
        uint256[2] calldata _pC,
        uint256[5] calldata _pubSignals
    ) public view returns (bool) {
        // Pairing check implementation
        // ...
    }
}
You don’t need to understand the verifier internals - just use the IRiscZeroVerifier interface.

Advanced Verification Patterns

Multi-Proof Verification

Verify multiple proofs in a single transaction:
function batchVerify(
    uint256[] calldata values,
    bytes[] calldata seals
) public {
    require(values.length == seals.length, "Length mismatch");
    
    for (uint256 i = 0; i < values.length; i++) {
        bytes memory journal = abi.encode(values[i]);
        verifier.verify(seals[i], IMAGE_ID, sha256(journal));
    }
    
    // All proofs verified, update state
    // ...
}

Complex Journal Structures

For multiple values in the journal:
struct Result {
    uint256 sum;
    uint256 product;
    bool isValid;
}

function verifyComputation(
    Result calldata result,
    bytes calldata seal
) public {
    // Encode the entire struct
    bytes memory journal = abi.encode(
        result.sum,
        result.product,
        result.isValid
    );
    
    verifier.verify(seal, COMPUTE_ID, sha256(journal));
    
    // Use verified results
    // ...
}

Proof Caching

Avoid re-verification of the same proof:
mapping(bytes32 => bool) public verifiedProofs;

function setWithCache(uint256 x, bytes calldata seal) public {
    bytes32 proofHash = keccak256(seal);
    
    if (!verifiedProofs[proofHash]) {
        bytes memory journal = abi.encode(x);
        verifier.verify(seal, IMAGE_ID, sha256(journal));
        verifiedProofs[proofHash] = true;
    }
    
    number = x;
}

Understanding Image IDs

The Image ID uniquely identifies your guest program:
// Generated by the build process
use methods::IS_EVEN_ID;

// In your Solidity contract
bytes32 public constant IS_EVEN_ID = 0x1234...5678;
The Image ID changes whenever you modify your guest code. Always update your contract when deploying new guest program versions.

Getting the Image ID

The build process generates it automatically:
// methods/src/lib.rs (auto-generated)
pub const IS_EVEN_ID: [u32; 8] = /* ... */;
pub const IS_EVEN_ELF: &[u8] = include_bytes!(/* ... */);
Convert to bytes32 for Solidity:
fn image_id_to_bytes32(id: &[u32; 8]) -> [u8; 32] {
    let mut bytes = [0u8; 32];
    for (i, &word) in id.iter().enumerate() {
        bytes[i*4..(i+1)*4].copy_from_slice(&word.to_le_bytes());
    }
    bytes
}

Gas Optimization Tips

1

Use Immutable Verifier Address

IRiscZeroVerifier public immutable verifier;
Saves ~2,100 gas per call vs. storage variable
2

Cache Image IDs as Constants

bytes32 public constant IMAGE_ID = 0x...;
Cheaper than storage
3

Minimize Journal Size

Only commit necessary data. Smaller journals = cheaper SHA-256 hashing
4

Batch Verification

If verifying multiple proofs, batch them to amortize costs

Troubleshooting

Verification Reverts

Common causes:
  1. Journal Mismatch: The journal constructed in Solidity doesn’t match what the guest committed
    // Guest commits: env::commit(&x)
    // Solidity must match: abi.encode(x)
    
  2. Wrong Image ID: Using an outdated or incorrect image ID
    // Ensure this matches your current build
    bytes32 public constant IMAGE_ID = /* correct ID */;
    
  3. Invalid Proof: The seal is corrupted or wasn’t generated correctly
    • Verify proof generation succeeded
    • Check proof encoding/decoding

Dev Mode Issues

Proofs generated in DEV_MODE will fail on-chain verification. Always use real proofs for testing contract integration.
# Don't use DEV_MODE for contract testing
# RISC0_DEV_MODE=1 cargo run  # ❌ Proofs won't verify

# Generate real proofs
cargo run --release  # ✅ Valid proofs

Testing Verification

Foundry Test Example

// test/EvenNumber.t.sol
import {Test, console} from "forge-std/Test.sol";
import {EvenNumber} from "../src/EvenNumber.sol";

contract EvenNumberTest is Test {
    EvenNumber public evenNumber;
    address verifierRouter = /* deployed verifier */;
    
    function setUp() public {
        evenNumber = new EvenNumber(
            IRiscZeroVerifier(verifierRouter)
        );
    }
    
    function testValidProof() public {
        // Load proof from file (generated by Rust)
        bytes memory seal = vm.parseBytes(
            vm.readFile("proofs/even_42.bin")
        );
        
        evenNumber.set(42, seal);
        assertEq(evenNumber.number(), 42);
    }
    
    function testInvalidProof() public {
        bytes memory fakeSeal = hex"deadbeef";
        
        vm.expectRevert();
        evenNumber.set(42, fakeSeal);
    }
}

Version Management

RISC Zero uses a version management system for verifier contracts:
  • Base Verifier: Core verification logic
  • Emergency Stop Wrapper: Allows disabling compromised verifiers
  • Router: Routes to correct verifier version
Always use the RiscZeroVerifierRouter to ensure your contracts work with future zkVM versions.
For more details, see the version management design.

Next Steps

Ethereum Integration Guide

Complete Ethereum integration walkthrough

Example Contracts

Browse working examples

IRiscZeroVerifier Source

View the verifier interface

Steel Library

Ethereum state proofs