Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,51 @@ cargo run --release -- --zkvms sp1 --dump-inputs debug-inputs stateless-validato

Note: Input files are zkVM-independent (the same input is used across all zkVMs), so they're only written once even when benchmarking multiple zkVMs.

### Proof Generation & Verification

The benchmark runner supports a decoupled prove/verify workflow using the `--action` flag. This allows generating proofs on one machine and verifying them on another.

**Actions:**
- `--action execute` (default): Only execute the zkVM, no proof generation.
- `--action prove`: Execute and generate a zkVM proof, with optional proof persistence via `--save-proofs`.
- `--action verify`: Verify pre-generated proofs loaded from disk or a remote URL.

**Step 1: Generate and save proofs**

```bash
cd crates/ere-hosts

# Prove and save proof artifacts to a folder
cargo run --release -- --zkvms sp1 --action prove --save-proofs my-proofs \
stateless-validator --execution-client reth
```

This creates proof files in the following structure:
```
my-proofs/
└── reth-v1.10.2/
└── sp1-v4.0.0/
├── fixture1.proof
└── fixture2.proof
```

**Step 2: Verify proofs**

From a local folder:
```bash
cargo run --release -- --zkvms sp1 --action verify --proofs-folder my-proofs \
stateless-validator --execution-client reth
```

From a remote `.tar.gz` archive (e.g., hosted on GitHub releases or S3):
```bash
cargo run --release -- --zkvms sp1 --action verify \
--proofs-url https://example.com/proofs.tar.gz \
stateless-validator --execution-client reth
```

When using `--proofs-url`, the archive is downloaded and extracted to a temporary directory that is cleaned up after verification completes. The `.tar.gz` should contain the same folder structure as `--save-proofs` produces.

## License

Licensed under either of
Expand Down
3 changes: 3 additions & 0 deletions crates/benchmark-runner/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ sha2.workspace = true
serde.workspace = true
serde_json.workspace = true
strum.workspace = true
flate2.workspace = true
reqwest.workspace = true
tar.workspace = true
anyhow.workspace = true
auto_impl.workspace = true
ere-guests-downloader.workspace = true
Expand Down
1 change: 1 addition & 0 deletions crates/benchmark-runner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ pub mod stateless_validator;
pub mod zisk_profiling;

pub mod runner;
pub mod verification;
51 changes: 50 additions & 1 deletion crates/benchmark-runner/src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ pub struct RunConfig {
pub dump_inputs_folder: Option<PathBuf>,
/// Optional Zisk profiling configuration
pub zisk_profile_config: Option<ProfileConfig>,
/// Optional folder to save proof artifacts for later verification
pub save_proofs_folder: Option<PathBuf>,
}

/// Action specifies whether we should prove or execute
Expand All @@ -61,6 +63,8 @@ pub enum Action {
Prove,
/// Only execute the zkVM without proving
Execute,
/// Verify proofs loaded from disk
Verify,
}

/// Executes benchmarks for a given guest program type and zkVM
Expand All @@ -81,6 +85,12 @@ pub fn run_benchmark(
Action::Prove => inputs
.into_iter()
.try_for_each(|input| process_input(zkvm, input, config, elf))?,

Action::Verify => {
return Err(anyhow!(
"run_benchmark should not be called with Action::Verify, use run_verify_from_disk"
))
}
}

Ok(())
Expand Down Expand Up @@ -159,6 +169,18 @@ fn process_input(
Ok(Ok((public_values, proof, report))) => {
verify_public_output(&io, &public_values)
.context("Failed to verify public output from proof")?;

// Save proof to disk if requested
if let Some(ref proofs_folder) = config.save_proofs_folder {
save_proof(
&proof,
&io.name(),
&zkvm_name,
proofs_folder,
config.sub_folder.as_deref(),
)?;
}

let verify_start = std::time::Instant::now();
let verif_public_values =
zkvm.verify(&proof).context("Failed to verify proof")?;
Expand All @@ -181,6 +203,11 @@ fn process_input(
};
(None, Some(proving))
}
Action::Verify => {
return Err(anyhow!(
"process_input should not be called with Action::Verify, use run_verify_from_disk"
))
}
};

let report = BenchmarkRun {
Expand All @@ -189,6 +216,7 @@ fn process_input(
metadata: io.metadata(),
execution,
proving,
verification: None,
};

info!("Saving report {}", io.name());
Expand All @@ -197,7 +225,7 @@ fn process_input(
Ok(())
}

fn get_panic_msg(panic_info: Box<dyn Any + Send>) -> String {
pub(crate) fn get_panic_msg(panic_info: Box<dyn Any + Send>) -> String {
panic_info
.downcast_ref::<&str>()
.map(|s| s.to_string())
Expand Down Expand Up @@ -281,6 +309,27 @@ fn dump_input(
Ok(())
}

/// Saves a proof's raw bytes to disk
fn save_proof(
proof: &ere_zkvm_interface::Proof,
name: &str,
zkvm_name: &str,
proofs_folder: &Path,
sub_folder: Option<&str>,
) -> Result<()> {
let proof_dir = proofs_folder.join(sub_folder.unwrap_or("")).join(zkvm_name);

fs::create_dir_all(&proof_dir)
.with_context(|| format!("Failed to create directory: {}", proof_dir.display()))?;

let proof_path = proof_dir.join(format!("{name}.proof"));
fs::write(&proof_path, proof.as_bytes())
.with_context(|| format!("Failed to write proof to {}", proof_path.display()))?;
info!("Saved proof to {}", proof_path.display());

Ok(())
}

fn verify_public_output(io: &impl GuestFixture, public_values: &[u8]) -> Result<()> {
match io.verify_public_values(public_values)? {
OutputVerifierResult::Match => Ok(()),
Expand Down
163 changes: 163 additions & 0 deletions crates/benchmark-runner/src/verification.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//! Proof verification from disk or remote URL

use anyhow::{anyhow, Context, Result};
use ere_dockerized::DockerizedzkVM;
use ere_zkvm_interface::{zkVM, ProofKind};
use std::fs;
use std::panic;
use std::path::{Path, PathBuf};
use tracing::info;
use zkevm_metrics::{BenchmarkRun, CrashInfo, HardwareInfo, VerificationMetrics};

use crate::runner::{get_panic_msg, RunConfig};

/// Loads proof artifacts from disk and verifies them using the given zkVM.
pub fn run_verify_from_disk(
zkvm: &DockerizedzkVM,
config: &RunConfig,
proofs_folder: &Path,
) -> Result<()> {
HardwareInfo::detect().to_path(config.output_folder.join("hardware.json"))?;

let zkvm_name = format!("{}-v{}", zkvm.name(), zkvm.sdk_version());
let proof_dir = proofs_folder
.join(config.sub_folder.as_deref().unwrap_or(""))
.join(&zkvm_name);

if !proof_dir.exists() {
info!("No proofs found for {zkvm_name} at {}", proof_dir.display());
return Ok(());
}

let proof_entries: Vec<_> = walkdir::WalkDir::new(&proof_dir)
.min_depth(1)
.max_depth(1)
.sort_by_file_name()
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
e.file_type().is_file() && e.path().extension().is_some_and(|ext| ext == "proof")
})
.collect();

// Warmup pass: verify the first proof to warm up the zkVM setup (if any).
if let Some(first) = proof_entries.first() {
info!(
"Warmup: verifying {} (result will be discarded)",
first.path().display()
);
let proof_bytes = fs::read(first.path())
.with_context(|| format!("Failed to read proof from {}", first.path().display()))?;
let proof = ere_zkvm_interface::Proof::new(ProofKind::Compressed, proof_bytes);
let _ = panic::catch_unwind(panic::AssertUnwindSafe(|| zkvm.verify(&proof)));
info!("Warmup complete");
}
Comment on lines +43 to +54
Copy link
Collaborator Author

@jsign jsign Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember the suggestion of using some zisk env whenever we use the cluster mode (i.e. rom setup and similars), but I feel this is a more general approach that can be better to have similar considerations for all zkvms.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense


for entry in &proof_entries {
let fixture_name = entry
.path()
.file_stem()
.ok_or_else(|| anyhow!("Invalid proof file name: {}", entry.path().display()))?
.to_string_lossy()
.to_string();

let out_path = config
.output_folder
.join(config.sub_folder.as_deref().unwrap_or(""))
.join(format!("{zkvm_name}/{fixture_name}.json"));

if !config.force_rerun && out_path.exists() {
info!("Skipping {fixture_name} (already exists)");
continue;
}

info!("Verifying proof for {fixture_name}");

let proof_bytes = fs::read(entry.path())
.with_context(|| format!("Failed to read proof from {}", entry.path().display()))?;
// NOTE: save_proof writes raw bytes from proof.as_bytes(), which strips the
// ProofKind discriminant. We reconstruct as Compressed here because the prove path
// always uses ProofKind::Compressed. If other proof kinds are added, the file
// format should be extended to store the proof kind.
let proof = ere_zkvm_interface::Proof::new(ProofKind::Compressed, proof_bytes);

let verify_start = std::time::Instant::now();
let verification_result =
panic::catch_unwind(panic::AssertUnwindSafe(|| zkvm.verify(&proof)));

let verification = match verification_result {
Ok(Ok(_public_values)) => {
let verification_time_ms = verify_start.elapsed().as_millis();
VerificationMetrics::Success {
proof_size: proof.as_bytes().len(),
verification_time_ms,
}
}
Ok(Err(e)) => VerificationMetrics::Crashed(CrashInfo {
reason: e.to_string(),
}),
Err(panic_info) => VerificationMetrics::Crashed(CrashInfo {
reason: get_panic_msg(panic_info),
}),
};

let report = BenchmarkRun {
name: fixture_name,
timestamp_completed: zkevm_metrics::chrono::Utc::now(),
metadata: serde_json::Value::Null,
execution: None,
proving: None,
verification: Some(verification),
};

info!("Saving verification report");
report.to_path(out_path)?;
}

Ok(())
}

/// Downloads a `.tar.gz` archive from a URL and extracts it to a temporary directory.
pub async fn download_and_extract_proofs(url: &str) -> Result<tempfile::TempDir> {
info!("Downloading proofs archive from {url}");
let client = reqwest::Client::new();
let response = client
.get(url)
.send()
.await
.context("Failed to send HTTP request for proofs archive")?
.error_for_status()
.context("HTTP error downloading proofs archive")?;

let bytes = response
.bytes()
.await
.context("Failed to read proofs archive response body")?;

info!("Downloaded {} bytes, extracting...", bytes.len());
let tmp = tempfile::tempdir().context("Failed to create temporary directory")?;

let decoder = flate2::read::GzDecoder::new(&bytes[..]);
let mut archive = tar::Archive::new(decoder);
archive
.unpack(tmp.path())
.context("Failed to extract proofs .tar.gz archive")?;

info!("Extracted proofs to {}", tmp.path().display());
Ok(tmp)
}

/// If the extracted archive contains a single top-level directory, return that
/// directory as the proofs root. Otherwise, return the extraction directory itself.
pub fn resolve_extracted_root(extracted: &Path) -> Result<PathBuf> {
let entries: Vec<_> = fs::read_dir(extracted)
.context("Failed to read extracted proofs directory")?
.filter_map(|e| e.ok())
.collect();

if entries.len() == 1 && entries[0].file_type().is_ok_and(|ft| ft.is_dir()) {
Ok(entries[0].path())
} else {
Ok(extracted.to_path_buf())
}
}
19 changes: 19 additions & 0 deletions crates/ere-hosts/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ pub struct Cli {
#[arg(long)]
pub dump_inputs: Option<PathBuf>,

/// Save generated proofs to the specified folder (only valid with --action prove)
#[arg(long)]
pub save_proofs: Option<PathBuf>,

/// Folder containing saved proofs (used with --action verify)
#[arg(
long,
default_value = "zkevm-fixtures-proofs",
conflicts_with = "proofs_url"
)]
pub proofs_folder: PathBuf,

/// URL to a .tar.gz archive containing proofs (used with --action verify).
#[arg(long, conflicts_with = "proofs_folder")]
pub proofs_url: Option<String>,

/// Base path for pre-compiled guest program binaries. If not set, they will be downloaded
/// from the latest ere-guests release.
#[arg(long)]
Expand Down Expand Up @@ -131,6 +147,8 @@ pub enum BenchmarkAction {
Execute,
/// Create a zkVM proof
Prove,
/// Verify proofs loaded from disk
Verify,
}

impl From<Resource> for ProverResourceType {
Expand All @@ -147,6 +165,7 @@ impl From<BenchmarkAction> for Action {
match action {
BenchmarkAction::Execute => Self::Execute,
BenchmarkAction::Prove => Self::Prove,
BenchmarkAction::Verify => Self::Verify,
}
}
}
Expand Down
Loading