diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..621673b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,93 @@ +name: CI/CD + +on: + push: + branches: + - "main" + tags: + - "v*" + pull_request: + branches: + - "*" + +env: + CARGO_TERM_COLOR: always + STONE_SDK_VERSION: v0.3.0 + STONE_INSTALL_DIR: ./dependencies/stone + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: 1.74 + override: true + components: rustfmt, clippy + + - name: Set up cargo cache + uses: actions/cache@v3 + continue-on-error: false + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Set Stone SDK version in context + id: set-env-sdk-version + run: | + echo "STONE_SDK_VERSION=${STONE_SDK_VERSION}" >> $GITHUB_ENV + echo "STONE_INSTALL_DIR=${STONE_INSTALL_DIR}" >> $GITHUB_ENV + + - name: Cache Stone prover and verifier + id: cache-stone + uses: actions/cache@v4 + with: + path: ${{ env.STONE_INSTALL_DIR }} + key: stone-${{ runner.os }}-${{ env.STONE_SDK_VERSION }} + + - name: Download Stone + if: steps.cache-stone.outputs.cache-hit != 'true' + run: | + mkdir -p "${STONE_INSTALL_DIR}" + wget https://github.com/Moonsong-Labs/stone-prover-sdk/releases/download/${STONE_SDK_VERSION}/cpu_air_prover -O "${STONE_INSTALL_DIR}/cpu_air_prover" + wget https://github.com/Moonsong-Labs/stone-prover-sdk/releases/download/${STONE_SDK_VERSION}/cpu_air_verifier -O "${STONE_INSTALL_DIR}/cpu_air_verifier" + + - name: Set Stone in PATH + run: | + INSTALL_DIR=$(readlink -f ${STONE_INSTALL_DIR}) + echo "${INSTALL_DIR}" >> $GITHUB_PATH + chmod +x ${INSTALL_DIR}/cpu_air_prover + chmod +x ${INSTALL_DIR}/cpu_air_verifier + + - name: Lint with Clippy + run: | + cargo clippy -- -D warnings + + - name: Build + run: | + cargo build --release --verbose + + - name: Run tests + run: | + cargo test --verbose + + - name: Upload release artifacts + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + target/release/stone-prover-cli diff --git a/.gitignore b/.gitignore index 6985cf1..1a5d894 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,13 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb + +# Added by cargo +/target + +# IDE files +.idea/ + +# Stone prover and verifier +dependencies/cpu_air_prover +dependencies/cpu_air_verifier diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8df2ff1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "dependencies/cairo-programs"] + path = dependencies/cairo-programs + url = https://github.com/Moonsong-Labs/cairo-programs.git diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..94f2ccc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "stone-prover-cli" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cairo-vm = { git = "https://github.com/Moonsong-Labs/cairo-vm", rev = "b2f69d230416129a84ad8237ccc13d088992f74b", features = ["extensive_hints"] } +clap = { version = "4.5.0", features = ["derive"] } +serde = { version = "1.0.196", features = ["derive"] } +serde_json = { version = "1.0.113" } +stone-prover-sdk = { git = "https://github.com/Moonsong-Labs/stone-prover-sdk", tag="v0.3.0" } +thiserror = { version = "1.0.57" } + +[dev-dependencies] +rstest = "0.18.2" +tempfile = "3.10.0" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cfa859e --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +.PHONY: deps all-tests clean distclean unit-tests integration-tests + +PROVER_BIN=dependencies/cpu_air_prover +VERIFIER_BIN=dependencies/cpu_air_verifier + +$(PROVER_BIN): + wget -O dependencies/cpu_air_prover https://github.com/Moonsong-Labs/stone-prover-sdk/releases/download/v0.1.0-rc1/cpu_air_prover + +$(VERIFIER_BIN): + wget -O dependencies/cpu_air_verifier https://github.com/Moonsong-Labs/stone-prover-sdk/releases/download/v0.1.0-rc1/cpu_air_verifier + +all-tests: deps + cargo test + +itests: deps + cargo test --release --test '*' + +deps: $(PROVER_BIN) $(VERIFIER_BIN) +clean: + cargo clean + +distclean: clean + rm -rf dependencies/cpu_air_prover + rm -rf dependencies/cpu_air_verifier diff --git a/README.md b/README.md index 6c6b89d..acf0ba5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,30 @@ -# stone-prover-cli -A CLI to run and prove Cairo programs. +# Stone Prover CLI + +A CLI to run, prove and verify Cairo programs with a simple interface. + +Features: + +* Run, prove and verify any Cairo program +* Run programs directly or with the Starknet bootloader for + compatibility with the Starknet L1 verifier +* Automatic generation of the prover configuration and parameters. + +## Usage + +### Run and prove a single program + +```shell +stone-prover-cli prove program.json +``` + +### Run and prove one or more programs/PIEs with the Starknet bootloader + +```shell +stone-prover-cli prove --with-bootloader program1.json program2.json pie1.zip +``` + +### Verify a proof + +```shell +stone-prover-cli verify proof.json +``` \ No newline at end of file diff --git a/dependencies/cairo-programs b/dependencies/cairo-programs new file mode 160000 index 0000000..2e5b915 --- /dev/null +++ b/dependencies/cairo-programs @@ -0,0 +1 @@ +Subproject commit 2e5b915ec8666204f4932fb1989a8308c07334fe diff --git a/scripts/install-stone-cli.sh b/scripts/install-stone-cli.sh new file mode 100644 index 0000000..d254599 --- /dev/null +++ b/scripts/install-stone-cli.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +set -eo pipefail + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +INSTALL_DIR="${HOME}/.stone" + +while true; do + case "$1" in + -i | --install-dir ) INSTALL_DIR="$2"; shift 2 ;; + * ) break ;; + esac +done + +echo "Installing Stone in ${INSTALL_DIR}..." +mkdir -p "${INSTALL_DIR}" +wget https://github.com/Moonsong-Labs/stone-prover-sdk/releases/download/v0.2.0/cpu_air_prover -O "${INSTALL_DIR}/cpu_air_prover" +wget https://github.com/Moonsong-Labs/stone-prover-sdk/releases/download/v0.2.0/cpu_air_verifier -O "${INSTALL_DIR}/cpu_air_verifier" + +# Add the tool to the PATH +echo "Configuring PATH..." +if [[ ":$PATH:" != *":${INSTALL_DIR}:"* ]]; then + PROFILE_FILE="" + # ZSH_NAME is set on zsh + if [ -v ZSH_NAME ]; then + PROFILE_FILE="${HOME}/.zsh" + elif [ -v BASH ]; then + PROFILE_FILE="${HOME}/.bashrc" + else + echo "Unsupported shell, you will need to add the export PATH statement in the right configuration file manually." + fi + + if [ -n "${PROFILE_FILE}" ]; then + echo -e "\n# Stone prover and verifier\nexport PATH=\"${INSTALL_DIR}:\$PATH\"" >> "${PROFILE_FILE}" + fi +fi + +# Notify the user to update the PATH immediately +echo "Done!" +echo "Stone was added to ${PROFILE_FILE} and will be available the next time you open a shell." +echo "To add Stone to your PATH immediately, run the following command:" +echo "export PATH=\"${INSTALL_DIR}:\$PATH\"" \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..8cca025 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,130 @@ +use clap::error::ErrorKind; +use clap::{Args, CommandFactory, Parser, Subcommand}; +use std::borrow::Cow; +use std::path::{Path, PathBuf}; +use stone_prover_sdk::models::Layout; + +#[derive(Parser, Debug)] +#[command(name = "stone")] +#[command(bin_name = "stone")] +pub enum Cli { + Prove(ProveArgs), + Verify(VerifyArgs), +} + +#[derive(Args, Debug)] +#[command(args_conflicts_with_subcommands = true)] +#[command(flatten_help = true)] +pub struct ProveArgs { + #[clap(long = "with-bootloader", default_value_t = false)] + pub with_bootloader: bool, + + #[clap(long = "program-input")] + pub program_input: Option, + + #[clap(long = "layout")] + pub layout: Option, + + #[clap(flatten)] + pub config: ConfigArgs, + + #[arg(required=true, num_args=1..)] + pub programs: Vec, +} + +impl ProveArgs { + pub fn command(mut self) -> ProveCommand { + let mut cmd = Cli::command(); + if self.with_bootloader { + if self.program_input.is_some() { + cmd.error( + ErrorKind::ArgumentConflict, + "Cannot load program input in bootloader mode", + ) + .exit(); + } + } else if self.programs.len() > 1 { + cmd.error( + ErrorKind::ArgumentConflict, + "Cannot prove multiple programs without bootloader", + ) + .exit(); + } + + if self.with_bootloader { + let args = ProveWithBootloaderArgs { + programs: self.programs, + config: self.config, + }; + return ProveCommand::WithBootloader(args); + } + let args = ProveBareArgs { + program: self.programs.remove(0), + program_input: self.program_input, + layout: self.layout, + config: self.config, + }; + ProveCommand::Bare(args) + } +} + +#[derive(Subcommand, Debug)] +pub enum ProveCommand { + Bare(ProveBareArgs), + WithBootloader(ProveWithBootloaderArgs), +} + +impl ProveCommand { + pub fn config(&self) -> &ConfigArgs { + match self { + ProveCommand::Bare(args) => &args.config, + ProveCommand::WithBootloader(args) => &args.config, + } + } +} + +#[derive(Args, Debug)] +pub struct ProveBareArgs { + pub program: PathBuf, + + #[clap(long = "program-input")] + pub program_input: Option, + + #[clap(long = "layout")] + pub layout: Option, + + #[clap(flatten)] + pub config: ConfigArgs, +} + +#[derive(Args, Debug)] +pub struct ProveWithBootloaderArgs { + pub programs: Vec, + + #[clap(flatten)] + pub config: ConfigArgs, +} + +#[derive(Args, Clone, Debug)] +pub struct ConfigArgs { + #[clap(long = "prover-config-file")] + pub prover_config_file: Option, + #[clap(long = "parameter-file")] + pub parameter_file: Option, + #[clap(long = "output-file")] + output_file: Option, +} + +impl ConfigArgs { + pub fn output_file(&self) -> Cow { + match self.output_file.as_ref() { + Some(path) => Cow::Borrowed(path), + None => Cow::Owned(Path::new("proof.json").to_path_buf()), + } + } +} + +#[derive(Args, Clone, Debug)] +pub struct VerifyArgs { + pub proof_file: PathBuf, +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..041e0b8 --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,5 @@ +pub mod prove; +pub mod verify; + +pub use prove::prove; +pub use verify::verify; diff --git a/src/commands/prove.rs b/src/commands/prove.rs new file mode 100644 index 0000000..8ed8dd1 --- /dev/null +++ b/src/commands/prove.rs @@ -0,0 +1,164 @@ +use std::fs::File; +use std::path::{Path, PathBuf}; + +use cairo_vm::hint_processor::builtin_hint_processor::bootloader::types::{Task, TaskSpec}; +use cairo_vm::types::errors::cairo_pie_error::CairoPieError; +use cairo_vm::types::errors::program_errors::ProgramError; +use cairo_vm::types::program::Program; +use cairo_vm::vm::runners::cairo_pie::CairoPie; +use serde::Serialize; +use stone_prover_sdk::cairo_vm::{ + extract_execution_artifacts, run_bootloader_in_proof_mode, run_in_proof_mode, + ExecutionArtifacts, ExecutionError, +}; +use stone_prover_sdk::error::ProverError; +use stone_prover_sdk::fri::generate_prover_parameters; +use stone_prover_sdk::models::{Layout, ProverConfig}; +use stone_prover_sdk::prover::run_prover; + +use crate::cli::{ProveBareArgs, ProveCommand, ProveWithBootloaderArgs}; +use crate::toolkit::json::{read_json_from_file, ReadJsonError}; + +const BOOTLOADER_PROGRAM: &[u8] = + include_bytes!("../../dependencies/cairo-programs/bootloader/bootloader-v0.13.0.json"); + +fn write_json_to_file>(obj: T, path: P) -> Result<(), std::io::Error> { + let mut file = File::create(path)?; + serde_json::to_writer(&mut file, &obj)?; + Ok(()) +} + +#[derive(thiserror::Error, Debug)] +pub enum RunError { + #[error("Failed to read file {0}: {1}")] + Io(PathBuf, std::io::Error), + + #[error("Failed to deserialize {0}: {1}")] + Deserialize(PathBuf, ReadJsonError), + + #[error("Internal error: failed to load bootloader")] + FailedToLoadBootloader(ProgramError), + + #[error("Failed to load program {0}: {1}")] + FailedToLoadProgram(PathBuf, ProgramError), + + #[error("Failed to load PIE {0}: {1}")] + FailedToLoadPie(PathBuf, CairoPieError), + + #[error(transparent)] + FailedExecution(#[from] ExecutionError), + + #[error(transparent)] + Prover(#[from] ProverError), +} + +pub fn run_program(args: ProveBareArgs) -> Result { + let layout = args.layout.unwrap_or(Layout::StarknetWithKeccak); + + let program = std::fs::read(&args.program).map_err(|e| RunError::Io(args.program, e))?; + let (runner, vm) = run_in_proof_mode(&program, layout).map_err(ExecutionError::RunFailed)?; + extract_execution_artifacts(runner, vm).map_err(|e| e.into()) +} + +fn is_zip_file(file: &Path) -> bool { + match file.extension() { + Some(extension) => extension == "zip", + None => false, + } +} + +#[derive(thiserror::Error, Debug)] +enum TaskError { + #[error(transparent)] + Pie(#[from] CairoPieError), + + #[error(transparent)] + Program(#[from] ProgramError), +} + +fn task_from_file(file: &Path) -> Result { + let task = if is_zip_file(file) { + let pie = CairoPie::from_file(file)?; + Task::Pie(pie) + } else { + let program = Program::from_file(file, Some("main"))?; + Task::Program(program) + }; + + Ok(TaskSpec { task }) +} + +pub fn run_with_bootloader(args: ProveWithBootloaderArgs) -> Result { + let bootloader = Program::from_bytes(BOOTLOADER_PROGRAM, Some("main")) + .map_err(RunError::FailedToLoadBootloader)?; + let tasks: Result, RunError> = args + .programs + .into_iter() + .map(|path_buf| { + task_from_file(path_buf.as_path()).map_err(|e| match e { + TaskError::Pie(e) => RunError::FailedToLoadPie(path_buf, e), + TaskError::Program(e) => RunError::FailedToLoadProgram(path_buf, e), + }) + }) + .collect(); + let tasks = tasks?; + run_bootloader_in_proof_mode(&bootloader, tasks).map_err(|e| e.into()) +} + +pub fn prove(command: ProveCommand) -> Result<(), RunError> { + // Cloning here is the easiest solution to avoid borrow checks. + let config_args = command.config().clone(); + + let user_prover_config = config_args + .prover_config_file + .as_ref() + .map(|path| read_json_from_file(path).map_err(|e| RunError::Deserialize(path.clone(), e))) + .transpose()?; + let prover_config = user_prover_config.unwrap_or(ProverConfig::default()); + + let user_prover_parameters = config_args + .parameter_file + .as_ref() + .map(|path| read_json_from_file(path).map_err(|e| RunError::Deserialize(path.clone(), e))) + .transpose()?; + + let execution_artifacts = match command { + ProveCommand::Bare(args) => run_program(args)?, + ProveCommand::WithBootloader(args) => run_with_bootloader(args)?, + }; + + let last_layer_degree_bound = 64; + let prover_parameters = user_prover_parameters.unwrap_or(generate_prover_parameters( + execution_artifacts.public_input.n_steps, + last_layer_degree_bound, + )); + + let proof = run_prover( + &execution_artifacts.public_input, + &execution_artifacts.private_input, + &execution_artifacts.memory, + &execution_artifacts.trace, + &prover_config, + &prover_parameters, + )?; + + let output_file = config_args.output_file(); + write_json_to_file(proof, output_file.as_ref()) + .map_err(|e| RunError::Io(output_file.into_owned(), e))?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + use std::path::PathBuf; + + #[rstest] + #[case("cairo_pie.zip", true)] + #[case("program.json", false)] + fn test_is_zip_file(#[case] file: PathBuf, #[case] expected: bool) { + assert_eq!(is_zip_file(file.as_path()), expected); + } +} diff --git a/src/commands/verify.rs b/src/commands/verify.rs new file mode 100644 index 0000000..9020a0a --- /dev/null +++ b/src/commands/verify.rs @@ -0,0 +1,8 @@ +use stone_prover_sdk::error::VerifierError; +use stone_prover_sdk::verifier::run_verifier; + +use crate::cli::VerifyArgs; + +pub fn verify(args: VerifyArgs) -> Result<(), VerifierError> { + run_verifier(args.proof_file.as_path()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f6c87f6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,92 @@ +use cairo_vm::vm::errors::cairo_run_errors::CairoRunError; +use clap::Parser; +use stone_prover_sdk::cairo_vm::ExecutionError; +use stone_prover_sdk::error::VerifierError; + +use crate::cli::Cli; +use crate::commands::prove::RunError; + +mod cli; +mod commands; +mod toolkit; + +#[derive(thiserror::Error, Debug)] +enum CliError { + #[error(transparent)] + Prove(#[from] RunError), + #[error(transparent)] + Verify(#[from] VerifierError), +} + +fn display_error(error: CliError) { + let error_message = match error { + CliError::Prove(run_error) => match run_error { + RunError::Io(path_buf, io_error) => { + format!("could not read {}: {io_error}.", path_buf.to_string_lossy()) + } + RunError::Deserialize(path_buf, json_error) => { + format!( + "could not read JSON file {}: {json_error}.", + path_buf.to_string_lossy() + ) + } + RunError::FailedToLoadBootloader(program_error) => { + format!("failed to load bootloader program: {program_error}. This is an internal error and should not happen.") + } + RunError::FailedToLoadProgram(path_buf, program_error) => { + format!( + "failed to load program {}: {program_error}.", + path_buf.to_string_lossy() + ) + } + RunError::FailedToLoadPie(path_buf, pie_error) => { + format!( + "failed to load Cairo PIE {}: {pie_error}.", + path_buf.to_string_lossy() + ) + } + RunError::FailedExecution(execution_error) => match execution_error { + ExecutionError::RunFailed(cairo_run_error) => match cairo_run_error { + CairoRunError::Program(program_error) => { + format!("failed to load program: {program_error}") + } + other => format!("failed to run Cairo program: {other}"), + }, + other => format!("failed to extract VM output(s): {other}"), + }, + RunError::Prover(prover_error) => { + format!("failed to run prover: {prover_error}") + } + }, + CliError::Verify(e) => match e { + VerifierError::IoError(_) => { + "could not find verifier program. Is cpu_air_verifier installed?".to_string() + } + VerifierError::CommandError(command_output) => { + format!( + "failed to run verifier: {}", + String::from_utf8_lossy(&command_output.stderr) + ) + } + }, + }; + println!("Error: {}", error_message); +} + +fn process_cli_command(command: Cli) -> Result<(), CliError> { + match command { + Cli::Prove(prove_args) => commands::prove(prove_args.command())?, + Cli::Verify(verify_args) => commands::verify(verify_args)?, + }; + + Ok(()) +} + +fn main() -> Result<(), Box> { + let command = Cli::parse(); + if let Err(e) = process_cli_command(command) { + display_error(e); + } + + Ok(()) +} diff --git a/src/toolkit/json.rs b/src/toolkit/json.rs new file mode 100644 index 0000000..3df343f --- /dev/null +++ b/src/toolkit/json.rs @@ -0,0 +1,21 @@ +use serde::de::DeserializeOwned; +use std::fs::File; +use std::path::Path; + +#[derive(thiserror::Error, Debug)] +pub enum ReadJsonError { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Json(#[from] serde_json::Error), +} + +pub fn read_json_from_file>( + path: P, +) -> Result { + let file = File::open(path)?; + let mut reader = std::io::BufReader::new(file); + + let obj: T = serde_json::from_reader(&mut reader)?; + Ok(obj) +} diff --git a/src/toolkit/mod.rs b/src/toolkit/mod.rs new file mode 100644 index 0000000..22fdbb3 --- /dev/null +++ b/src/toolkit/mod.rs @@ -0,0 +1 @@ +pub mod json; diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..f4e10fb --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,31 @@ +use rstest::fixture; +use std::path::{Path, PathBuf}; + +/// Inspired by the `test_bin` crate +fn get_target_dir() -> PathBuf { + // Cargo puts the integration test binary in target//deps + let current_exe = std::env::current_exe().unwrap(); + let build_dir = current_exe.parent().unwrap().parent().unwrap(); + + build_dir.to_path_buf() +} + +#[fixture] +pub fn cli_in_path() { + // Add build dir to path for the duration of the test + let path = std::env::var("PATH").unwrap_or_default(); + + // The prover and verifier are downloaded in dependencies by the Makefile + let deps_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("dependencies"); + // The final binary is located in the target directory + let target_dir = get_target_dir(); + + std::env::set_var( + "PATH", + format!( + "{}:{}:{path}", + deps_dir.to_string_lossy(), + target_dir.to_string_lossy() + ), + ); +} diff --git a/tests/test_prove.rs b/tests/test_prove.rs new file mode 100644 index 0000000..1102edb --- /dev/null +++ b/tests/test_prove.rs @@ -0,0 +1,192 @@ +use std::path::Path; + +use cairo_vm::air_private_input::{AirPrivateInput, AirPrivateInputSerializable}; +use rstest::rstest; +use stone_prover_sdk::json::read_json_from_file; +use stone_prover_sdk::models::Proof; + +use crate::common::cli_in_path; + +mod common; + +fn invoke_cli( + with_bootloader: bool, + programs: &[&Path], + program_input: Option<&Path>, + prover_config: Option<&Path>, + prover_parameters: Option<&Path>, + output_file: Option<&Path>, +) -> Result { + let mut command = std::process::Command::new("stone-prover-cli"); + + command.arg("prove"); + + if with_bootloader { + command.arg("--with-bootloader"); + } + for program in programs { + command.arg(*program); + } + + if let Some(input_file) = program_input { + command.arg("--program-input").arg(input_file); + } + if let Some(config_file) = prover_config { + command.arg("--prover-config-file").arg(config_file); + } + if let Some(parameters_file) = prover_parameters { + command.arg("--parameter-file").arg(parameters_file); + } + if let Some(output_file) = output_file { + command.arg("--output-file").arg(output_file); + } + + command.output() +} + +fn assert_private_input_eq( + private_input: AirPrivateInputSerializable, + expected_private_input: AirPrivateInputSerializable, +) { + fn remove_file_keys(private_input: &mut AirPrivateInput) { + private_input.0.remove("trace_path"); + private_input.0.remove("memory_path"); + } + + let mut private_input = AirPrivateInput::from(private_input); + let mut expected_private_input = AirPrivateInput::from(expected_private_input); + + remove_file_keys(&mut private_input); + remove_file_keys(&mut expected_private_input); + + assert_eq!(private_input, expected_private_input); +} + +fn assert_proof_eq(proof: Proof, expected_proof: Proof) { + assert_private_input_eq(proof.private_input, expected_proof.private_input); + assert_eq!(proof.public_input, expected_proof.public_input); + assert_eq!(proof.prover_config, expected_proof.prover_config); + assert_eq!(proof.proof_parameters, expected_proof.proof_parameters); + assert_eq!(proof.proof_hex, expected_proof.proof_hex); +} + +#[rstest] +fn execute_and_prove_program( + #[from(cli_in_path)] _path: (), + #[values(true, false)] provide_config: bool, + #[values(true, false)] provide_parameters: bool, +) { + let output_dir = tempfile::tempdir().unwrap(); + let proof_file = output_dir.path().join("proof.json"); + + // Sanity check + assert!(!proof_file.exists()); + + let test_case_dir = + Path::new(env!("CARGO_MANIFEST_DIR")).join("dependencies/cairo-programs/cairo0/fibonacci"); + + let program = test_case_dir.join("fibonacci.json"); + let prover_config = test_case_dir.join("cpu_air_prover_config.json"); + let prover_parameters = test_case_dir.join("cpu_air_params.json"); + let expected_proof = test_case_dir.join("proof.json"); + + let prover_config = match provide_config { + true => Some(prover_config.as_path()), + false => None, + }; + + let prover_parameters = match provide_parameters { + true => Some(prover_parameters.as_path()), + false => None, + }; + + let result = invoke_cli( + false, + &vec![program.as_path()], + None, + prover_config, + prover_parameters, + Some(proof_file.as_path()), + ) + .expect("Command should succeed"); + + assert!( + result.status.success(), + "{}", + String::from_utf8(result.stderr).unwrap() + ); + + assert!(proof_file.exists()); + + let proof: Proof = read_json_from_file(proof_file).unwrap(); + let expected_proof: Proof = read_json_from_file(expected_proof).unwrap(); + assert_proof_eq(proof, expected_proof); +} + +#[rstest] +fn execute_and_prove_program_with_bootloader(#[from(cli_in_path)] _path: ()) { + let output_dir = tempfile::tempdir().unwrap(); + let proof_file = output_dir.path().join("proof.json"); + + // Sanity check + assert!(!proof_file.exists()); + + let test_case_dir = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("dependencies/cairo-programs/bootloader/programs/fibonacci"); + + let program = test_case_dir.join("program.json"); + + let result = invoke_cli( + true, + &vec![program.as_path()], + None, + None, + None, + Some(proof_file.as_path()), + ) + .expect("Command should succeed"); + + assert!( + result.status.success(), + "{}", + String::from_utf8(result.stderr).unwrap() + ); + + assert!(proof_file.exists()); +} + +#[rstest] +fn execute_and_prove_pie_with_bootloader(#[from(cli_in_path)] _path: ()) { + let output_dir = tempfile::tempdir().unwrap(); + let proof_file = output_dir.path().join("proof.json"); + + // Sanity check + assert!(!proof_file.exists()); + + let test_case_dir = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("dependencies/cairo-programs/bootloader/pies/fibonacci-stone-e2e"); + + let pie = test_case_dir.join("cairo_pie.zip"); + let expected_proof = test_case_dir.join("output/proof.json"); + + let result = invoke_cli( + true, + &vec![pie.as_path()], + None, + None, + None, + Some(proof_file.as_path()), + ) + .expect("Command should succeed"); + + assert!( + result.status.success(), + "{}", + String::from_utf8(result.stderr).unwrap() + ); + + assert!(proof_file.exists()); + let proof: Proof = read_json_from_file(proof_file).unwrap(); + let expected_proof: Proof = read_json_from_file(expected_proof).unwrap(); + assert_proof_eq(proof, expected_proof); +} diff --git a/tests/test_verify.rs b/tests/test_verify.rs new file mode 100644 index 0000000..6d865fb --- /dev/null +++ b/tests/test_verify.rs @@ -0,0 +1,32 @@ +use std::path::Path; + +use rstest::rstest; + +use crate::common::cli_in_path; + +mod common; + +fn invoke_cli(proof_file: &Path) -> Result { + let mut command = std::process::Command::new("stone-prover-cli"); + command.arg("verify").arg(proof_file); + + command.output() +} + +#[rstest] +fn test_verify_program(#[from(cli_in_path)] _path: ()) { + let test_case_dir = + Path::new(env!("CARGO_MANIFEST_DIR")).join("dependencies/cairo-programs/cairo0/fibonacci"); + let proof_file = test_case_dir.join("proof.json"); + + invoke_cli(proof_file.as_path()).expect("Command should succeed"); +} + +#[rstest] +fn test_verify_pie_with_bootloader(#[from(cli_in_path)] _path: ()) { + let test_case_dir = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("dependencies/cairo-programs/bootloader/fibonacci-stone-e2e"); + let proof_file = test_case_dir.join("output/proof.json"); + + invoke_cli(proof_file.as_path()).expect("Command should succeed"); +}