From a0a29cbb7d0410ea8a7422446c4d72ceebdab2df Mon Sep 17 00:00:00 2001 From: Han Date: Thu, 14 Aug 2025 22:00:23 +0800 Subject: [PATCH] Add crate `test-utils` (#82) --- Cargo.lock | 10 ++ Cargo.toml | 2 + crates/ere-dockerized/Cargo.toml | 1 + crates/ere-dockerized/src/lib.rs | 17 +- crates/ere-sp1/Cargo.toml | 9 +- crates/ere-sp1/src/compile.rs | 48 +----- crates/ere-sp1/src/lib.rs | 188 +++++++---------------- crates/test-utils/Cargo.toml | 19 +++ crates/test-utils/src/guest.rs | 28 ++++ crates/test-utils/src/host.rs | 73 +++++++++ crates/test-utils/src/lib.rs | 8 + tests/sp1/{compile => }/basic/Cargo.toml | 1 + tests/sp1/basic/src/main.rs | 18 +++ tests/sp1/compile/basic/src/main.rs | 10 -- tests/sp1/execute/basic/Cargo.toml | 9 -- tests/sp1/execute/basic/src/main.rs | 10 -- tests/sp1/prove/basic/Cargo.toml | 9 -- tests/sp1/prove/basic/src/main.rs | 10 -- 18 files changed, 233 insertions(+), 237 deletions(-) create mode 100644 crates/test-utils/Cargo.toml create mode 100644 crates/test-utils/src/guest.rs create mode 100644 crates/test-utils/src/host.rs create mode 100644 crates/test-utils/src/lib.rs rename tests/sp1/{compile => }/basic/Cargo.toml (69%) create mode 100644 tests/sp1/basic/src/main.rs delete mode 100644 tests/sp1/compile/basic/src/main.rs delete mode 100644 tests/sp1/execute/basic/Cargo.toml delete mode 100644 tests/sp1/execute/basic/src/main.rs delete mode 100644 tests/sp1/prove/basic/Cargo.toml delete mode 100644 tests/sp1/prove/basic/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index ac8e91e..7616af5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2276,6 +2276,7 @@ dependencies = [ "risc0-zkvm", "serde", "tempfile", + "test-utils", "thiserror 2.0.12", "zkvm-interface", ] @@ -2367,6 +2368,7 @@ dependencies = [ "build-utils", "sp1-sdk", "tempfile", + "test-utils", "thiserror 2.0.12", "toml", "tracing", @@ -9410,6 +9412,14 @@ dependencies = [ "test-case-core", ] +[[package]] +name = "test-utils" +version = "0.0.11" +dependencies = [ + "serde", + "zkvm-interface", +] + [[package]] name = "thiserror" version = "1.0.69" diff --git a/Cargo.toml b/Cargo.toml index 8d59cf7..1fb420d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crates/build-utils", + "crates/test-utils", # zkVMs "crates/ere-jolt", "crates/ere-nexus", @@ -77,6 +78,7 @@ sp1-sdk = "5.1.0" # Local dependencies zkvm-interface = { path = "crates/zkvm-interface" } build-utils = { path = "crates/build-utils" } +test-utils = { path = "crates/test-utils" } ere-cli = { path = "crates/ere-cli", default-features = false } ere-dockerized = { path = "crates/ere-dockerized" } ere-jolt = { path = "crates/ere-jolt" } diff --git a/crates/ere-dockerized/Cargo.toml b/crates/ere-dockerized/Cargo.toml index 746792d..ccb4ef5 100644 --- a/crates/ere-dockerized/Cargo.toml +++ b/crates/ere-dockerized/Cargo.toml @@ -20,6 +20,7 @@ zkvm-interface = { workspace = true, features = ["clap"] } ere-cli.workspace = true [dev-dependencies] +test-utils = { workspace = true, features = ["host"] } [build-dependencies] build-utils.workspace = true diff --git a/crates/ere-dockerized/src/lib.rs b/crates/ere-dockerized/src/lib.rs index 8cb0abb..cbfbaa7 100644 --- a/crates/ere-dockerized/src/lib.rs +++ b/crates/ere-dockerized/src/lib.rs @@ -453,6 +453,9 @@ fn workspace_dir() -> PathBuf { #[cfg(test)] mod test { use crate::{EreDockerizedCompiler, EreDockerizedzkVM, ErezkVM, workspace_dir}; + use test_utils::host::{ + BasicProgramInputGen, run_zkvm_execute, run_zkvm_prove, testing_guest_directory, + }; use zkvm_interface::{Compiler, Input, ProverResourceType, zkVM}; // TODO: Test other ere-{zkvm} when they are end-to-end ready: @@ -506,22 +509,16 @@ mod test { fn dockerized_sp1() { let zkvm = ErezkVM::SP1; - let guest_directory = workspace_dir().join(format!("tests/{zkvm}/prove/basic")); + let guest_directory = testing_guest_directory(zkvm.as_str(), "basic"); let program = EreDockerizedCompiler::new(zkvm, workspace_dir()) .compile(&guest_directory) .unwrap(); let zkvm = EreDockerizedzkVM::new(zkvm, program, ProverResourceType::Cpu).unwrap(); - let mut inputs = Input::new(); - inputs.write(42u32); - inputs.write(42u16); - - let _report = zkvm.execute(&inputs).unwrap(); - - let (proof, _report) = zkvm.prove(&inputs).unwrap(); - - zkvm.verify(&proof).unwrap(); + let inputs = BasicProgramInputGen::valid(); + run_zkvm_execute(&zkvm, &inputs); + run_zkvm_prove(&zkvm, &inputs); } #[test] diff --git a/crates/ere-sp1/Cargo.toml b/crates/ere-sp1/Cargo.toml index fe68695..827edb7 100644 --- a/crates/ere-sp1/Cargo.toml +++ b/crates/ere-sp1/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "ere-sp1" -version.workspace = true edition.workspace = true -rust-version.workspace = true license.workspace = true +name = "ere-sp1" +rust-version.workspace = true +version.workspace = true [dependencies] bincode.workspace = true @@ -18,5 +18,8 @@ sp1-sdk.workspace = true # Local dependencies zkvm-interface.workspace = true +[dev-dependencies] +test-utils = { workspace = true, features = ["host"] } + [build-dependencies] build-utils.workspace = true diff --git a/crates/ere-sp1/src/compile.rs b/crates/ere-sp1/src/compile.rs index 66b009f..2b13b5e 100644 --- a/crates/ere-sp1/src/compile.rs +++ b/crates/ere-sp1/src/compile.rs @@ -95,50 +95,14 @@ pub fn compile(guest_directory: &Path) -> Result, CompileError> { #[cfg(test)] mod tests { + use crate::RV32_IM_SUCCINCT_ZKVM_ELF; + use test_utils::host::testing_guest_directory; use zkvm_interface::Compiler; - use crate::RV32_IM_SUCCINCT_ZKVM_ELF; - - use super::*; - use std::path::PathBuf; - - // TODO: for now, we just get one test file - // TODO: but this should get the whole directory and compile each test - fn get_compile_test_guest_program_path() -> PathBuf { - let workspace_dir = env!("CARGO_WORKSPACE_DIR"); - PathBuf::from(workspace_dir) - .join("tests") - .join("sp1") - .join("compile") - .join("basic") - .canonicalize() - .expect("Failed to find or canonicalize test guest program at /tests/compile/sp1") - } - #[test] - fn test_compile_sp1_program() { - let test_guest_path = get_compile_test_guest_program_path(); - - match compile(&test_guest_path) { - Ok(elf_bytes) => { - assert!(!elf_bytes.is_empty(), "ELF bytes should not be empty."); - } - Err(err) => { - panic!("compile failed for dedicated guest: {err}"); - } - } - } - - #[test] - fn test_compile_trait() { - let test_guest_path = get_compile_test_guest_program_path(); - match RV32_IM_SUCCINCT_ZKVM_ELF.compile(&test_guest_path) { - Ok(elf_bytes) => { - assert!(!elf_bytes.is_empty(), "ELF bytes should not be empty."); - } - Err(err) => { - panic!("compile_sp1_program direct call failed for dedicated guest: {err}"); - } - } + fn test_compiler_impl() { + let guest_directory = testing_guest_directory("sp1", "basic"); + let elf_bytes = RV32_IM_SUCCINCT_ZKVM_ELF.compile(&guest_directory).unwrap(); + assert!(!elf_bytes.is_empty(), "ELF bytes should not be empty."); } } diff --git a/crates/ere-sp1/src/lib.rs b/crates/ere-sp1/src/lib.rs index 8a3eaba..5e1b05d 100644 --- a/crates/ere-sp1/src/lib.rs +++ b/crates/ere-sp1/src/lib.rs @@ -225,127 +225,74 @@ fn serialize_inputs(stdin: &mut SP1Stdin, inputs: &Input) { } #[cfg(test)] -mod execute_tests { - use std::path::PathBuf; - +mod tests { use super::*; - use zkvm_interface::Input; + use std::{panic, sync::OnceLock}; + use test_utils::host::{ + BasicProgramInputGen, run_zkvm_execute, run_zkvm_prove, testing_guest_directory, + }; - fn get_compiled_test_sp1_elf() -> Result, SP1Error> { - let test_guest_path = get_execute_test_guest_program_path(); - RV32_IM_SUCCINCT_ZKVM_ELF.compile(&test_guest_path) - } + static BASIC_PRORGAM: OnceLock> = OnceLock::new(); - fn get_execute_test_guest_program_path() -> PathBuf { - let workspace_dir = env!("CARGO_WORKSPACE_DIR"); - PathBuf::from(workspace_dir) - .join("tests") - .join("sp1") - .join("execute") - .join("basic") - .canonicalize() - .expect("Failed to find or canonicalize test guest program at /tests/execute/sp1") + fn basic_program() -> Vec { + BASIC_PRORGAM + .get_or_init(|| { + RV32_IM_SUCCINCT_ZKVM_ELF + .compile(&testing_guest_directory("sp1", "basic")) + .unwrap() + }) + .to_vec() } #[test] - fn test_execute_sp1_dummy_input() { - let elf_bytes = get_compiled_test_sp1_elf() - .expect("Failed to compile test SP1 guest for execution test"); + fn test_execute() { + let program = basic_program(); + let zkvm = EreSP1::new(program, ProverResourceType::Cpu); - let mut input_builder = Input::new(); - let n: u32 = 42; - let a: u16 = 42; - input_builder.write(n); - input_builder.write(a); + let inputs = BasicProgramInputGen::valid(); + run_zkvm_execute(&zkvm, &inputs); + } - let zkvm = EreSP1::new(elf_bytes, ProverResourceType::Cpu); + #[test] + fn test_execute_invalid_inputs() { + let program = basic_program(); + let zkvm = EreSP1::new(program, ProverResourceType::Cpu); - let result = zkvm.execute(&input_builder); - - if let Err(err) = &result { - panic!("Execution error: {err}"); + for inputs in [ + BasicProgramInputGen::empty(), + BasicProgramInputGen::invalid_string(), + BasicProgramInputGen::invalid_type(), + ] { + zkvm.execute(&inputs).unwrap_err(); } } #[test] - fn test_execute_sp1_no_input_for_guest_expecting_input() { - let elf_bytes = get_compiled_test_sp1_elf() - .expect("Failed to compile test SP1 guest for execution test"); + fn test_prove() { + let program = basic_program(); + let zkvm = EreSP1::new(program, ProverResourceType::Cpu); - let empty_input = Input::new(); - - let zkvm = EreSP1::new(elf_bytes, ProverResourceType::Cpu); - let result = zkvm.execute(&empty_input); - - assert!( - result.is_err(), - "execute should fail if guest expects input but none is provided." - ); - } -} - -#[cfg(test)] -mod prove_tests { - use std::path::PathBuf; - - use super::*; - use zkvm_interface::Input; - - fn get_prove_test_guest_program_path() -> PathBuf { - let workspace_dir = env!("CARGO_WORKSPACE_DIR"); - PathBuf::from(workspace_dir) - .join("tests") - .join("sp1") - .join("prove") - .join("basic") - .canonicalize() - .expect("Failed to find or canonicalize test guest program at /tests/execute/sp1") - } - - fn get_compiled_test_sp1_elf_for_prove() -> Result, SP1Error> { - let test_guest_path = get_prove_test_guest_program_path(); - RV32_IM_SUCCINCT_ZKVM_ELF.compile(&test_guest_path) + let inputs = BasicProgramInputGen::valid(); + run_zkvm_prove(&zkvm, &inputs); } #[test] - fn test_prove_sp1_dummy_input() { - let elf_bytes = get_compiled_test_sp1_elf_for_prove() - .expect("Failed to compile test SP1 guest for proving test"); + fn test_prove_invalid_inputs() { + let program = basic_program(); + let zkvm = EreSP1::new(program, ProverResourceType::Cpu); - let mut input_builder = Input::new(); - let n: u32 = 42; - let a: u16 = 42; - input_builder.write(n); - input_builder.write(a); - - let zkvm = EreSP1::new(elf_bytes, ProverResourceType::Cpu); - - let proof_bytes = match zkvm.prove(&input_builder) { - Ok((prove_result, _)) => prove_result, - Err(err) => { - panic!("Proving error in test: {err}"); - } - }; - - assert!(!proof_bytes.is_empty(), "Proof bytes should not be empty."); - - let verify_results = zkvm.verify(&proof_bytes).is_ok(); - assert!(verify_results); - - // TODO: Check public inputs - } - - #[test] - #[should_panic] - fn test_prove_sp1_fails_on_bad_input_causing_execution_failure() { - let elf_bytes = get_compiled_test_sp1_elf_for_prove() - .expect("Failed to compile test SP1 guest for proving test"); - - let empty_input = Input::new(); - - let zkvm = EreSP1::new(elf_bytes, ProverResourceType::Cpu); - let prove_result = zkvm.prove(&empty_input); - assert!(prove_result.is_err()) + // On invalid inputs SP1 prove will panics, the issue for tracking: + // https://github.com/eth-act/ere/issues/16. + // + // Note that we iterate on methods because `InputItem::Object` doesn't + // implement `RefUnwindSafe`. + for inputs_gen in [ + BasicProgramInputGen::empty, + BasicProgramInputGen::invalid_string, + BasicProgramInputGen::invalid_type, + ] { + panic::catch_unwind(|| zkvm.prove(&inputs_gen())).unwrap_err(); + } } #[test] @@ -357,42 +304,15 @@ mod prove_tests { return; } - let elf_bytes = get_compiled_test_sp1_elf_for_prove() - .expect("Failed to compile test SP1 guest for proving test"); - - let mut input_builder = Input::new(); - let n: u32 = 42; - let a: u16 = 42; - input_builder.write(n); - input_builder.write(a); - // Create a network prover configuration let network_config = NetworkProverConfig { endpoint: std::env::var("NETWORK_RPC_URL").unwrap_or_default(), api_key: std::env::var("NETWORK_PRIVATE_KEY").ok(), }; + let program = basic_program(); + let zkvm = EreSP1::new(program, ProverResourceType::Network(network_config)); - let zkvm = EreSP1::new(elf_bytes, ProverResourceType::Network(network_config)); - - // Execute first to ensure the program works - let exec_result = zkvm.execute(&input_builder); - assert!(exec_result.is_ok(), "Execution should succeed"); - - // Now prove using the network - let proof_bytes = match zkvm.prove(&input_builder) { - Ok((prove_result, report)) => { - println!("Network proving completed in {:?}", report.proving_time); - prove_result - } - Err(err) => { - panic!("Network proving error: {err}"); - } - }; - - assert!(!proof_bytes.is_empty(), "Proof bytes should not be empty."); - - // Verify the proof - let verify_result = zkvm.verify(&proof_bytes); - assert!(verify_result.is_ok(), "Verification should succeed"); + let inputs = BasicProgramInputGen::valid(); + run_zkvm_prove(&zkvm, &inputs); } } diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml new file mode 100644 index 0000000..4d36779 --- /dev/null +++ b/crates/test-utils/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "test-utils" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde = { workspace = true, features = ["derive"] } + +# Local dependencies +zkvm-interface = { workspace = true, optional = true } + +[lints] +workspace = true + +[features] +default = [] +host = ["dep:zkvm-interface"] diff --git a/crates/test-utils/src/guest.rs b/crates/test-utils/src/guest.rs new file mode 100644 index 0000000..41defa5 --- /dev/null +++ b/crates/test-utils/src/guest.rs @@ -0,0 +1,28 @@ +use alloc::vec::Vec; +use core::iter; +use serde::{Deserialize, Serialize}; + +#[derive(Default, Serialize, Deserialize)] +pub struct BasicStruct { + pub a: u8, + pub b: u16, + pub c: u32, + pub d: u64, + pub e: Vec, +} + +impl BasicStruct { + /// Performs some computation (Xoring all fields as bytes into `[u8; 32]`). + pub fn output(&self) -> [u8; 32] { + let mut output = [0; 32]; + iter::empty() + .chain(self.a.to_le_bytes()) + .chain(self.b.to_le_bytes()) + .chain(self.c.to_le_bytes()) + .chain(self.d.to_le_bytes()) + .chain(self.e.iter().copied()) + .enumerate() + .for_each(|(idx, byte)| output[idx % output.len()] ^= byte); + output + } +} diff --git a/crates/test-utils/src/host.rs b/crates/test-utils/src/host.rs new file mode 100644 index 0000000..ece0f0f --- /dev/null +++ b/crates/test-utils/src/host.rs @@ -0,0 +1,73 @@ +use crate::guest::BasicStruct; +use std::path::PathBuf; +use zkvm_interface::{Input, zkVM}; + +fn workspace() -> PathBuf { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.pop(); + path.pop(); + path +} + +pub fn testing_guest_directory(zkvm_name: &str, program: &str) -> PathBuf { + workspace().join("tests").join(zkvm_name).join(program) +} + +pub fn run_zkvm_execute(zkvm: &impl zkVM, inputs: &Input) { + let _report = zkvm + .execute(inputs) + .expect("execute should not fail with valid input"); + + // TODO: Check output are expected. +} + +pub fn run_zkvm_prove(zkvm: &impl zkVM, inputs: &Input) { + let (proof, _report) = zkvm + .prove(inputs) + .expect("prove should not fail with valid input"); + + zkvm.verify(&proof) + .expect("verify should not fail with valid input"); + + // TODO: Check output are expected. +} + +/// The basic program takes 2 inputs: +/// - `Vec` that supposed to be "Hello world" +/// - `BasicStruct` +/// +/// Outputs `[u8; 32]` which computed by xoring fields of `BasicStruct`. +pub struct BasicProgramInputGen; + +impl BasicProgramInputGen { + pub fn valid() -> Input { + let mut inputs = Input::new(); + inputs.write_bytes("Hello world".as_bytes().to_vec()); + inputs.write(BasicStruct { + a: 0xff, + b: 0x7777, + c: 0xffffffff, + d: 0x7777777777777777, + e: (0..u8::MAX).collect(), + }); + inputs + } + + pub fn invalid_string() -> Input { + let mut inputs = Input::new(); + inputs.write_bytes("Unexpected string".as_bytes().to_vec()); + inputs.write(BasicStruct::default()); + inputs + } + + pub fn invalid_type() -> Input { + let mut inputs = Input::new(); + inputs.write(BasicStruct::default()); + inputs.write_bytes("Hello world".as_bytes().to_vec()); + inputs + } + + pub fn empty() -> Input { + Input::new() + } +} diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs new file mode 100644 index 0000000..0442a0b --- /dev/null +++ b/crates/test-utils/src/lib.rs @@ -0,0 +1,8 @@ +#![cfg_attr(not(feature = "host"), no_std)] + +extern crate alloc; + +pub mod guest; + +#[cfg(feature = "host")] +pub mod host; diff --git a/tests/sp1/compile/basic/Cargo.toml b/tests/sp1/basic/Cargo.toml similarity index 69% rename from tests/sp1/compile/basic/Cargo.toml rename to tests/sp1/basic/Cargo.toml index 6ba65ff..bcfbd42 100644 --- a/tests/sp1/compile/basic/Cargo.toml +++ b/tests/sp1/basic/Cargo.toml @@ -7,3 +7,4 @@ edition = "2021" [dependencies] sp1-zkvm = "5.1.0" +test-utils = { path = "../../../crates/test-utils" } diff --git a/tests/sp1/basic/src/main.rs b/tests/sp1/basic/src/main.rs new file mode 100644 index 0000000..2fff5c3 --- /dev/null +++ b/tests/sp1/basic/src/main.rs @@ -0,0 +1,18 @@ +#![no_main] + +use test_utils::guest::BasicStruct; + +sp1_zkvm::entrypoint!(main); + +pub fn main() { + // Read `Hello world` bytes. + let bytes = sp1_zkvm::io::read_vec(); + assert_eq!(String::from_utf8_lossy(&bytes), "Hello world"); + + // Read `BasicStruct`. + let basic_struct = sp1_zkvm::io::read::(); + let output = basic_struct.output(); + + // Write `output` + sp1_zkvm::io::commit(&output); +} diff --git a/tests/sp1/compile/basic/src/main.rs b/tests/sp1/compile/basic/src/main.rs deleted file mode 100644 index 32a3566..0000000 --- a/tests/sp1/compile/basic/src/main.rs +++ /dev/null @@ -1,10 +0,0 @@ -#![no_main] - -sp1_zkvm::entrypoint!(main); - -pub fn main() { - // Read an input - let n = sp1_zkvm::io::read::(); - // Write n*2 to output - sp1_zkvm::io::commit(&(n * 2)); -} \ No newline at end of file diff --git a/tests/sp1/execute/basic/Cargo.toml b/tests/sp1/execute/basic/Cargo.toml deleted file mode 100644 index 6ba65ff..0000000 --- a/tests/sp1/execute/basic/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "ere-test-sp1-guest" -version = "0.1.0" -edition = "2021" - -[workspace] - -[dependencies] -sp1-zkvm = "5.1.0" diff --git a/tests/sp1/execute/basic/src/main.rs b/tests/sp1/execute/basic/src/main.rs deleted file mode 100644 index cbd6bf7..0000000 --- a/tests/sp1/execute/basic/src/main.rs +++ /dev/null @@ -1,10 +0,0 @@ -#![no_main] - -sp1_zkvm::entrypoint!(main); -pub fn main() { - // Read an input - let n = sp1_zkvm::io::read::(); - let a = sp1_zkvm::io::read::() as u32; - - sp1_zkvm::io::commit(&((n + a) * 2)); -} diff --git a/tests/sp1/prove/basic/Cargo.toml b/tests/sp1/prove/basic/Cargo.toml deleted file mode 100644 index 6ba65ff..0000000 --- a/tests/sp1/prove/basic/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "ere-test-sp1-guest" -version = "0.1.0" -edition = "2021" - -[workspace] - -[dependencies] -sp1-zkvm = "5.1.0" diff --git a/tests/sp1/prove/basic/src/main.rs b/tests/sp1/prove/basic/src/main.rs deleted file mode 100644 index cbd6bf7..0000000 --- a/tests/sp1/prove/basic/src/main.rs +++ /dev/null @@ -1,10 +0,0 @@ -#![no_main] - -sp1_zkvm::entrypoint!(main); -pub fn main() { - // Read an input - let n = sp1_zkvm::io::read::(); - let a = sp1_zkvm::io::read::() as u32; - - sp1_zkvm::io::commit(&((n + a) * 2)); -}