From fdfe2bf752771b9611dc71953d50423b4ae7ec44 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Wed, 19 Feb 2025 18:07:05 +0000 Subject: [PATCH] feat(cli): add noir-execute binary (#7384) Co-authored-by: Tom French Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com> --- Cargo.lock | 26 ++- Cargo.toml | 3 + acvm-repo/acir/src/circuit/mod.rs | 1 + compiler/noirc_errors/src/reporter.rs | 1 + tooling/acvm_cli/Cargo.toml | 11 +- tooling/acvm_cli/src/cli/execute_cmd.rs | 18 +- tooling/acvm_cli/src/cli/fs/inputs.rs | 54 ----- tooling/acvm_cli/src/cli/fs/mod.rs | 2 - tooling/acvm_cli/src/cli/fs/witness.rs | 63 ------ tooling/acvm_cli/src/cli/mod.rs | 1 - tooling/acvm_cli/src/errors.rs | 50 ----- tooling/acvm_cli/src/fs/mod.rs | 1 + tooling/acvm_cli/src/fs/witness.rs | 60 +++++ tooling/acvm_cli/src/main.rs | 3 +- tooling/artifact_cli/Cargo.toml | 43 ++++ tooling/artifact_cli/src/bin/execute.rs | 46 ++++ .../artifact_cli/src/commands/execute_cmd.rs | 209 ++++++++++++++++++ tooling/artifact_cli/src/commands/mod.rs | 17 ++ tooling/artifact_cli/src/errors.rs | 64 ++++++ tooling/artifact_cli/src/fs/artifact.rs | 37 ++++ tooling/artifact_cli/src/fs/inputs.rs | 56 +++++ tooling/artifact_cli/src/fs/mod.rs | 3 + tooling/artifact_cli/src/fs/witness.rs | 27 +++ tooling/artifact_cli/src/lib.rs | 12 + tooling/noirc_abi/src/input_parser/mod.rs | 20 +- tooling/noirc_artifacts/src/contract.rs | 4 +- 26 files changed, 644 insertions(+), 188 deletions(-) delete mode 100644 tooling/acvm_cli/src/cli/fs/inputs.rs delete mode 100644 tooling/acvm_cli/src/cli/fs/mod.rs delete mode 100644 tooling/acvm_cli/src/cli/fs/witness.rs delete mode 100644 tooling/acvm_cli/src/errors.rs create mode 100644 tooling/acvm_cli/src/fs/mod.rs create mode 100644 tooling/acvm_cli/src/fs/witness.rs create mode 100644 tooling/artifact_cli/Cargo.toml create mode 100644 tooling/artifact_cli/src/bin/execute.rs create mode 100644 tooling/artifact_cli/src/commands/execute_cmd.rs create mode 100644 tooling/artifact_cli/src/commands/mod.rs create mode 100644 tooling/artifact_cli/src/errors.rs create mode 100644 tooling/artifact_cli/src/fs/artifact.rs create mode 100644 tooling/artifact_cli/src/fs/inputs.rs create mode 100644 tooling/artifact_cli/src/fs/mod.rs create mode 100644 tooling/artifact_cli/src/fs/witness.rs create mode 100644 tooling/artifact_cli/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index a522ccb3bed..8b6912452f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,10 +90,10 @@ dependencies = [ "color-eyre", "const_format", "nargo", + "noir_artifact_cli", "paste", "proptest", "rand", - "thiserror", "toml", "tracing-appender", "tracing-subscriber", @@ -3155,6 +3155,30 @@ dependencies = [ "libc", ] +[[package]] +name = "noir_artifact_cli" +version = "1.0.0-beta.2" +dependencies = [ + "acir", + "acvm", + "bn254_blackbox_solver", + "clap", + "color-eyre", + "const_format", + "fm", + "nargo", + "noirc_abi", + "noirc_artifacts", + "noirc_artifacts_info", + "noirc_driver", + "noirc_errors", + "serde", + "serde_json", + "thiserror", + "toml", + "tracing-subscriber", +] + [[package]] name = "noir_debugger" version = "1.0.0-beta.2" diff --git a/Cargo.toml b/Cargo.toml index 02c4592a5d4..c91eb816083 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "tooling/noirc_abi", "tooling/noirc_abi_wasm", "tooling/acvm_cli", + "tooling/artifact_cli", "tooling/profiler", "tooling/inspector", # ACVM @@ -40,6 +41,7 @@ members = [ default-members = [ "tooling/nargo_cli", "tooling/acvm_cli", + "tooling/artifact_cli", "tooling/profiler", "tooling/inspector", ] @@ -91,6 +93,7 @@ noir_debugger = { path = "tooling/debugger" } noirc_abi = { path = "tooling/noirc_abi" } noirc_artifacts = { path = "tooling/noirc_artifacts" } noirc_artifacts_info = { path = "tooling/noirc_artifacts_info" } +noir_artifact_cli = { path = "tooling/artifact_cli" } # Arkworks ark-bn254 = { version = "^0.5.0", default-features = false, features = [ diff --git a/acvm-repo/acir/src/circuit/mod.rs b/acvm-repo/acir/src/circuit/mod.rs index e651e6998a4..091a3dcb0e5 100644 --- a/acvm-repo/acir/src/circuit/mod.rs +++ b/acvm-repo/acir/src/circuit/mod.rs @@ -272,6 +272,7 @@ impl Deserialize<'a>> Program { .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err)) } + /// Deserialize bytecode. pub fn deserialize_program(serialized_circuit: &[u8]) -> std::io::Result { Program::read(serialized_circuit) } diff --git a/compiler/noirc_errors/src/reporter.rs b/compiler/noirc_errors/src/reporter.rs index 20a765fdaa5..30bf49348b6 100644 --- a/compiler/noirc_errors/src/reporter.rs +++ b/compiler/noirc_errors/src/reporter.rs @@ -211,6 +211,7 @@ pub fn report_all<'files>( } impl FileDiagnostic { + /// Print the report; return true if it was an error. pub fn report<'files>( &self, files: &'files impl Files<'files, FileId = fm::FileId>, diff --git a/tooling/acvm_cli/Cargo.toml b/tooling/acvm_cli/Cargo.toml index 06dd5e676bd..d3906175df3 100644 --- a/tooling/acvm_cli/Cargo.toml +++ b/tooling/acvm_cli/Cargo.toml @@ -21,15 +21,16 @@ name = "acvm" path = "src/main.rs" [dependencies] -thiserror.workspace = true -toml.workspace = true color-eyre.workspace = true clap.workspace = true -acvm.workspace = true -nargo.workspace = true const_format.workspace = true -bn254_blackbox_solver.workspace = true +toml.workspace = true + acir.workspace = true +acvm.workspace = true +bn254_blackbox_solver.workspace = true +nargo.workspace = true +noir_artifact_cli.workspace = true # Logs tracing-subscriber.workspace = true diff --git a/tooling/acvm_cli/src/cli/execute_cmd.rs b/tooling/acvm_cli/src/cli/execute_cmd.rs index e5d48073ca8..6f6fba7cd1e 100644 --- a/tooling/acvm_cli/src/cli/execute_cmd.rs +++ b/tooling/acvm_cli/src/cli/execute_cmd.rs @@ -1,4 +1,5 @@ use std::io::{self, Write}; +use std::path::PathBuf; use acir::circuit::Program; use acir::native_types::{WitnessMap, WitnessStack}; @@ -7,11 +8,12 @@ use bn254_blackbox_solver::Bn254BlackBoxSolver; use clap::Args; use nargo::PrintOutput; -use crate::cli::fs::inputs::{read_bytecode_from_file, read_inputs_from_file}; -use crate::errors::CliError; use nargo::{foreign_calls::DefaultForeignCallBuilder, ops::execute_program}; +use noir_artifact_cli::errors::CliError; +use noir_artifact_cli::fs::artifact::read_bytecode_from_file; +use noir_artifact_cli::fs::witness::save_witness_to_dir; -use super::fs::witness::{create_output_witness_string, save_witness_to_dir}; +use crate::fs::witness::{create_output_witness_string, read_witness_from_file}; /// Executes a circuit to calculate its return value #[derive(Debug, Clone, Args)] @@ -30,7 +32,7 @@ pub(crate) struct ExecuteCommand { /// The working directory #[clap(long, short)] - working_directory: String, + working_directory: PathBuf, /// Set to print output witness to stdout #[clap(long, short, action)] @@ -45,9 +47,9 @@ pub(crate) struct ExecuteCommand { fn run_command(args: ExecuteCommand) -> Result { let bytecode = read_bytecode_from_file(&args.working_directory, &args.bytecode)?; - let circuit_inputs = read_inputs_from_file(&args.working_directory, &args.input_witness)?; + let input_witness = read_witness_from_file(&args.working_directory.join(&args.input_witness))?; let output_witness = - execute_program_from_witness(circuit_inputs, &bytecode, args.pedantic_solving)?; + execute_program_from_witness(input_witness, &bytecode, args.pedantic_solving)?; assert_eq!(output_witness.length(), 1, "ACVM CLI only supports a witness stack of size 1"); let output_witness_string = create_output_witness_string( &output_witness.peek().expect("Should have a witness stack item").witness, @@ -76,8 +78,8 @@ pub(crate) fn execute_program_from_witness( bytecode: &[u8], pedantic_solving: bool, ) -> Result, CliError> { - let program: Program = Program::deserialize_program(bytecode) - .map_err(|_| CliError::CircuitDeserializationError())?; + let program: Program = + Program::deserialize_program(bytecode).map_err(CliError::CircuitDeserializationError)?; execute_program( &program, inputs_map, diff --git a/tooling/acvm_cli/src/cli/fs/inputs.rs b/tooling/acvm_cli/src/cli/fs/inputs.rs deleted file mode 100644 index a0b6e3a9545..00000000000 --- a/tooling/acvm_cli/src/cli/fs/inputs.rs +++ /dev/null @@ -1,54 +0,0 @@ -use acir::{ - native_types::{Witness, WitnessMap}, - AcirField, FieldElement, -}; -use toml::Table; - -use crate::errors::{CliError, FilesystemError}; -use std::{fs::read, path::Path}; - -/// Returns the circuit's parameters parsed from a toml file at the given location -pub(crate) fn read_inputs_from_file>( - working_directory: P, - file_name: &String, -) -> Result, CliError> { - let file_path = working_directory.as_ref().join(file_name); - if !file_path.exists() { - return Err(CliError::FilesystemError(FilesystemError::MissingTomlFile( - file_name.to_owned(), - file_path, - ))); - } - - let input_string = std::fs::read_to_string(file_path) - .map_err(|_| FilesystemError::InvalidTomlFile(file_name.clone()))?; - let input_map = input_string - .parse::() - .map_err(|_| FilesystemError::InvalidTomlFile(file_name.clone()))?; - let mut witnesses: WitnessMap = WitnessMap::new(); - for (key, value) in input_map.into_iter() { - let index = - Witness(key.trim().parse().map_err(|_| CliError::WitnessIndexError(key.clone()))?); - if !value.is_str() { - return Err(CliError::WitnessValueError(key.clone())); - } - let field = FieldElement::from_hex(value.as_str().unwrap()).unwrap(); - witnesses.insert(index, field); - } - - Ok(witnesses) -} - -/// Returns the circuit's bytecode read from the file at the given location -pub(crate) fn read_bytecode_from_file>( - working_directory: P, - file_name: &String, -) -> Result, FilesystemError> { - let file_path = working_directory.as_ref().join(file_name); - if !file_path.exists() { - return Err(FilesystemError::MissingBytecodeFile(file_name.to_owned(), file_path)); - } - let bytecode: Vec = - read(file_path).map_err(|_| FilesystemError::InvalidBytecodeFile(file_name.clone()))?; - Ok(bytecode) -} diff --git a/tooling/acvm_cli/src/cli/fs/mod.rs b/tooling/acvm_cli/src/cli/fs/mod.rs deleted file mode 100644 index f23ba06fd8b..00000000000 --- a/tooling/acvm_cli/src/cli/fs/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub(super) mod inputs; -pub(super) mod witness; diff --git a/tooling/acvm_cli/src/cli/fs/witness.rs b/tooling/acvm_cli/src/cli/fs/witness.rs deleted file mode 100644 index 6ecba9792c3..00000000000 --- a/tooling/acvm_cli/src/cli/fs/witness.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::{ - collections::BTreeMap, - fs::File, - io::Write, - path::{Path, PathBuf}, -}; - -use acir::FieldElement; -use acvm::acir::{ - native_types::{WitnessMap, WitnessStack}, - AcirField, -}; - -use crate::errors::{CliError, FilesystemError}; - -fn create_named_dir(named_dir: &Path, name: &str) -> PathBuf { - std::fs::create_dir_all(named_dir) - .unwrap_or_else(|_| panic!("could not create the `{name}` directory")); - - PathBuf::from(named_dir) -} - -fn write_to_file(bytes: &[u8], path: &Path) -> String { - let display = path.display(); - - let mut file = match File::create(path) { - Err(why) => panic!("couldn't create {display}: {why}"), - Ok(file) => file, - }; - - match file.write_all(bytes) { - Err(why) => panic!("couldn't write to {display}: {why}"), - Ok(_) => display.to_string(), - } -} - -/// Creates a toml representation of the provided witness map -pub(crate) fn create_output_witness_string( - witnesses: &WitnessMap, -) -> Result { - let mut witness_map: BTreeMap = BTreeMap::new(); - for (key, value) in witnesses.clone().into_iter() { - witness_map.insert(key.0.to_string(), format!("0x{}", value.to_hex())); - } - - toml::to_string(&witness_map).map_err(|_| CliError::OutputWitnessSerializationFailed()) -} - -pub(crate) fn save_witness_to_dir>( - witnesses: WitnessStack, - witness_name: &str, - witness_dir: P, -) -> Result { - create_named_dir(witness_dir.as_ref(), "witness"); - let witness_path = witness_dir.as_ref().join(witness_name).with_extension("gz"); - - let buf: Vec = witnesses - .try_into() - .map_err(|_op| FilesystemError::OutputWitnessCreationFailed(witness_name.to_string()))?; - write_to_file(buf.as_slice(), &witness_path); - - Ok(witness_path) -} diff --git a/tooling/acvm_cli/src/cli/mod.rs b/tooling/acvm_cli/src/cli/mod.rs index f31e123d0cd..1459ae43014 100644 --- a/tooling/acvm_cli/src/cli/mod.rs +++ b/tooling/acvm_cli/src/cli/mod.rs @@ -3,7 +3,6 @@ use color_eyre::eyre; use const_format::formatcp; mod execute_cmd; -mod fs; const ACVM_VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/tooling/acvm_cli/src/errors.rs b/tooling/acvm_cli/src/errors.rs deleted file mode 100644 index 886c1bf80f2..00000000000 --- a/tooling/acvm_cli/src/errors.rs +++ /dev/null @@ -1,50 +0,0 @@ -use acir::FieldElement; -use nargo::NargoError; -use std::path::PathBuf; -use thiserror::Error; - -#[derive(Debug, Error)] -pub(crate) enum FilesystemError { - #[error( - " Error: cannot find {0} in expected location {1:?}.\n Please generate this file at the expected location." - )] - MissingTomlFile(String, PathBuf), - #[error(" Error: failed to parse toml file {0}.")] - InvalidTomlFile(String), - #[error( - " Error: cannot find {0} in expected location {1:?}.\n Please generate this file at the expected location." - )] - MissingBytecodeFile(String, PathBuf), - - #[error(" Error: failed to read bytecode file {0}.")] - InvalidBytecodeFile(String), - - #[error(" Error: failed to create output witness file {0}.")] - OutputWitnessCreationFailed(String), -} - -#[derive(Debug, Error)] -pub(crate) enum CliError { - /// Filesystem errors - #[error(transparent)] - FilesystemError(#[from] FilesystemError), - - /// Error related to circuit deserialization - #[error("Error: failed to deserialize circuit in ACVM CLI")] - CircuitDeserializationError(), - - /// Error related to circuit execution - #[error(transparent)] - CircuitExecutionError(#[from] NargoError), - - /// Input Witness Value Error - #[error("Error: failed to parse witness value {0}")] - WitnessValueError(String), - - /// Input Witness Index Error - #[error("Error: failed to parse witness index {0}")] - WitnessIndexError(String), - - #[error(" Error: failed to serialize output witness.")] - OutputWitnessSerializationFailed(), -} diff --git a/tooling/acvm_cli/src/fs/mod.rs b/tooling/acvm_cli/src/fs/mod.rs new file mode 100644 index 00000000000..b4bfa6e48b6 --- /dev/null +++ b/tooling/acvm_cli/src/fs/mod.rs @@ -0,0 +1 @@ +pub(crate) mod witness; diff --git a/tooling/acvm_cli/src/fs/witness.rs b/tooling/acvm_cli/src/fs/witness.rs new file mode 100644 index 00000000000..5c2d2a9ad05 --- /dev/null +++ b/tooling/acvm_cli/src/fs/witness.rs @@ -0,0 +1,60 @@ +//! These witness functions are only used by the ACVM CLI and we'll most likely deprecate them. +use std::{collections::BTreeMap, path::Path}; + +use acir::{native_types::Witness, FieldElement}; +use acvm::acir::{native_types::WitnessMap, AcirField}; + +use noir_artifact_cli::errors::{CliError, FilesystemError}; + +/// Returns the circuit's parameters parsed from a TOML file at the given location. +/// +/// The expected format is the witness map, not ABI inputs, for example: +/// ```toml +/// "0" = '0x0000000000000000000000000000000000000000000000000000000000100000' +/// "1" = '0x0000000000000000000000000000000000000000000000000000000000000020' +/// "2" = '0x00000000000000000000000000000000000000000000000000000000000328b1' +/// "3" = '0x0000000000000000000000000000000000000000000000000000000000000001' +/// "4" = '0x0000000000000000000000000000000000000000000000000000000000000010' +/// "5" = '0x0000000000000000000000000000000000000000000000000000000000000011' +/// ``` +pub(crate) fn read_witness_from_file( + file_path: &Path, +) -> Result, CliError> { + if !file_path.exists() { + return Err(CliError::FilesystemError(FilesystemError::MissingInputFile( + file_path.to_path_buf(), + ))); + } + + let input_string = std::fs::read_to_string(file_path) + .map_err(|e| FilesystemError::InvalidInputFile(file_path.to_path_buf(), e.to_string()))?; + + let input_map = input_string + .parse::() + .map_err(|e| FilesystemError::InvalidInputFile(file_path.to_path_buf(), e.to_string()))?; + + let mut witnesses: WitnessMap = WitnessMap::new(); + + for (key, value) in input_map.into_iter() { + let index = + Witness(key.trim().parse().map_err(|_| CliError::WitnessIndexError(key.clone()))?); + if !value.is_str() { + return Err(CliError::WitnessValueError(key.clone())); + } + let field = FieldElement::from_hex(value.as_str().unwrap()).unwrap(); + witnesses.insert(index, field); + } + + Ok(witnesses) +} + +/// Creates a TOML representation of the provided witness map +pub(crate) fn create_output_witness_string( + witnesses: &WitnessMap, +) -> Result { + let mut witness_map: BTreeMap = BTreeMap::new(); + for (key, value) in witnesses.clone().into_iter() { + witness_map.insert(key.0.to_string(), format!("0x{}", value.to_hex())); + } + toml::to_string(&witness_map).map_err(CliError::OutputWitnessSerializationFailed) +} diff --git a/tooling/acvm_cli/src/main.rs b/tooling/acvm_cli/src/main.rs index 33cadc73a7c..a30360a947c 100644 --- a/tooling/acvm_cli/src/main.rs +++ b/tooling/acvm_cli/src/main.rs @@ -4,13 +4,14 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies, unused_extern_crates))] mod cli; -mod errors; use std::env; use tracing_appender::rolling; use tracing_subscriber::{fmt::format::FmtSpan, EnvFilter}; +mod fs; + fn main() { // Setup tracing if let Ok(log_dir) = env::var("ACVM_LOG_DIR") { diff --git a/tooling/artifact_cli/Cargo.toml b/tooling/artifact_cli/Cargo.toml new file mode 100644 index 00000000000..2f7ae68a477 --- /dev/null +++ b/tooling/artifact_cli/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "noir_artifact_cli" +description = "Commands working on noir build artifacts" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +repository.workspace = true + +[lints] +workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +path = "src/lib.rs" + +[[bin]] +name = "noir-execute" +path = "src/bin/execute.rs" + +[dependencies] +clap.workspace = true +color-eyre.workspace = true +const_format.workspace = true +serde_json.workspace = true +thiserror.workspace = true +toml.workspace = true +tracing-subscriber.workspace = true + +# Noir repo dependencies +acir.workspace = true +acvm.workspace = true +bn254_blackbox_solver.workspace = true +fm.workspace = true +nargo = { workspace = true, features = ["rpc"] } +noirc_abi.workspace = true +noirc_artifacts_info.workspace = true +noirc_artifacts.workspace = true +noirc_driver.workspace = true +noirc_errors.workspace = true +serde.workspace = true diff --git a/tooling/artifact_cli/src/bin/execute.rs b/tooling/artifact_cli/src/bin/execute.rs new file mode 100644 index 00000000000..b66177a3d57 --- /dev/null +++ b/tooling/artifact_cli/src/bin/execute.rs @@ -0,0 +1,46 @@ +#![forbid(unsafe_code)] +#![warn(unreachable_pub)] + +use clap::{command, Parser, Subcommand}; +use color_eyre::eyre; +use const_format::formatcp; +use tracing_subscriber::{fmt::format::FmtSpan, EnvFilter}; + +use noir_artifact_cli::commands::execute_cmd; + +const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); +static VERSION_STRING: &str = formatcp!("version = {}\n", PKG_VERSION,); + +#[derive(Parser, Debug)] +#[command(name="noir-execute", author, version=VERSION_STRING, about, long_about = None)] +struct ExecutorCli { + #[command(flatten)] + command: execute_cmd::ExecuteCommand, +} + +#[non_exhaustive] +#[derive(Subcommand, Clone, Debug)] +enum ArtifactCommand { + Execute(execute_cmd::ExecuteCommand), +} + +pub fn start_cli() -> eyre::Result<()> { + let ExecutorCli { command } = ExecutorCli::parse(); + + execute_cmd::run(command)?; + + Ok(()) +} + +fn main() { + tracing_subscriber::fmt() + .with_span_events(FmtSpan::ACTIVE) + .with_ansi(true) + .with_env_filter(EnvFilter::from_env("NOIR_LOG")) + .init(); + + if let Err(e) = start_cli() { + eprintln!("{e:?}"); + std::process::exit(1); + } +} diff --git a/tooling/artifact_cli/src/commands/execute_cmd.rs b/tooling/artifact_cli/src/commands/execute_cmd.rs new file mode 100644 index 00000000000..9a117329c15 --- /dev/null +++ b/tooling/artifact_cli/src/commands/execute_cmd.rs @@ -0,0 +1,209 @@ +use std::{collections::BTreeMap, path::PathBuf}; + +use acir::{circuit::Program, native_types::WitnessStack, FieldElement}; +use bn254_blackbox_solver::Bn254BlackBoxSolver; +use clap::Args; +use color_eyre::eyre::{self, bail}; + +use crate::{ + errors::CliError, + fs::{inputs::read_inputs_from_file, witness::save_witness_to_dir}, + Artifact, +}; +use nargo::{foreign_calls::DefaultForeignCallBuilder, NargoError, PrintOutput}; +use noirc_abi::{input_parser::InputValue, Abi}; +use noirc_artifacts::debug::DebugArtifact; + +use super::parse_and_normalize_path; + +/// Execute a binary program or a circuit artifact. +#[derive(Debug, Clone, Args)] +pub struct ExecuteCommand { + /// Path to the JSON build artifact (either a program or a contract). + #[clap(long, short, value_parser = parse_and_normalize_path)] + artifact_path: PathBuf, + + /// Path to the Prover.toml file which contains the inputs and the + /// optional return value in ABI format. + #[clap(long, short, value_parser = parse_and_normalize_path)] + prover_file: PathBuf, + + /// Path to the directory where the output witness should be saved. + /// If empty then the results are discarded. + #[clap(long, short, value_parser = parse_and_normalize_path)] + output_dir: Option, + + /// Write the execution witness to named file + /// + /// Defaults to the name of the circuit being executed. + #[clap(long, short)] + witness_name: Option, + + /// Name of the function to execute, if the artifact is a contract. + #[clap(long)] + contract_fn: Option, + + /// JSON RPC url to solve oracle calls. + #[clap(long)] + oracle_resolver: Option, + + /// Use pedantic ACVM solving, i.e. double-check some black-box function assumptions when solving. + #[clap(long, default_value_t = false)] + pedantic_solving: bool, +} + +pub fn run(args: ExecuteCommand) -> eyre::Result<()> { + let artifact = Artifact::read_from_file(&args.artifact_path)?; + + let circuit = match artifact { + Artifact::Program(program) => Circuit { + name: None, + abi: program.abi, + bytecode: program.bytecode, + debug_symbols: program.debug_symbols, + file_map: program.file_map, + }, + Artifact::Contract(contract) => { + let names = + contract.functions.iter().map(|f| f.name.clone()).collect::>().join(","); + + let Some(ref name) = args.contract_fn else { + bail!("--contract-fn missing; options: [{names}]"); + }; + let Some(function) = contract.functions.into_iter().find(|f| f.name == *name) else { + bail!("unknown --contract-fn '{name}'; options: [{names}]"); + }; + + Circuit { + name: Some(name.clone()), + abi: function.abi, + bytecode: function.bytecode, + debug_symbols: function.debug_symbols, + file_map: contract.file_map, + } + } + }; + + match execute(&circuit, &args) { + Ok(solved) => { + save_witness(circuit, args, solved)?; + } + Err(CliError::CircuitExecutionError(err)) => { + show_diagnostic(circuit, err); + } + Err(e) => { + bail!("failed to execute the circuit: {e}"); + } + } + Ok(()) +} + +/// Parameters necessary to execute a circuit, display execution failures, etc. +struct Circuit { + name: Option, + abi: Abi, + bytecode: Program, + debug_symbols: noirc_errors::debug_info::ProgramDebugInfo, + file_map: BTreeMap, +} + +struct SolvedWitnesses { + expected_return: Option, + actual_return: Option, + witness_stack: WitnessStack, +} + +/// Execute a circuit and return the output witnesses. +fn execute(circuit: &Circuit, args: &ExecuteCommand) -> Result { + let (input_map, expected_return) = read_inputs_from_file(&args.prover_file, &circuit.abi)?; + + let initial_witness = circuit.abi.encode(&input_map, None)?; + + // TODO: Build a custom foreign call executor that reads from the Oracle transcript, + // and use it as a base for the default executor; see `DefaultForeignCallBuilder::build_with_base` + let mut foreign_call_executor = DefaultForeignCallBuilder { + output: PrintOutput::Stdout, + enable_mocks: false, + resolver_url: args.oracle_resolver.clone(), + root_path: None, + package_name: None, + } + .build(); + + let witness_stack = nargo::ops::execute_program( + &circuit.bytecode, + initial_witness, + &Bn254BlackBoxSolver(args.pedantic_solving), + &mut foreign_call_executor, + )?; + + let main_witness = + &witness_stack.peek().expect("Should have at least one witness on the stack").witness; + + let (_, actual_return) = circuit.abi.decode(main_witness)?; + + Ok(SolvedWitnesses { expected_return, actual_return, witness_stack }) +} + +/// Print an error stack trace, if possible. +fn show_diagnostic(circuit: Circuit, err: NargoError) { + if let Some(diagnostic) = nargo::errors::try_to_diagnose_runtime_error( + &err, + &circuit.abi, + &circuit.debug_symbols.debug_infos, + ) { + let debug_artifact = DebugArtifact { + debug_symbols: circuit.debug_symbols.debug_infos, + file_map: circuit.file_map, + }; + diagnostic.report(&debug_artifact, false); + } +} + +/// Print information about the witness and compare to expectations, +/// returning errors if something isn't right. +fn save_witness( + circuit: Circuit, + args: ExecuteCommand, + solved: SolvedWitnesses, +) -> eyre::Result<()> { + let artifact = args.artifact_path.file_stem().and_then(|s| s.to_str()).unwrap_or_default(); + let name = circuit + .name + .as_ref() + .map(|name| format!("{artifact}.{name}")) + .unwrap_or_else(|| artifact.to_string()); + + println!("[{}] Circuit witness successfully solved", name); + + if let Some(ref witness_dir) = args.output_dir { + let witness_path = save_witness_to_dir( + solved.witness_stack, + &args.witness_name.unwrap_or_else(|| name.clone()), + witness_dir, + )?; + println!("[{}] Witness saved to {}", name, witness_path.display()); + } + + // Check that the circuit returned a non-empty result if the ABI expects a return value. + if let Some(ref expected) = circuit.abi.return_type { + if solved.actual_return.is_none() { + bail!("Missing return witness; expected a value of type {expected:?}"); + } + } + + // Check that if the prover file contained a `return` entry then that's what we got. + if let Some(expected) = solved.expected_return { + match solved.actual_return { + None => { + bail!("Missing return witness;\nexpected:\n{expected:?}"); + } + Some(actual) if actual != expected => { + bail!("Unexpected return witness;\nexpected:\n{expected:?}\ngot:\n{actual:?}"); + } + _ => {} + } + } + + Ok(()) +} diff --git a/tooling/artifact_cli/src/commands/mod.rs b/tooling/artifact_cli/src/commands/mod.rs new file mode 100644 index 00000000000..78f3b19292f --- /dev/null +++ b/tooling/artifact_cli/src/commands/mod.rs @@ -0,0 +1,17 @@ +use std::path::PathBuf; + +use color_eyre::eyre; +use eyre::eyre; + +pub mod execute_cmd; + +/// Parses a path and turns it into an absolute one by joining to the current directory, +/// then normalizes it. +fn parse_and_normalize_path(path: &str) -> eyre::Result { + use fm::NormalizePath; + let mut path: PathBuf = path.parse().map_err(|e| eyre!("failed to parse path: {e}"))?; + if !path.is_absolute() { + path = std::env::current_dir().unwrap().join(path).normalize(); + } + Ok(path) +} diff --git a/tooling/artifact_cli/src/errors.rs b/tooling/artifact_cli/src/errors.rs new file mode 100644 index 00000000000..5f302f78695 --- /dev/null +++ b/tooling/artifact_cli/src/errors.rs @@ -0,0 +1,64 @@ +use acir::FieldElement; +use nargo::NargoError; +use noirc_abi::errors::{AbiError, InputParserError}; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum FilesystemError { + #[error("Cannot find input file '{0}'")] + MissingInputFile(PathBuf), + + #[error("Failed to parse input file '{0}': {1}")] + InvalidInputFile(PathBuf, String), + + #[error("Cannot find bytecode file '{0}'")] + MissingBytecodeFile(PathBuf), + + #[error("Failed to read bytecode file '{0}': {1}")] + InvalidBytecodeFile(PathBuf, String), + + #[error("Failed to create output witness file '{0}': {1}")] + OutputWitnessCreationFailed(PathBuf, String), + + #[error(transparent)] + IoError(#[from] std::io::Error), +} + +#[derive(Debug, Error)] +pub enum CliError { + /// Filesystem errors + #[error(transparent)] + FilesystemError(#[from] FilesystemError), + + /// Error related to ABI input deserialization + #[error("Failed to deserialize inputs")] + InputDeserializationError(#[from] InputParserError), + + /// Error related to ABI encoding + #[error(transparent)] + AbiError(#[from] AbiError), + + /// Error related to artifact deserialization + #[error("Failed to deserialize artifact from JSON")] + ArtifactDeserializationError(#[from] serde_json::Error), + + /// Error related to circuit deserialization + #[error("Failed to deserialize circuit from bytecode")] + CircuitDeserializationError(#[from] std::io::Error), + + /// Error related to circuit execution + #[error(transparent)] + CircuitExecutionError(#[from] NargoError), + + /// Input Witness Value Error + #[error("Failed to parse witness value {0}")] + WitnessValueError(String), + + /// Input Witness Index Error + #[error("Failed to parse witness index {0}")] + WitnessIndexError(String), + + #[error("Failed to serialize output witness: {0}")] + OutputWitnessSerializationFailed(#[from] toml::ser::Error), +} diff --git a/tooling/artifact_cli/src/fs/artifact.rs b/tooling/artifact_cli/src/fs/artifact.rs new file mode 100644 index 00000000000..856fc472fc7 --- /dev/null +++ b/tooling/artifact_cli/src/fs/artifact.rs @@ -0,0 +1,37 @@ +use std::path::Path; + +use crate::{ + errors::{CliError, FilesystemError}, + Artifact, +}; +use noirc_artifacts::contract::ContractArtifact; +use noirc_artifacts::program::ProgramArtifact; + +impl Artifact { + /// Try to parse an artifact as a binary program or a contract + pub fn read_from_file(path: &Path) -> Result { + let json = std::fs::read(path).map_err(FilesystemError::from)?; + + let as_program = || serde_json::from_slice::(&json).map(Artifact::Program); + let as_contract = + || serde_json::from_slice::(&json).map(Artifact::Contract); + + as_program() + .or_else(|e| as_contract().map_err(|_| e)) + .map_err(CliError::ArtifactDeserializationError) + } +} + +/// Returns the circuit's bytecode read from the file at the given location +pub fn read_bytecode_from_file( + work_dir: &Path, + file_name: &str, +) -> Result, FilesystemError> { + let file_path = work_dir.join(file_name); + if !file_path.exists() { + return Err(FilesystemError::MissingBytecodeFile(file_path.clone())); + } + let bytecode: Vec = std::fs::read(&file_path) + .map_err(|e| FilesystemError::InvalidBytecodeFile(file_path, e.to_string()))?; + Ok(bytecode) +} diff --git a/tooling/artifact_cli/src/fs/inputs.rs b/tooling/artifact_cli/src/fs/inputs.rs new file mode 100644 index 00000000000..f115af92041 --- /dev/null +++ b/tooling/artifact_cli/src/fs/inputs.rs @@ -0,0 +1,56 @@ +use noirc_abi::{ + input_parser::{Format, InputValue}, + Abi, InputMap, MAIN_RETURN_NAME, +}; +use std::{collections::BTreeMap, path::Path}; + +use crate::errors::CliError; + +/// Returns the circuit's parameters and its return value, if one exists. +/// +/// The file is is expected to contain ABI encoded inputs in TOML or JSON format. +pub fn read_inputs_from_file( + file_path: &Path, + abi: &Abi, +) -> Result<(InputMap, Option), CliError> { + use crate::errors::FilesystemError::{InvalidInputFile, MissingInputFile}; + use CliError::FilesystemError; + + let has_params = !abi.parameters.is_empty(); + let has_return = abi.return_type.is_some(); + let has_file = file_path.exists(); + + if !has_params && !has_return { + return Ok((BTreeMap::new(), None)); + } + if !has_params && !has_file { + // Reading a return value from the `Prover.toml` is optional, + // so if the ABI has no parameters we can skip reading the file if it doesn't exist. + return Ok((BTreeMap::new(), None)); + } + if has_params && !has_file { + return Err(FilesystemError(MissingInputFile(file_path.to_path_buf()))); + } + + let Some(ext) = file_path.extension().and_then(|e| e.to_str()) else { + return Err(FilesystemError(InvalidInputFile( + file_path.to_path_buf(), + "cannot determine input format".to_string(), + ))); + }; + + let Some(format) = Format::from_ext(ext) else { + return Err(FilesystemError(InvalidInputFile( + file_path.to_path_buf(), + format!("unknown input format: {ext}"), + ))); + }; + + let inputs = std::fs::read_to_string(file_path) + .map_err(|e| FilesystemError(InvalidInputFile(file_path.to_path_buf(), e.to_string())))?; + + let mut inputs = format.parse(&inputs, abi)?; + let return_value = inputs.remove(MAIN_RETURN_NAME); + + Ok((inputs, return_value)) +} diff --git a/tooling/artifact_cli/src/fs/mod.rs b/tooling/artifact_cli/src/fs/mod.rs new file mode 100644 index 00000000000..1a704315168 --- /dev/null +++ b/tooling/artifact_cli/src/fs/mod.rs @@ -0,0 +1,3 @@ +pub mod artifact; +pub mod inputs; +pub mod witness; diff --git a/tooling/artifact_cli/src/fs/witness.rs b/tooling/artifact_cli/src/fs/witness.rs new file mode 100644 index 00000000000..f486a53f14d --- /dev/null +++ b/tooling/artifact_cli/src/fs/witness.rs @@ -0,0 +1,27 @@ +use std::path::{Path, PathBuf}; + +use acir::{native_types::WitnessStackError, FieldElement}; +use acvm::acir::native_types::WitnessStack; + +use crate::errors::FilesystemError; + +/// Write `witness.gz` to the output directory. +pub fn save_witness_to_dir( + witnesses: WitnessStack, + witness_name: &str, + witness_dir: &Path, +) -> Result { + std::fs::create_dir_all(witness_dir)?; + + let witness_path = witness_dir.join(witness_name).with_extension("gz"); + + let buf: Vec = witnesses.try_into().map_err(|e: WitnessStackError| { + FilesystemError::OutputWitnessCreationFailed(witness_path.clone(), format!("{e:?}")) + })?; + + std::fs::write(&witness_path, buf.as_slice()).map_err(|e| { + FilesystemError::OutputWitnessCreationFailed(witness_path.clone(), e.to_string()) + })?; + + Ok(witness_path) +} diff --git a/tooling/artifact_cli/src/lib.rs b/tooling/artifact_cli/src/lib.rs new file mode 100644 index 00000000000..2cd2341b7b7 --- /dev/null +++ b/tooling/artifact_cli/src/lib.rs @@ -0,0 +1,12 @@ +use noirc_artifacts::{contract::ContractArtifact, program::ProgramArtifact}; + +pub mod commands; +pub mod errors; +pub mod fs; + +/// A parsed JSON build artifact. +#[derive(Debug, Clone)] +pub enum Artifact { + Program(ProgramArtifact), + Contract(ContractArtifact), +} diff --git a/tooling/noirc_abi/src/input_parser/mod.rs b/tooling/noirc_abi/src/input_parser/mod.rs index 565870b0835..fce627c8d31 100644 --- a/tooling/noirc_abi/src/input_parser/mod.rs +++ b/tooling/noirc_abi/src/input_parser/mod.rs @@ -186,6 +186,7 @@ impl InputValue { /// The different formats that are supported when parsing /// the initial witness values +#[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(strum_macros::EnumIter))] pub enum Format { Json, @@ -199,6 +200,14 @@ impl Format { Format::Toml => "toml", } } + + pub fn from_ext(ext: &str) -> Option { + match ext { + "json" => Some(Self::Json), + "toml" => Some(Self::Toml), + _ => None, + } + } } impl Format { @@ -443,8 +452,9 @@ fn field_to_signed_hex(f: FieldElement, bit_size: u32) -> String { mod test { use acvm::{AcirField, FieldElement}; use num_bigint::BigUint; + use strum::IntoEnumIterator; - use super::{parse_str_to_field, parse_str_to_signed}; + use super::{parse_str_to_field, parse_str_to_signed, Format}; fn big_uint_from_field(field: FieldElement) -> BigUint { BigUint::from_bytes_be(&field.to_be_bytes()) @@ -521,6 +531,14 @@ mod test { ); assert!(parse_str_to_signed("-32769", 16, "arg_name").is_err()); } + + #[test] + fn test_from_ext() { + for fmt in Format::iter() { + assert_eq!(Format::from_ext(fmt.ext()), Some(fmt)); + } + assert_eq!(Format::from_ext("invalid extension"), None); + } } #[cfg(test)] diff --git a/tooling/noirc_artifacts/src/contract.rs b/tooling/noirc_artifacts/src/contract.rs index 2b78b647037..0649a34e625 100644 --- a/tooling/noirc_artifacts/src/contract.rs +++ b/tooling/noirc_artifacts/src/contract.rs @@ -9,7 +9,7 @@ use std::collections::{BTreeMap, HashMap}; use fm::FileId; -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize, Debug)] pub struct ContractOutputsArtifact { pub structs: HashMap>, pub globals: HashMap>, @@ -21,7 +21,7 @@ impl From for ContractOutputsArtifact { } } -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize, Debug)] pub struct ContractArtifact { /// Version of noir used to compile this contract pub noir_version: String,