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 {