diff --git a/.gitignore b/.gitignore
index 6e8b711..e8817cb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,5 @@ target/
*.tar.zst
manifest.json
-data/
\ No newline at end of file
+data/
+.worktrees/
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 7902c61..08b7544 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1096,6 +1096,18 @@ dependencies = [
"serde",
]
+[[package]]
+name = "async-compression"
+version = "0.4.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1"
+dependencies = [
+ "compression-codecs",
+ "compression-core",
+ "pin-project-lite",
+ "tokio",
+]
+
[[package]]
name = "async-lock"
version = "3.4.1"
@@ -1613,16 +1625,22 @@ version = "0.1.0"
dependencies = [
"alloy",
"anyhow",
+ "async-compression",
"bonsai-sdk",
"boundless-market",
"clap",
+ "futures-util",
"hex",
+ "reqwest 0.12.24",
"risc0-zkvm",
"serde",
"serde_json",
"sqlx",
"tabled",
+ "tempfile",
"tokio",
+ "tokio-tar",
+ "tokio-util",
"tracing",
"tracing-subscriber 0.3.20",
"url",
@@ -2000,7 +2018,7 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
- "windows-link",
+ "windows-link 0.2.1",
]
[[package]]
@@ -2088,6 +2106,23 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "compression-codecs"
+version = "0.4.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7"
+dependencies = [
+ "compression-core",
+ "zstd",
+ "zstd-safe",
+]
+
+[[package]]
+name = "compression-core"
+version = "0.4.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
+
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -2745,6 +2780,17 @@ dependencies = [
"subtle",
]
+[[package]]
+name = "filetime"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "libredox",
+]
+
[[package]]
name = "find-msvc-tools"
version = "0.1.4"
@@ -3247,6 +3293,22 @@ dependencies = [
"webpki-roots",
]
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
[[package]]
name = "hyper-util"
version = "0.1.17"
@@ -3266,9 +3328,11 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"socket2",
+ "system-configuration",
"tokio",
"tower-service",
"tracing",
+ "windows-registry",
]
[[package]]
@@ -3651,7 +3715,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
- "windows-link",
+ "windows-link 0.2.1",
]
[[package]]
@@ -3668,7 +3732,7 @@ checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
"bitflags 2.10.0",
"libc",
- "redox_syscall",
+ "redox_syscall 0.5.18",
]
[[package]]
@@ -4159,9 +4223,9 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
- "redox_syscall",
+ "redox_syscall 0.5.18",
"smallvec",
- "windows-link",
+ "windows-link 0.2.1",
]
[[package]]
@@ -4566,6 +4630,15 @@ dependencies = [
"rand_core 0.9.3",
]
+[[package]]
+name = "redox_syscall"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -4649,17 +4722,22 @@ checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
dependencies = [
"base64",
"bytes",
+ "encoding_rs",
"futures-channel",
"futures-core",
"futures-util",
+ "h2",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper",
"hyper-rustls",
+ "hyper-tls",
"hyper-util",
"js-sys",
"log",
+ "mime",
+ "native-tls",
"percent-encoding",
"pin-project-lite",
"quinn",
@@ -4670,6 +4748,7 @@ dependencies = [
"serde_urlencoded",
"sync_wrapper",
"tokio",
+ "tokio-native-tls",
"tokio-rustls",
"tokio-util",
"tower",
@@ -5953,6 +6032,27 @@ dependencies = [
"syn 2.0.108",
]
+[[package]]
+name = "system-configuration"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
+dependencies = [
+ "bitflags 2.10.0",
+ "core-foundation 0.9.4",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "tabled"
version = "0.20.0"
@@ -6192,6 +6292,21 @@ dependencies = [
"tokio-util",
]
+[[package]]
+name = "tokio-tar"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d5714c010ca3e5c27114c1cdeb9d14641ace49874aa5626d7149e47aedace75"
+dependencies = [
+ "filetime",
+ "futures-core",
+ "libc",
+ "redox_syscall 0.3.5",
+ "tokio",
+ "tokio-stream",
+ "xattr",
+]
+
[[package]]
name = "tokio-tungstenite"
version = "0.24.0"
@@ -6815,9 +6930,9 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
- "windows-link",
- "windows-result",
- "windows-strings",
+ "windows-link 0.2.1",
+ "windows-result 0.4.1",
+ "windows-strings 0.5.1",
]
[[package]]
@@ -6842,19 +6957,54 @@ dependencies = [
"syn 2.0.108",
]
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+[[package]]
+name = "windows-registry"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
+dependencies = [
+ "windows-link 0.1.3",
+ "windows-result 0.3.4",
+ "windows-strings 0.4.2",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
- "windows-link",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link 0.1.3",
]
[[package]]
@@ -6863,7 +7013,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
- "windows-link",
+ "windows-link 0.2.1",
]
[[package]]
@@ -6908,7 +7058,7 @@ version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
- "windows-link",
+ "windows-link 0.2.1",
]
[[package]]
@@ -6963,7 +7113,7 @@ version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
- "windows-link",
+ "windows-link 0.2.1",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
@@ -7184,6 +7334,16 @@ dependencies = [
"tap",
]
+[[package]]
+name = "xattr"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
+dependencies = [
+ "libc",
+ "rustix",
+]
+
[[package]]
name = "xmlparser"
version = "0.13.6"
@@ -7323,3 +7483,31 @@ dependencies = [
"quote",
"syn 2.0.108",
]
+
+[[package]]
+name = "zstd"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.16+zstd.1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 83196c4..02ca8b8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,3 +23,9 @@ tokio = "1.44"
tracing = "0.1"
tracing-subscriber = "0.3"
url = "2.5"
+reqwest = { version = "0.12", features = ["stream"] }
+async-compression = { version = "0.4", features = ["tokio", "zstd"] }
+tokio-tar = "0.3"
+tempfile = "3"
+tokio-util = { version = "0.7", features = ["io"] }
+futures-util = "0.3"
diff --git a/README.md b/README.md
index 8441588..b05e572 100644
--- a/README.md
+++ b/README.md
@@ -31,15 +31,20 @@ A collection of prepared suites are available:
> Note: Please open an issue if you would like to see other suites added.
-To fetch and untar:
+To fetch and run a suite directly:
```shell
-curl | tar -xv --zstd
+bento-bench run --fetch
```
-Once you have a data directory, benchmarks can be run with
+Example:
```shell
-bento-bench run \
- --data-dir ./data
+bento-bench run --fetch https://boundless-benchmarks.mintybasil.dev/suites/suite-og-4-1m-10m.tar.zst
+```
+
+Or download and extract manually:
+```shell
+curl | tar -xv --zstd
+bento-bench run --data-dir ./data
```
See `bento-bench run --help` for more configuration options.
diff --git a/crates/bento-bench/Cargo.toml b/crates/bento-bench/Cargo.toml
index b01069d..5208ed5 100644
--- a/crates/bento-bench/Cargo.toml
+++ b/crates/bento-bench/Cargo.toml
@@ -23,3 +23,9 @@ sqlx = { version = "0.8.6", features = ["postgres", "sqlite", "runtime-tokio", "
serde_json = "1.0.104"
tabled = "0.20.0"
url.workspace = true
+reqwest.workspace = true
+async-compression.workspace = true
+tokio-tar.workspace = true
+tempfile.workspace = true
+tokio-util.workspace = true
+futures-util.workspace = true
diff --git a/crates/bento-bench/src/commands/fetch.rs b/crates/bento-bench/src/commands/fetch.rs
new file mode 100644
index 0000000..941fd71
--- /dev/null
+++ b/crates/bento-bench/src/commands/fetch.rs
@@ -0,0 +1,174 @@
+use anyhow::{Context, Result, bail};
+use async_compression::tokio::bufread::ZstdDecoder;
+use futures_util::StreamExt;
+use std::path::PathBuf;
+use tokio::io::BufReader;
+use tokio_util::io::StreamReader;
+
+/// Download a `.tar.zst` suite from `url` and extract it into a temp directory.
+/// Returns the path to the extracted data directory.
+pub async fn fetch_suite(url: &str) -> Result {
+ tracing::info!("Fetching suite from {url}");
+
+ // Create a temp directory under the system temp dir.
+ // keep() converts TempDir into a plain PathBuf, preventing automatic
+ // deletion when the value is dropped. The directory persists for the
+ // lifetime of the process and the OS cleans it up on exit.
+ let data_dir = tempfile::tempdir()
+ .context("Failed to create temp directory")?
+ .keep();
+
+ // Download the archive with streaming
+ let response = reqwest::get(url)
+ .await
+ .with_context(|| format!("Failed to fetch suite from {url}"))?;
+
+ if !response.status().is_success() {
+ let status = response.status();
+ let body = response.text().await.unwrap_or_default();
+ bail!("Failed to fetch suite: HTTP {status}: {body}");
+ }
+
+ // Stream the response body through zstd decompression then tar extraction
+ let stream = response
+ .bytes_stream()
+ .map(|result| result.map_err(std::io::Error::other));
+ let reader = StreamReader::new(stream);
+ let buf_reader = BufReader::new(reader);
+ let zstd_reader = ZstdDecoder::new(buf_reader);
+ let mut archive = tokio_tar::Archive::new(zstd_reader);
+ archive
+ .unpack(&data_dir)
+ .await
+ .context("Failed to extract suite archive")?;
+
+ // Check if the archive extracted into a subdirectory (common for tar archives).
+ // If the data dir contains a single subdirectory and no manifest.json at top level,
+ // use that subdirectory as the data dir instead.
+ let effective_dir = if !data_dir.join("manifest.json").exists() {
+ let mut entries: Vec = std::fs::read_dir(&data_dir)
+ .context("Failed to read extracted directory")?
+ .filter_map(|e| e.ok())
+ .collect();
+
+ if entries.len() == 1
+ && entries[0].path().is_dir()
+ && entries[0].path().join("manifest.json").exists()
+ {
+ entries.remove(0).path()
+ } else {
+ data_dir.clone()
+ }
+ } else {
+ data_dir.clone()
+ };
+
+ // Validate essential files exist
+ if !effective_dir.join("manifest.json").exists() {
+ bail!("Fetched suite does not contain manifest.json");
+ }
+
+ tracing::info!("Suite extracted to {}", effective_dir.display());
+ Ok(effective_dir)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ /// Helper: create a .tar.zst archive containing a valid suite structure,
+ /// plus a flat variant (no wrapping directory).
+ fn create_test_suite_archive(wrap_in_subdir: bool) -> (tempfile::TempDir, PathBuf) {
+ let src_dir = tempfile::tempdir().unwrap();
+
+ let content_dir = if wrap_in_subdir {
+ let sub = src_dir.path().join("suite");
+ std::fs::create_dir_all(&sub).unwrap();
+ sub
+ } else {
+ src_dir.path().to_path_buf()
+ };
+
+ let images_dir = content_dir.join("images");
+ let inputs_dir = content_dir.join("inputs");
+ std::fs::create_dir_all(&images_dir).unwrap();
+ std::fs::create_dir_all(&inputs_dir).unwrap();
+
+ // Write manifest.json
+ let manifest = serde_json::json!({
+ "description": "test suite",
+ "entries": []
+ });
+ std::fs::write(content_dir.join("manifest.json"), manifest.to_string()).unwrap();
+
+ // Write dummy files
+ std::fs::write(images_dir.join("test.elf"), b"elf-content").unwrap();
+ std::fs::write(inputs_dir.join("test.input"), b"input-content").unwrap();
+
+ // Create .tar.zst archive using CLI tools
+ let archive_dir = tempfile::tempdir().unwrap();
+ let archive_path = archive_dir.path().join("suite.tar.zst");
+
+ let status = std::process::Command::new("tar")
+ .arg("--zstd")
+ .arg("-cf")
+ .arg(&archive_path)
+ .arg("-C")
+ .arg(src_dir.path())
+ .arg(if wrap_in_subdir { "suite" } else { "." })
+ .status()
+ .expect("tar command failed to start");
+ assert!(status.success(), "tar --zstd failed");
+
+ (archive_dir, archive_path)
+ }
+
+ /// Extract a .tar.zst file using the same pipeline as fetch_suite
+ /// (ZstdDecoder + tokio_tar), but reading from a local file.
+ async fn extract_archive(archive_path: &std::path::Path, dest: &std::path::Path) -> Result<()> {
+ let file = tokio::fs::File::open(archive_path).await?;
+ let buf_reader = tokio::io::BufReader::new(file);
+ let zstd_reader = ZstdDecoder::new(buf_reader);
+ let mut archive = tokio_tar::Archive::new(zstd_reader);
+ archive.unpack(dest).await?;
+ Ok(())
+ }
+
+ #[tokio::test]
+ async fn test_extract_flat_archive() {
+ let (_archive_dir, archive_path) = create_test_suite_archive(false);
+
+ let extract_dir = tempfile::tempdir().unwrap();
+ extract_archive(&archive_path, extract_dir.path())
+ .await
+ .expect("extraction failed");
+
+ assert!(
+ extract_dir.path().join("manifest.json").exists(),
+ "manifest.json should be at top level for flat archive"
+ );
+ }
+
+ #[tokio::test]
+ async fn test_extract_subdirectory_archive() {
+ let (_archive_dir, archive_path) = create_test_suite_archive(true);
+
+ let extract_dir = tempfile::tempdir().unwrap();
+ extract_archive(&archive_path, extract_dir.path())
+ .await
+ .expect("extraction failed");
+
+ // The archive was created with `tar -C suite`, so it contains
+ // a `suite/` directory. Files extract under extract_dir/suite/.
+ assert!(
+ extract_dir.path().join("suite/manifest.json").exists(),
+ "manifest.json should exist under suite/ subdirectory"
+ );
+ }
+
+ #[tokio::test]
+ async fn test_fetch_suite_rejects_bad_url() {
+ let result = fetch_suite("http://127.0.0.1:1/nonexistent.tar.zst").await;
+ assert!(result.is_err(), "expected error for bad URL");
+ }
+}
diff --git a/crates/bento-bench/src/commands/mod.rs b/crates/bento-bench/src/commands/mod.rs
index 07e1de5..5cfc8e6 100644
--- a/crates/bento-bench/src/commands/mod.rs
+++ b/crates/bento-bench/src/commands/mod.rs
@@ -10,6 +10,7 @@ mod prepare_local;
mod prepare_request;
mod run_bench;
+mod fetch;
mod manifest;
#[derive(Parser, Debug)]
diff --git a/crates/bento-bench/src/commands/prepare.rs b/crates/bento-bench/src/commands/prepare.rs
index 6019ede..923b4d2 100644
--- a/crates/bento-bench/src/commands/prepare.rs
+++ b/crates/bento-bench/src/commands/prepare.rs
@@ -7,7 +7,7 @@ use risc0_zkvm::{ExecutorEnv, compute_image_id, default_executor};
use std::path::{Path, PathBuf};
use tokio::fs;
-pub async fn fetch_image(url: &String, dir: &Path) -> Result {
+pub async fn fetch_image(url: &str, dir: &Path) -> Result {
let elf = StandardDownloader::new()
.await
.download_url(url.parse()?)
diff --git a/crates/bento-bench/src/commands/run_bench.rs b/crates/bento-bench/src/commands/run_bench.rs
index 28447b6..148b084 100644
--- a/crates/bento-bench/src/commands/run_bench.rs
+++ b/crates/bento-bench/src/commands/run_bench.rs
@@ -1,5 +1,6 @@
use crate::ProverConfig;
use crate::commands::CommonArgs;
+use crate::commands::fetch::fetch_suite;
use crate::commands::manifest::load_manifest;
use crate::prove::{prove_snark, prove_stark};
use anyhow::{Context, Result, anyhow};
@@ -17,6 +18,10 @@ pub struct RunArgs {
#[clap(flatten)]
common: CommonArgs,
+ /// Fetch a suite from a URL (.tar.zst) instead of using --data-dir
+ #[clap(long)]
+ fetch: Option,
+
/// Execute only (no prove)
#[clap(long, default_value_t = false)]
exec_only: bool,
@@ -101,7 +106,13 @@ pub struct BenchJsonOutput {
impl RunArgs {
pub async fn run(&self) -> Result<()> {
- let manifest = load_manifest(&self.common.data_dir, false)?;
+ let data_dir = if let Some(ref url) = self.fetch {
+ fetch_suite(url).await?
+ } else {
+ self.common.data_dir.clone()
+ };
+
+ let manifest = load_manifest(&data_dir, false)?;
self.prover_config
.proving_backend
@@ -110,7 +121,6 @@ impl RunArgs {
// Currently only support Bento, not default prover
let prover: BonsaiClient = BonsaiClient::from_env(risc0_zkvm::VERSION)?;
- let data_dir = self.common.data_dir.clone();
let images_dir = data_dir.join("images");
let inputs_dir = data_dir.join("inputs");
@@ -291,7 +301,7 @@ fn median(values: &mut [f64]) -> Option {
let len = values.len();
let mid = len / 2;
- if len % 2 == 0 {
+ if len.is_multiple_of(2) {
// Even number of elements: average the two middle values
Some((values[mid - 1] + values[mid]) / 2.0)
} else {