Skip to main content

Generate a ZK Proof

We'll create a program that allows users to prove they are above a certain age without revealing their actual birthdate. This is particularly useful for age-restricted services where privacy is important. The system uses RISC0 zkVM to generate zero-knowledge proofs that can be verified in the Cartesi Machine without exposing sensitive personal information.

Creating the Project

First, install the RISC Zero toolchain:

  1. Install rzup:

    curl -L https://risczero.com/install | bash
  2. Run rzup to install Risc Zero toolchain:

    rzup install
  3. Create a new project:

    cargo risczero new generate_proof --guest-name age_verify
    cd generate_proof

This command scaffolds a new RISC Zero project with the basic structure and necessary dependencies.

Project Structure

Create a new project with the following structure:

generate_proof/
├── Cargo.toml # Workspace configuration
├── host/
│ ├── Cargo.toml # Host dependencies
│ └── src/
│ └── main.rs # Host program that runs the zkVM
└── methods/
├── guest/
│ ├── Cargo.toml # Guest dependencies
│ └── src/
│ └── main.rs # Guest program that runs inside zkVM
└── Cargo.toml

Configuration Files

1. Workspace Cargo.toml

The workspace configuration manages all the components of our RISC Zero project. We set optimization levels high even in dev mode because zkVM execution is significantly faster with optimizations:

[workspace]
resolver = "2"
members = ["host", "methods"]

[profile.dev]
opt-level = 3

[profile.release]
debug = 1
lto = true

2. Host Cargo.toml

The host program needs several dependencies for proof generation, serialization, and cryptographic operations:

[package]
name = "host"
version = "0.1.0"
edition = "2021"

[dependencies]
methods = { path = "../methods" }
risc0-zkvm = { version = "1.2.5" }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
serde = "1.0"
serde_json = "1.0"
hex = "0.4"
bincode = "1.3"

3. Guest Cargo.toml

The guest code runs inside the zkVM and needs minimal dependencies. We disable default features to reduce the binary size:

[package]
name = "age_verify"
version = "0.1.0"
edition = "2021"

[workspace]

[dependencies]
risc0-zkvm = { version = "1.2.5", default-features = false, features = ['std'] }

Writing the Code

1. Guest Program (methods/guest/src/main.rs)

use risc0_zkvm::guest::env;

// Minimum age requirement (cannot be modified during runtime)
const MINIMUM_AGE: u64 = 21;
const SECONDS_IN_YEAR: u64 = 31_536_000; // 365 days
const SECONDS_IN_DAY: u64 = 86_400;

fn main() {
// Read the birthdate timestamp from the host
let birthdate_timestamp: u64 = env::read();

// Read the current time from host (to prevent time manipulation)
let current_timestamp: u64 = env::read();

// Calculate approximate age in years
let age_in_seconds = current_timestamp.saturating_sub(birthdate_timestamp);
let mut age = age_in_seconds / SECONDS_IN_YEAR;

// Account for leap years more precisely by checking day of year
let birthdate_day_of_year = day_of_year(birthdate_timestamp);
let current_day_of_year = day_of_year(current_timestamp);

// If we haven't reached the birthday day this year, subtract 1 from age
if current_day_of_year < birthdate_day_of_year {
age = age.saturating_sub(1);
}

// Verify age requirement
assert!(age >= MINIMUM_AGE, "Age verification failed: Too young!");

// Commit only the verification result, not the actual age
let verification_result = true;
env::commit(&verification_result);
}

// Helper function to calculate day of year (1-366)
fn day_of_year(timestamp: u64) -> u64 {
// Get days since epoch
let days_since_epoch = timestamp / SECONDS_IN_DAY;

// Calculate year (approximate)
let year = 1970 + (days_since_epoch / 365);

// Calculate start of year timestamp
let mut year_start = 0;
for y in 1970..year {
year_start += if is_leap_year(y) { 366 } else { 365 };
}
year_start *= SECONDS_IN_DAY;

// Calculate day of year
((timestamp - year_start) / SECONDS_IN_DAY) + 1
}

// Helper function to check if a year is a leap year
fn is_leap_year(year: u64) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}

2. Host Program (host/src/main.rs)

By default, we use the standard composite receipt type which is suitable for most development scenarios:

// These constants represent the RISC-V ELF and the image ID generated by risc0-build.
// The ELF is used for proving and the ID is used for verification.
use methods::{AGE_VERIFY_ELF, AGE_VERIFY_ID};
use risc0_zkvm::{default_prover, ExecutorEnv};
use serde::Serialize;
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};

#[derive(Serialize)]
struct ProofData {
input: String, // Hex string containing receipt and image_id
}

fn main() {
// Initialize tracing
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env())
.init();

// Example birthdate: January 1, 2000 (timestamp in seconds)
let birthdate_timestamp = 946684800u64; // 2000-01-01T00:00:00Z

// Get current time
let current_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();

// Create the execution environment
let env = ExecutorEnv::builder()
.write(&birthdate_timestamp)
.unwrap()
.write(&current_timestamp)
.unwrap()
.build()
.unwrap();

// Get the prover
let prover = default_prover();

// Generate proof
println!("Generating age verification proof...");
let receipt = prover
.prove(env, AGE_VERIFY_ELF)
.unwrap()
.receipt;

// Prepare proof data
let receipt_bytes = bincode::serialize(&receipt).unwrap();
let image_id_bytes: Vec<u8> = AGE_VERIFY_ID
.iter()
.flat_map(|&id| id.to_le_bytes().to_vec())
.collect();

// Combine receipt and image_id
let mut combined_bytes = Vec::new();
combined_bytes.extend_from_slice(&receipt_bytes);
combined_bytes.extend_from_slice(&image_id_bytes);

// Create proof data with hex encoding
let proof_data = ProofData {
input: hex::encode(&combined_bytes),
};

// Log the image ID
println!("\n=== AGE VERIFICATION IMAGE ID ===");
println!("const AGE_VERIFY_ID: [u32; 8] = [");
for (i, &id) in AGE_VERIFY_ID.iter().enumerate() {
if i < AGE_VERIFY_ID.len() - 1 {
println!(" 0x{:08x},", id);
} else {
println!(" 0x{:08x}", id);
}
}
println!("];");

// Save proof to file
fs::write(
"proof_input.json",
serde_json::to_string_pretty(&proof_data).unwrap(),
)
.unwrap();

println!("\n=== PROOF STATS ===");
println!("Proof size: {} bytes", combined_bytes.len());
println!("Hex string length: {} chars", proof_data.input.len());

}

Generate the Proof

  1. Build and run to generate the proof:
RISC0_DEV_MODE=0 cargo run --release
  1. Check the generated proof:
cat proof_input.json

The proof is saved in proof_input.json. This file will be used in the next step to verify the proof in the Cartesi Machine.

Receipt Types and Proving Options

RISC Zero supports three types of receipts through ProverOpts:

// Default composite receipt
prover.prove(env, ELF_PATH)

// Succinct receipt
prover.prove_with_opts(env, ELF_PATH, &ProverOpts::succinct())

// Groth16 receipt (x86 only)
prover.prove_with_opts(env, ELF_PATH, &ProverOpts::groth16())

Using Groth16 Receipts

For on-chain verification and production deployments, Groth16 receipts are recommended due to their minimal size (a few bytes). To use Groth16:

let receipt = prover
.prove_with_opts(env, PASSWORD_ELF, &ProverOpts::groth16())
.unwrap()
.receipt;
important

Groth16 receipt generation requires x86 architecture due to the STARK-to-SNARK prover implementation. If you're on Apple Silicon or another architecture, you'll either need to:

  • Use a remote x86 server
  • Use the Bonsai proving service
  • Use composite or succinct receipts instead

Using Bonsai for Remote Proving

For production deployments, we recommend using Bonsai proving service with Groth16 receipts:

1. Configure Bonsai Credentials

export BONSAI_API_KEY=your_api_key_here
export BONSAI_API_URL=https://api.bonsai.xyz

2. Generate the proof

RISC0_DEV_MODE=0 cargo run --release

The default_prover() function automatically detects these environment variables and uses Bonsai for proving.

3. Specify Groth16 Receipts

// It is recommended to use Groth16 receipts for Cartesi(on-chain) integration
let receipt = prover
.prove_with_opts(env, PASSWORD_ELF, &ProverOpts::groth16())
.unwrap()
.receipt;
tip

We strongly recommend using Groth16 receipts for Cartesi Rollups integration because:

  • Much smaller proof size (a few bytes vs >100kb)
  • Faster verification time
  • Lower resource usage in the Cartesi Machine
  • Better scalability for blockchain applications

Next, we'll see how to verify this proof inside a Cartesi Rollup.