diff --git a/barretenberg/acir_tests/Dockerfile.bb b/barretenberg/acir_tests/Dockerfile.bb index 60144b8a707..cda8d054ca3 100644 --- a/barretenberg/acir_tests/Dockerfile.bb +++ b/barretenberg/acir_tests/Dockerfile.bb @@ -14,6 +14,8 @@ RUN FLOW=prove_then_verify ./run_acir_tests.sh RUN FLOW=prove_and_verify_ultra_honk ./run_acir_tests.sh # Construct and verify a Goblin UltraHonk (GUH) proof for a single arbitrary program RUN FLOW=prove_and_verify_goblin_ultra_honk ./run_acir_tests.sh 6_array +# Construct and verify a UltraHonk proof for all ACIR programs using the new witness stack workflow +RUN FLOW=prove_and_verify_ultra_honk_program ./run_acir_tests.sh # This is a "full" Goblin flow. It constructs and verifies four proofs: GoblinUltraHonk, ECCVM, Translator, and merge RUN FLOW=prove_and_verify_goblin ./run_acir_tests.sh 6_array # Run 1_mul through native bb build, all_cmds flow, to test all cli args. diff --git a/barretenberg/acir_tests/flows/prove_and_verify_ultra_honk_program.sh b/barretenberg/acir_tests/flows/prove_and_verify_ultra_honk_program.sh new file mode 100755 index 00000000000..53c9d08e1a6 --- /dev/null +++ b/barretenberg/acir_tests/flows/prove_and_verify_ultra_honk_program.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -eu + +VFLAG=${VERBOSE:+-v} + +$BIN prove_and_verify_ultra_honk_program $VFLAG -c $CRS_PATH -b ./target/acir.gz \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/bb/main.cpp b/barretenberg/cpp/src/barretenberg/bb/main.cpp index 34e4ef0fe4c..bab9d9dfae1 100644 --- a/barretenberg/cpp/src/barretenberg/bb/main.cpp +++ b/barretenberg/cpp/src/barretenberg/bb/main.cpp @@ -1,5 +1,6 @@ #include "barretenberg/bb/file_io.hpp" #include "barretenberg/common/serialize.hpp" +#include "barretenberg/dsl/acir_format/acir_format.hpp" #include "barretenberg/dsl/types.hpp" #include "barretenberg/honk/proof_system/types/proof.hpp" #include "barretenberg/plonk/proof_system/proving_key/serialize.hpp" @@ -83,6 +84,18 @@ acir_format::AcirFormat get_constraint_system(std::string const& bytecode_path) return acir_format::circuit_buf_to_acir_format(bytecode); } +acir_format::WitnessVectorStack get_witness_stack(std::string const& witness_path) +{ + auto witness_data = get_bytecode(witness_path); + return acir_format::witness_buf_to_witness_stack(witness_data); +} + +std::vector get_constraint_systems(std::string const& bytecode_path) +{ + auto bytecode = get_bytecode(bytecode_path); + return acir_format::program_buf_to_acir_format(bytecode); +} + /** * @brief Proves and Verifies an ACIR circuit * @@ -127,24 +140,14 @@ bool proveAndVerify(const std::string& bytecodePath, const std::string& witnessP return verified; } -/** - * @brief Constructs and verifies a Honk proof for an acir-generated circuit - * - * @tparam Flavor - * @param bytecodePath Path to serialized acir circuit data - * @param witnessPath Path to serialized acir witness data - */ -template bool proveAndVerifyHonk(const std::string& bytecodePath, const std::string& witnessPath) +template +bool proveAndVerifyHonkAcirFormat(acir_format::AcirFormat constraint_system, acir_format::WitnessVector witness) { using Builder = Flavor::CircuitBuilder; using Prover = UltraProver_; using Verifier = UltraVerifier_; using VerificationKey = Flavor::VerificationKey; - // Populate the acir constraint system and witness from gzipped data - auto constraint_system = get_constraint_system(bytecodePath); - auto witness = get_witness(witnessPath); - // Construct a bberg circuit from the acir representation auto builder = acir_format::create_circuit(constraint_system, 0, witness); @@ -165,6 +168,49 @@ template bool proveAndVerifyHonk(const std::string& bytec return verifier.verify_proof(proof); } +/** + * @brief Constructs and verifies a Honk proof for an acir-generated circuit + * + * @tparam Flavor + * @param bytecodePath Path to serialized acir circuit data + * @param witnessPath Path to serialized acir witness data + */ +template bool proveAndVerifyHonk(const std::string& bytecodePath, const std::string& witnessPath) +{ + // Populate the acir constraint system and witness from gzipped data + auto constraint_system = get_constraint_system(bytecodePath); + auto witness = get_witness(witnessPath); + + return proveAndVerifyHonkAcirFormat(constraint_system, witness); +} + +/** + * @brief Constructs and verifies multiple Honk proofs for an ACIR-generated program. + * + * @tparam Flavor + * @param bytecodePath Path to serialized acir program data. An ACIR program contains a list of circuits. + * @param witnessPath Path to serialized acir witness stack data. This dictates the execution trace the backend should + * follow. + */ +template +bool proveAndVerifyHonkProgram(const std::string& bytecodePath, const std::string& witnessPath) +{ + auto constraint_systems = get_constraint_systems(bytecodePath); + auto witness_stack = get_witness_stack(witnessPath); + + while (!witness_stack.empty()) { + auto witness_stack_item = witness_stack.back(); + auto witness = witness_stack_item.second; + auto constraint_system = constraint_systems[witness_stack_item.first]; + + if (!proveAndVerifyHonkAcirFormat(constraint_system, witness)) { + return false; + } + witness_stack.pop_back(); + } + return true; +} + /** * @brief Proves and Verifies an ACIR circuit * @@ -576,6 +622,9 @@ int main(int argc, char* argv[]) if (command == "prove_and_verify_goblin_ultra_honk") { return proveAndVerifyHonk(bytecode_path, witness_path) ? 0 : 1; } + if (command == "prove_and_verify_ultra_honk_program") { + return proveAndVerifyHonkProgram(bytecode_path, witness_path) ? 0 : 1; + } if (command == "prove_and_verify_goblin") { return proveAndVerifyGoblin(bytecode_path, witness_path) ? 0 : 1; } diff --git a/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_format.hpp b/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_format.hpp index 9a03f96d37a..c5b64473d80 100644 --- a/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_format.hpp +++ b/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_format.hpp @@ -17,6 +17,7 @@ #include "recursion_constraint.hpp" #include "schnorr_verify.hpp" #include "sha256_constraint.hpp" +#include namespace acir_format { @@ -93,6 +94,7 @@ struct AcirFormat { }; using WitnessVector = std::vector>; +using WitnessVectorStack = std::vector>; template Builder create_circuit(const AcirFormat& constraint_system, size_t size_hint = 0, WitnessVector const& witness = {}); diff --git a/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_to_constraint_buf.hpp b/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_to_constraint_buf.hpp index ddad2310a49..c3b35c2c88c 100644 --- a/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_to_constraint_buf.hpp +++ b/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_to_constraint_buf.hpp @@ -18,6 +18,7 @@ #include "barretenberg/plonk_honk_shared/arithmetization/gate_data.hpp" #include "serde/index.hpp" #include +#include namespace acir_format { @@ -363,12 +364,8 @@ void handle_memory_op(Program::Opcode::MemoryOp const& mem_op, BlockConstraint& block.trace.push_back(acir_mem_op); } -AcirFormat circuit_buf_to_acir_format(std::vector const& buf) +AcirFormat circuit_serde_to_acir_format(Program::Circuit const& circuit) { - // TODO(maxim): Handle the new `Program` structure once ACVM supports a function call stack. - // For now we expect a single ACIR function - auto circuit = Program::Program::bincodeDeserialize(buf).functions[0]; - AcirFormat af; // `varnum` is the true number of variables, thus we add one to the index which starts at zero af.varnum = circuit.current_witness_index + 1; @@ -406,24 +403,29 @@ AcirFormat circuit_buf_to_acir_format(std::vector const& buf) return af; } +AcirFormat circuit_buf_to_acir_format(std::vector const& buf) +{ + // TODO(https://github.com/AztecProtocol/barretenberg/issues/927): Move to using just `program_buf_to_acir_format` + // once Honk fully supports all ACIR test flows + // For now the backend still expects to work with a single ACIR function + auto circuit = Program::Program::bincodeDeserialize(buf).functions[0]; + + return circuit_serde_to_acir_format(circuit); +} + /** * @brief Converts from the ACIR-native `WitnessMap` format to Barretenberg's internal `WitnessVector` format. * - * @param buf Serialized representation of a `WitnessMap`. + * @param witness_map ACIR-native `WitnessMap` deserialized from a buffer * @return A `WitnessVector` equivalent to the passed `WitnessMap`. * @note This transformation results in all unassigned witnesses within the `WitnessMap` being assigned the value 0. * Converting the `WitnessVector` back to a `WitnessMap` is unlikely to return the exact same `WitnessMap`. */ -WitnessVector witness_buf_to_witness_data(std::vector const& buf) +WitnessVector witness_map_to_witness_vector(WitnessStack::WitnessMap const& witness_map) { - // TODO(maxim): Handle the new `WitnessStack` structure once ACVM supports a function call stack - // A `StackItem` contains an index to an ACIR circuit and its respective ACIR-native `WitnessMap`. - // For now we expect the `WitnessStack` to contain a single witness. - auto w = WitnessStack::WitnessStack::bincodeDeserialize(buf).stack[0].witness; - WitnessVector wv; size_t index = 0; - for (auto& e : w.value) { + for (auto& e : witness_map.value) { // ACIR uses a sparse format for WitnessMap where unused witness indices may be left unassigned. // To ensure that witnesses sit at the correct indices in the `WitnessVector`, we fill any indices // which do not exist within the `WitnessMap` with the dummy value of zero. @@ -437,4 +439,48 @@ WitnessVector witness_buf_to_witness_data(std::vector const& buf) return wv; } +/** + * @brief Converts from the ACIR-native `WitnessMap` format to Barretenberg's internal `WitnessVector` format. + * + * @param buf Serialized representation of a `WitnessMap`. + * @return A `WitnessVector` equivalent to the passed `WitnessMap`. + * @note This transformation results in all unassigned witnesses within the `WitnessMap` being assigned the value 0. + * Converting the `WitnessVector` back to a `WitnessMap` is unlikely to return the exact same `WitnessMap`. + */ +WitnessVector witness_buf_to_witness_data(std::vector const& buf) +{ + // TODO(https://github.com/AztecProtocol/barretenberg/issues/927): Move to using just `witness_buf_to_witness_stack` + // once Honk fully supports all ACIR test flows. + // For now the backend still expects to work with the stop of the `WitnessStack`. + auto witness_stack = WitnessStack::WitnessStack::bincodeDeserialize(buf); + auto w = witness_stack.stack[witness_stack.stack.size() - 1].witness; + + return witness_map_to_witness_vector(w); +} + +std::vector program_buf_to_acir_format(std::vector const& buf) +{ + auto program = Program::Program::bincodeDeserialize(buf); + + std::vector constraint_systems; + constraint_systems.reserve(program.functions.size()); + for (auto const& function : program.functions) { + constraint_systems.emplace_back(circuit_serde_to_acir_format(function)); + } + + return constraint_systems; +} + +WitnessVectorStack witness_buf_to_witness_stack(std::vector const& buf) +{ + auto witness_stack = WitnessStack::WitnessStack::bincodeDeserialize(buf); + WitnessVectorStack witness_vector_stack; + witness_vector_stack.reserve(witness_stack.stack.size()); + for (auto const& stack_item : witness_stack.stack) { + witness_vector_stack.emplace_back( + std::make_pair(stack_item.index, witness_map_to_witness_vector(stack_item.witness))); + } + return witness_vector_stack; +} + } // namespace acir_format diff --git a/noir/noir-repo/acvm-repo/acir/src/native_types/witness_stack.rs b/noir/noir-repo/acvm-repo/acir/src/native_types/witness_stack.rs index 9592d90b014..a9e8f219b3e 100644 --- a/noir/noir-repo/acvm-repo/acir/src/native_types/witness_stack.rs +++ b/noir/noir-repo/acvm-repo/acir/src/native_types/witness_stack.rs @@ -32,6 +32,20 @@ pub struct StackItem { pub witness: WitnessMap, } +impl WitnessStack { + pub fn push(&mut self, index: u32, witness: WitnessMap) { + self.stack.push(StackItem { index, witness }); + } + + pub fn peek(&self) -> Option<&StackItem> { + self.stack.last() + } + + pub fn length(&self) -> usize { + self.stack.len() + } +} + impl From for WitnessStack { fn from(witness: WitnessMap) -> Self { let stack = vec![StackItem { index: 0, witness }]; diff --git a/noir/noir-repo/acvm-repo/acvm/src/pwg/mod.rs b/noir/noir-repo/acvm-repo/acvm/src/pwg/mod.rs index 0fd733a6336..3cedcfc0399 100644 --- a/noir/noir-repo/acvm-repo/acvm/src/pwg/mod.rs +++ b/noir/noir-repo/acvm-repo/acvm/src/pwg/mod.rs @@ -49,6 +49,12 @@ pub enum ACVMStatus { /// /// Once this is done, the ACVM can be restarted to solve the remaining opcodes. RequiresForeignCall(ForeignCallWaitInfo), + + /// The ACVM has encountered a request for an ACIR [call][acir::circuit::Opcode] + /// to execute a separate ACVM instance. The result of the ACIR call must be passd back to the ACVM. + /// + /// Once this is done, the ACVM can be restarted to solve the remaining opcodes. + RequiresAcirCall(AcirCallWaitInfo), } impl std::fmt::Display for ACVMStatus { @@ -58,6 +64,7 @@ impl std::fmt::Display for ACVMStatus { ACVMStatus::InProgress => write!(f, "In progress"), ACVMStatus::Failure(_) => write!(f, "Execution failure"), ACVMStatus::RequiresForeignCall(_) => write!(f, "Waiting on foreign call"), + ACVMStatus::RequiresAcirCall(_) => write!(f, "Waiting on acir call"), } } } @@ -117,6 +124,10 @@ pub enum OpcodeResolutionError { BlackBoxFunctionFailed(BlackBoxFunc, String), #[error("Failed to solve brillig function, reason: {message}")] BrilligFunctionFailed { message: String, call_stack: Vec }, + #[error("Attempted to call `main` with a `Call` opcode")] + AcirMainCallAttempted { opcode_location: ErrorLocation }, + #[error("{results_size:?} result values were provided for {outputs_size:?} call output witnesses, most likely due to bad ACIR codegen")] + AcirCallOutputsMismatch { opcode_location: ErrorLocation, results_size: u32, outputs_size: u32 }, } impl From for OpcodeResolutionError { @@ -147,6 +158,13 @@ pub struct ACVM<'a, B: BlackBoxFunctionSolver> { witness_map: WitnessMap, brillig_solver: Option>, + + /// A counter maintained throughout an ACVM process that determines + /// whether the caller has resolved the results of an ACIR [call][Opcode::Call]. + acir_call_counter: usize, + /// Represents the outputs of all ACIR calls during an ACVM process + /// List is appended onto by the caller upon reaching a [ACVMStatus::RequiresAcirCall] + acir_call_results: Vec>, } impl<'a, B: BlackBoxFunctionSolver> ACVM<'a, B> { @@ -161,6 +179,8 @@ impl<'a, B: BlackBoxFunctionSolver> ACVM<'a, B> { instruction_pointer: 0, witness_map: initial_witness, brillig_solver: None, + acir_call_counter: 0, + acir_call_results: Vec::default(), } } @@ -244,6 +264,29 @@ impl<'a, B: BlackBoxFunctionSolver> ACVM<'a, B> { self.status(ACVMStatus::InProgress); } + /// Sets the status of the VM to `RequiresAcirCall` + /// Indicating that the VM is now waiting for an ACIR call to be resolved + fn wait_for_acir_call(&mut self, acir_call: AcirCallWaitInfo) -> ACVMStatus { + self.status(ACVMStatus::RequiresAcirCall(acir_call)) + } + + /// Resolves an ACIR call's result (simply a list of fields) using a result calculated by a separate ACVM instance. + /// + /// The current ACVM instance can then be restarted to solve the remaining ACIR opcodes. + pub fn resolve_pending_acir_call(&mut self, call_result: Vec) { + if !matches!(self.status, ACVMStatus::RequiresAcirCall(_)) { + panic!("ACVM is not expecting an ACIR call response as no call was made"); + } + + if self.acir_call_counter < self.acir_call_results.len() { + panic!("No unresolved ACIR calls"); + } + self.acir_call_results.push(call_result); + + // Now that the ACIR call has been resolved then we can resume execution. + self.status(ACVMStatus::InProgress); + } + /// Executes the ACVM's circuit until execution halts. /// /// Execution can halt due to three reasons: @@ -281,7 +324,10 @@ impl<'a, B: BlackBoxFunctionSolver> ACVM<'a, B> { Ok(Some(foreign_call)) => return self.wait_for_foreign_call(foreign_call), res => res.map(|_| ()), }, - Opcode::Call { .. } => todo!("Handle Call opcodes in the ACVM"), + Opcode::Call { .. } => match self.solve_call_opcode() { + Ok(Some(input_values)) => return self.wait_for_acir_call(input_values), + res => res.map(|_| ()), + }, }; self.handle_opcode_resolution(resolution) } @@ -400,6 +446,46 @@ impl<'a, B: BlackBoxFunctionSolver> ACVM<'a, B> { self.brillig_solver = Some(solver); self.solve_opcode() } + + pub fn solve_call_opcode(&mut self) -> Result, OpcodeResolutionError> { + let Opcode::Call { id, inputs, outputs } = &self.opcodes[self.instruction_pointer] else { + unreachable!("Not executing a Call opcode"); + }; + if *id == 0 { + return Err(OpcodeResolutionError::AcirMainCallAttempted { + opcode_location: ErrorLocation::Resolved(OpcodeLocation::Acir( + self.instruction_pointer(), + )), + }); + } + + if self.acir_call_counter >= self.acir_call_results.len() { + let mut initial_witness = WitnessMap::default(); + for (i, input_witness) in inputs.iter().enumerate() { + let input_value = *witness_to_value(&self.witness_map, *input_witness)?; + initial_witness.insert(Witness(i as u32), input_value); + } + return Ok(Some(AcirCallWaitInfo { id: *id, initial_witness })); + } + + let result_values = &self.acir_call_results[self.acir_call_counter]; + if outputs.len() != result_values.len() { + return Err(OpcodeResolutionError::AcirCallOutputsMismatch { + opcode_location: ErrorLocation::Resolved(OpcodeLocation::Acir( + self.instruction_pointer(), + )), + results_size: result_values.len() as u32, + outputs_size: outputs.len() as u32, + }); + } + + for (output_witness, result_value) in outputs.iter().zip(result_values) { + insert_value(output_witness, *result_value, &mut self.witness_map)?; + } + + self.acir_call_counter += 1; + Ok(None) + } } // Returns the concrete value for a particular witness @@ -469,3 +555,11 @@ fn any_witness_from_expression(expr: &Expression) -> Option { Some(expr.linear_combinations[0].1) } } + +#[derive(Debug, Clone, PartialEq)] +pub struct AcirCallWaitInfo { + /// Index in the list of ACIR function's that should be called + pub id: u32, + /// Initial witness for the given circuit to be called + pub initial_witness: WitnessMap, +} diff --git a/noir/noir-repo/acvm-repo/acvm_js/src/execute.rs b/noir/noir-repo/acvm-repo/acvm_js/src/execute.rs index ac71a573e64..60d27a489e2 100644 --- a/noir/noir-repo/acvm-repo/acvm_js/src/execute.rs +++ b/noir/noir-repo/acvm-repo/acvm_js/src/execute.rs @@ -113,6 +113,9 @@ pub async fn execute_circuit_with_black_box_solver( acvm.resolve_pending_foreign_call(result); } + ACVMStatus::RequiresAcirCall(_) => { + todo!("Handle acir calls in acvm JS"); + } } } diff --git a/noir/noir-repo/test_programs/execution_success/fold_basic/Nargo.toml b/noir/noir-repo/test_programs/execution_success/fold_basic/Nargo.toml new file mode 100644 index 00000000000..575ba1f3ad1 --- /dev/null +++ b/noir/noir-repo/test_programs/execution_success/fold_basic/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "fold_basic" +type = "bin" +authors = [""] +compiler_version = ">=0.25.0" + +[dependencies] \ No newline at end of file diff --git a/noir/noir-repo/test_programs/execution_success/fold_basic/Prover.toml b/noir/noir-repo/test_programs/execution_success/fold_basic/Prover.toml new file mode 100644 index 00000000000..f28f2f8cc48 --- /dev/null +++ b/noir/noir-repo/test_programs/execution_success/fold_basic/Prover.toml @@ -0,0 +1,2 @@ +x = "5" +y = "10" diff --git a/noir/noir-repo/test_programs/execution_success/fold_basic/src/main.nr b/noir/noir-repo/test_programs/execution_success/fold_basic/src/main.nr new file mode 100644 index 00000000000..6c17120660b --- /dev/null +++ b/noir/noir-repo/test_programs/execution_success/fold_basic/src/main.nr @@ -0,0 +1,11 @@ +fn main(x: Field, y: pub Field) { + let z = foo(x, y); + let z2 = foo(x, y); + assert(z == z2); +} + +#[fold] +fn foo(x: Field, y: Field) -> Field { + assert(x != y); + x +} diff --git a/noir/noir-repo/test_programs/execution_success/fold_basic_nested_call/Nargo.toml b/noir/noir-repo/test_programs/execution_success/fold_basic_nested_call/Nargo.toml new file mode 100644 index 00000000000..1b3c32999ae --- /dev/null +++ b/noir/noir-repo/test_programs/execution_success/fold_basic_nested_call/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "fold_basic_nested_call" +type = "bin" +authors = [""] +compiler_version = ">=0.25.0" + +[dependencies] \ No newline at end of file diff --git a/noir/noir-repo/test_programs/execution_success/fold_basic_nested_call/Prover.toml b/noir/noir-repo/test_programs/execution_success/fold_basic_nested_call/Prover.toml new file mode 100644 index 00000000000..f28f2f8cc48 --- /dev/null +++ b/noir/noir-repo/test_programs/execution_success/fold_basic_nested_call/Prover.toml @@ -0,0 +1,2 @@ +x = "5" +y = "10" diff --git a/noir/noir-repo/test_programs/execution_success/fold_basic_nested_call/src/main.nr b/noir/noir-repo/test_programs/execution_success/fold_basic_nested_call/src/main.nr new file mode 100644 index 00000000000..3eb16b409ff --- /dev/null +++ b/noir/noir-repo/test_programs/execution_success/fold_basic_nested_call/src/main.nr @@ -0,0 +1,16 @@ +fn main(x: Field, y: pub Field) { + let z = func_with_nested_foo_call(x, y); + let z2 = func_with_nested_foo_call(x, y); + assert(z == z2); +} + +#[fold] +fn func_with_nested_foo_call(x: Field, y: Field) -> Field { + foo(x + 2, y) +} + +#[fold] +fn foo(x: Field, y: Field) -> Field { + assert(x != y); + x +} \ No newline at end of file diff --git a/noir/noir-repo/tooling/acvm_cli/src/cli/execute_cmd.rs b/noir/noir-repo/tooling/acvm_cli/src/cli/execute_cmd.rs index b76d0eccc29..86e7277451f 100644 --- a/noir/noir-repo/tooling/acvm_cli/src/cli/execute_cmd.rs +++ b/noir/noir-repo/tooling/acvm_cli/src/cli/execute_cmd.rs @@ -1,14 +1,14 @@ use std::io::{self, Write}; use acir::circuit::Program; -use acir::native_types::WitnessMap; +use acir::native_types::{WitnessMap, WitnessStack}; use bn254_blackbox_solver::Bn254BlackBoxSolver; use clap::Args; use crate::cli::fs::inputs::{read_bytecode_from_file, read_inputs_from_file}; use crate::cli::fs::witness::save_witness_to_dir; use crate::errors::CliError; -use nargo::ops::{execute_circuit, DefaultForeignCallExecutor}; +use nargo::ops::{execute_program, DefaultForeignCallExecutor}; use super::fs::witness::create_output_witness_string; @@ -39,8 +39,11 @@ 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 output_witness = execute_program_from_witness(&circuit_inputs, &bytecode, None)?; - let output_witness_string = create_output_witness_string(&output_witness)?; + let output_witness = execute_program_from_witness(circuit_inputs, &bytecode, None)?; + 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, + )?; if args.output_witness.is_some() { save_witness_to_dir( &output_witness_string, @@ -61,16 +64,16 @@ pub(crate) fn run(args: ExecuteCommand) -> Result { } pub(crate) fn execute_program_from_witness( - inputs_map: &WitnessMap, + inputs_map: WitnessMap, bytecode: &[u8], foreign_call_resolver_url: Option<&str>, -) -> Result { +) -> Result { let blackbox_solver = Bn254BlackBoxSolver::new(); let program: Program = Program::deserialize_program(bytecode) .map_err(|_| CliError::CircuitDeserializationError())?; - execute_circuit( - &program.functions[0], - inputs_map.clone(), + execute_program( + &program, + inputs_map, &blackbox_solver, &mut DefaultForeignCallExecutor::new(true, foreign_call_resolver_url), ) diff --git a/noir/noir-repo/tooling/backend_interface/src/proof_system.rs b/noir/noir-repo/tooling/backend_interface/src/proof_system.rs index 105ae337793..3b47a7ced3a 100644 --- a/noir/noir-repo/tooling/backend_interface/src/proof_system.rs +++ b/noir/noir-repo/tooling/backend_interface/src/proof_system.rs @@ -56,7 +56,7 @@ impl Backend { pub fn prove( &self, program: &Program, - witness_values: WitnessStack, + witness_stack: WitnessStack, ) -> Result, BackendError> { let binary_path = self.assert_binary_exists()?; self.assert_correct_version()?; @@ -66,7 +66,7 @@ impl Backend { // Create a temporary file for the witness let serialized_witnesses: Vec = - witness_values.try_into().expect("could not serialize witness map"); + witness_stack.try_into().expect("could not serialize witness map"); let witness_path = temp_directory.join("witness").with_extension("tr"); write_to_file(&serialized_witnesses, &witness_path); diff --git a/noir/noir-repo/tooling/debugger/src/context.rs b/noir/noir-repo/tooling/debugger/src/context.rs index f0de8d5d1c8..ba12a20460d 100644 --- a/noir/noir-repo/tooling/debugger/src/context.rs +++ b/noir/noir-repo/tooling/debugger/src/context.rs @@ -381,6 +381,9 @@ impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { ACVMStatus::RequiresForeignCall(_) => { unreachable!("Unexpected pending foreign call resolution"); } + ACVMStatus::RequiresAcirCall(_) => { + todo!("Multiple ACIR calls are not supported"); + } } } diff --git a/noir/noir-repo/tooling/nargo/src/errors.rs b/noir/noir-repo/tooling/nargo/src/errors.rs index c743768bee2..ff238d79a46 100644 --- a/noir/noir-repo/tooling/nargo/src/errors.rs +++ b/noir/noir-repo/tooling/nargo/src/errors.rs @@ -64,7 +64,9 @@ impl NargoError { ExecutionError::SolvingError(error) => match error { OpcodeResolutionError::IndexOutOfBounds { .. } | OpcodeResolutionError::OpcodeNotSolvable(_) - | OpcodeResolutionError::UnsatisfiedConstrain { .. } => None, + | OpcodeResolutionError::UnsatisfiedConstrain { .. } + | OpcodeResolutionError::AcirMainCallAttempted { .. } + | OpcodeResolutionError::AcirCallOutputsMismatch { .. } => None, OpcodeResolutionError::BrilligFunctionFailed { message, .. } => Some(message), OpcodeResolutionError::BlackBoxFunctionFailed(_, reason) => Some(reason), }, diff --git a/noir/noir-repo/tooling/nargo/src/ops/execute.rs b/noir/noir-repo/tooling/nargo/src/ops/execute.rs index 370393fea09..6d328d65119 100644 --- a/noir/noir-repo/tooling/nargo/src/ops/execute.rs +++ b/noir/noir-repo/tooling/nargo/src/ops/execute.rs @@ -1,5 +1,7 @@ +use acvm::acir::circuit::Program; +use acvm::acir::native_types::WitnessStack; use acvm::brillig_vm::brillig::ForeignCallResult; -use acvm::pwg::{ACVMStatus, ErrorLocation, OpcodeResolutionError, ACVM}; +use acvm::pwg::{ACVMStatus, ErrorLocation, OpcodeNotSolvable, OpcodeResolutionError, ACVM}; use acvm::BlackBoxFunctionSolver; use acvm::{acir::circuit::Circuit, acir::native_types::WitnessMap}; @@ -8,73 +10,145 @@ use crate::NargoError; use super::foreign_calls::{ForeignCallExecutor, NargoForeignCallResult}; -#[tracing::instrument(level = "trace", skip_all)] -pub fn execute_circuit( - circuit: &Circuit, - initial_witness: WitnessMap, - blackbox_solver: &B, - foreign_call_executor: &mut F, -) -> Result { - let mut acvm = ACVM::new(blackbox_solver, &circuit.opcodes, initial_witness); - - // This message should be resolved by a nargo foreign call only when we have an unsatisfied assertion. - let mut assert_message: Option = None; - loop { - let solver_status = acvm.solve(); - - match solver_status { - ACVMStatus::Solved => break, - ACVMStatus::InProgress => { - unreachable!("Execution should not stop while in `InProgress` state.") - } - ACVMStatus::Failure(error) => { - let call_stack = match &error { - OpcodeResolutionError::UnsatisfiedConstrain { - opcode_location: ErrorLocation::Resolved(opcode_location), - } => Some(vec![*opcode_location]), - OpcodeResolutionError::BrilligFunctionFailed { call_stack, .. } => { - Some(call_stack.clone()) - } - _ => None, - }; - - return Err(NargoError::ExecutionError(match call_stack { - Some(call_stack) => { - // First check whether we have a runtime assertion message that should be resolved on an ACVM failure - // If we do not have a runtime assertion message, we should check whether the circuit has any hardcoded - // messages associated with a specific `OpcodeLocation`. - // Otherwise return the provided opcode resolution error. - if let Some(assert_message) = assert_message { - ExecutionError::AssertionFailed(assert_message.to_owned(), call_stack) - } else if let Some(assert_message) = circuit.get_assert_message( - *call_stack.last().expect("Call stacks should not be empty"), - ) { - ExecutionError::AssertionFailed(assert_message.to_owned(), call_stack) - } else { - ExecutionError::SolvingError(error) +struct ProgramExecutor<'a, B: BlackBoxFunctionSolver, F: ForeignCallExecutor> { + functions: &'a [Circuit], + // This gets built as we run through the program looking at each function call + witness_stack: WitnessStack, + + blackbox_solver: &'a B, + + foreign_call_executor: &'a mut F, +} + +impl<'a, B: BlackBoxFunctionSolver, F: ForeignCallExecutor> ProgramExecutor<'a, B, F> { + fn new( + functions: &'a [Circuit], + blackbox_solver: &'a B, + foreign_call_executor: &'a mut F, + ) -> Self { + ProgramExecutor { + functions, + witness_stack: WitnessStack::default(), + blackbox_solver, + foreign_call_executor, + } + } + + fn finalize(self) -> WitnessStack { + self.witness_stack + } + + #[tracing::instrument(level = "trace", skip_all)] + fn execute_circuit( + &mut self, + circuit: &Circuit, + initial_witness: WitnessMap, + ) -> Result { + let mut acvm = ACVM::new(self.blackbox_solver, &circuit.opcodes, initial_witness); + + // This message should be resolved by a nargo foreign call only when we have an unsatisfied assertion. + let mut assert_message: Option = None; + loop { + let solver_status = acvm.solve(); + + match solver_status { + ACVMStatus::Solved => break, + ACVMStatus::InProgress => { + unreachable!("Execution should not stop while in `InProgress` state.") + } + ACVMStatus::Failure(error) => { + let call_stack = match &error { + OpcodeResolutionError::UnsatisfiedConstrain { + opcode_location: ErrorLocation::Resolved(opcode_location), + } => Some(vec![*opcode_location]), + OpcodeResolutionError::BrilligFunctionFailed { call_stack, .. } => { + Some(call_stack.clone()) } - } - None => ExecutionError::SolvingError(error), - })); - } - ACVMStatus::RequiresForeignCall(foreign_call) => { - let foreign_call_result = foreign_call_executor.execute(&foreign_call)?; - match foreign_call_result { - NargoForeignCallResult::BrilligOutput(foreign_call_result) => { - acvm.resolve_pending_foreign_call(foreign_call_result); - } - NargoForeignCallResult::ResolvedAssertMessage(message) => { - if assert_message.is_some() { - unreachable!("Resolving an assert message should happen only once as the VM should have failed"); + _ => None, + }; + + return Err(NargoError::ExecutionError(match call_stack { + Some(call_stack) => { + // First check whether we have a runtime assertion message that should be resolved on an ACVM failure + // If we do not have a runtime assertion message, we should check whether the circuit has any hardcoded + // messages associated with a specific `OpcodeLocation`. + // Otherwise return the provided opcode resolution error. + if let Some(assert_message) = assert_message { + ExecutionError::AssertionFailed( + assert_message.to_owned(), + call_stack, + ) + } else if let Some(assert_message) = circuit.get_assert_message( + *call_stack.last().expect("Call stacks should not be empty"), + ) { + ExecutionError::AssertionFailed( + assert_message.to_owned(), + call_stack, + ) + } else { + ExecutionError::SolvingError(error) + } + } + None => ExecutionError::SolvingError(error), + })); + } + ACVMStatus::RequiresForeignCall(foreign_call) => { + let foreign_call_result = self.foreign_call_executor.execute(&foreign_call)?; + match foreign_call_result { + NargoForeignCallResult::BrilligOutput(foreign_call_result) => { + acvm.resolve_pending_foreign_call(foreign_call_result); } - assert_message = Some(message); + NargoForeignCallResult::ResolvedAssertMessage(message) => { + if assert_message.is_some() { + unreachable!("Resolving an assert message should happen only once as the VM should have failed"); + } + assert_message = Some(message); - acvm.resolve_pending_foreign_call(ForeignCallResult::default()); + acvm.resolve_pending_foreign_call(ForeignCallResult::default()); + } } } + ACVMStatus::RequiresAcirCall(call_info) => { + let acir_to_call = &self.functions[call_info.id as usize]; + let initial_witness = call_info.initial_witness; + let call_solved_witness = + self.execute_circuit(acir_to_call, initial_witness)?; + let mut call_resolved_outputs = Vec::new(); + for return_witness_index in acir_to_call.return_values.indices() { + if let Some(return_value) = + call_solved_witness.get_index(return_witness_index) + { + call_resolved_outputs.push(*return_value); + } else { + return Err(ExecutionError::SolvingError( + OpcodeNotSolvable::MissingAssignment(return_witness_index).into(), + ) + .into()); + } + } + acvm.resolve_pending_acir_call(call_resolved_outputs); + self.witness_stack.push(call_info.id, call_solved_witness); + } } } + + Ok(acvm.finalize()) } +} + +#[tracing::instrument(level = "trace", skip_all)] +pub fn execute_program( + program: &Program, + initial_witness: WitnessMap, + blackbox_solver: &B, + foreign_call_executor: &mut F, +) -> Result { + let main = &program.functions[0]; + + let mut executor = + ProgramExecutor::new(&program.functions, blackbox_solver, foreign_call_executor); + let main_witness = executor.execute_circuit(main, initial_witness)?; + executor.witness_stack.push(0, main_witness); - Ok(acvm.finalize()) + Ok(executor.finalize()) } diff --git a/noir/noir-repo/tooling/nargo/src/ops/mod.rs b/noir/noir-repo/tooling/nargo/src/ops/mod.rs index 55e9e927800..2f5e6ebb7d4 100644 --- a/noir/noir-repo/tooling/nargo/src/ops/mod.rs +++ b/noir/noir-repo/tooling/nargo/src/ops/mod.rs @@ -2,7 +2,7 @@ pub use self::compile::{ collect_errors, compile_contract, compile_program, compile_program_with_debug_instrumenter, compile_workspace, report_errors, }; -pub use self::execute::execute_circuit; +pub use self::execute::execute_program; pub use self::foreign_calls::{ DefaultForeignCallExecutor, ForeignCall, ForeignCallExecutor, NargoForeignCallResult, }; diff --git a/noir/noir-repo/tooling/nargo/src/ops/test.rs b/noir/noir-repo/tooling/nargo/src/ops/test.rs index 8cf2934da4d..45b1a88f99c 100644 --- a/noir/noir-repo/tooling/nargo/src/ops/test.rs +++ b/noir/noir-repo/tooling/nargo/src/ops/test.rs @@ -1,4 +1,7 @@ -use acvm::{acir::native_types::WitnessMap, BlackBoxFunctionSolver}; +use acvm::{ + acir::native_types::{WitnessMap, WitnessStack}, + BlackBoxFunctionSolver, +}; use noirc_driver::{compile_no_check, CompileError, CompileOptions}; use noirc_errors::{debug_info::DebugInfo, FileDiagnostic}; use noirc_evaluator::errors::RuntimeError; @@ -6,7 +9,7 @@ use noirc_frontend::hir::{def_map::TestFunction, Context}; use crate::{errors::try_to_diagnose_runtime_error, NargoError}; -use super::{execute_circuit, DefaultForeignCallExecutor}; +use super::{execute_program, DefaultForeignCallExecutor}; pub enum TestStatus { Pass, @@ -33,9 +36,8 @@ pub fn run_test( Ok(compiled_program) => { // Run the backend to ensure the PWG evaluates functions like std::hash::pedersen, // otherwise constraints involving these expressions will not error. - let circuit_execution = execute_circuit( - // TODO(https://github.com/noir-lang/noir/issues/4428) - &compiled_program.program.functions[0], + let circuit_execution = execute_program( + &compiled_program.program, WitnessMap::new(), blackbox_solver, &mut DefaultForeignCallExecutor::new(show_output, foreign_call_resolver_url), @@ -83,7 +85,7 @@ fn test_status_program_compile_fail(err: CompileError, test_function: &TestFunct fn test_status_program_compile_pass( test_function: &TestFunction, debug: DebugInfo, - circuit_execution: Result, + circuit_execution: Result, ) -> TestStatus { let circuit_execution_err = match circuit_execution { // Circuit execution was successful; ie no errors or unsatisfied constraints diff --git a/noir/noir-repo/tooling/nargo_cli/src/cli/debug_cmd.rs b/noir/noir-repo/tooling/nargo_cli/src/cli/debug_cmd.rs index 1f448105ee2..4f3e2886b2e 100644 --- a/noir/noir-repo/tooling/nargo_cli/src/cli/debug_cmd.rs +++ b/noir/noir-repo/tooling/nargo_cli/src/cli/debug_cmd.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use acvm::acir::native_types::WitnessMap; +use acvm::acir::native_types::{WitnessMap, WitnessStack}; use bn254_blackbox_solver::Bn254BlackBoxSolver; use clap::Args; @@ -187,7 +187,11 @@ fn run_async( } if let Some(witness_name) = witness_name { - let witness_path = save_witness_to_dir(solved_witness, witness_name, target_dir)?; + let witness_path = save_witness_to_dir( + WitnessStack::from(solved_witness), + witness_name, + target_dir, + )?; println!("[{}] Witness saved to {}", package.name, witness_path.display()); } diff --git a/noir/noir-repo/tooling/nargo_cli/src/cli/execute_cmd.rs b/noir/noir-repo/tooling/nargo_cli/src/cli/execute_cmd.rs index 022bf7b761e..697f6d7c1ea 100644 --- a/noir/noir-repo/tooling/nargo_cli/src/cli/execute_cmd.rs +++ b/noir/noir-repo/tooling/nargo_cli/src/cli/execute_cmd.rs @@ -1,4 +1,4 @@ -use acvm::acir::native_types::WitnessMap; +use acvm::acir::native_types::WitnessStack; use bn254_blackbox_solver::Bn254BlackBoxSolver; use clap::Args; @@ -91,7 +91,7 @@ pub(crate) fn run( let compiled_program = nargo::ops::transform_program(compiled_program, expression_width); - let (return_value, solved_witness) = execute_program_and_decode( + let (return_value, witness_stack) = execute_program_and_decode( compiled_program, package, &args.prover_name, @@ -103,7 +103,7 @@ pub(crate) fn run( println!("[{}] Circuit output: {return_value:?}", package.name); } if let Some(witness_name) = &args.witness_name { - let witness_path = save_witness_to_dir(solved_witness, witness_name, target_dir)?; + let witness_path = save_witness_to_dir(witness_stack, witness_name, target_dir)?; println!("[{}] Witness saved to {}", package.name, witness_path.display()); } @@ -116,35 +116,37 @@ fn execute_program_and_decode( package: &Package, prover_name: &str, foreign_call_resolver_url: Option<&str>, -) -> Result<(Option, WitnessMap), CliError> { +) -> Result<(Option, WitnessStack), CliError> { // Parse the initial witness values from Prover.toml let (inputs_map, _) = read_inputs_from_file(&package.root_dir, prover_name, Format::Toml, &program.abi)?; - let solved_witness = execute_program(&program, &inputs_map, foreign_call_resolver_url)?; + let witness_stack = execute_program(&program, &inputs_map, foreign_call_resolver_url)?; let public_abi = program.abi.public_abi(); - let (_, return_value) = public_abi.decode(&solved_witness)?; + // Get the entry point witness for the ABI + let main_witness = + &witness_stack.peek().expect("Should have at least one witness on the stack").witness; + let (_, return_value) = public_abi.decode(main_witness)?; - Ok((return_value, solved_witness)) + Ok((return_value, witness_stack)) } pub(crate) fn execute_program( compiled_program: &CompiledProgram, inputs_map: &InputMap, foreign_call_resolver_url: Option<&str>, -) -> Result { +) -> Result { let blackbox_solver = Bn254BlackBoxSolver::new(); let initial_witness = compiled_program.abi.encode(inputs_map, None)?; - // TODO(https://github.com/noir-lang/noir/issues/4428) - let solved_witness_err = nargo::ops::execute_circuit( - &compiled_program.program.functions[0], + let solved_witness_stack_err = nargo::ops::execute_program( + &compiled_program.program, initial_witness, &blackbox_solver, &mut DefaultForeignCallExecutor::new(true, foreign_call_resolver_url), ); - match solved_witness_err { - Ok(solved_witness) => Ok(solved_witness), + match solved_witness_stack_err { + Ok(solved_witness_stack) => Ok(solved_witness_stack), Err(err) => { let debug_artifact = DebugArtifact { debug_symbols: vec![compiled_program.debug.clone()], diff --git a/noir/noir-repo/tooling/nargo_cli/src/cli/fs/witness.rs b/noir/noir-repo/tooling/nargo_cli/src/cli/fs/witness.rs index 52b5c385e5d..613cdec28da 100644 --- a/noir/noir-repo/tooling/nargo_cli/src/cli/fs/witness.rs +++ b/noir/noir-repo/tooling/nargo_cli/src/cli/fs/witness.rs @@ -1,21 +1,19 @@ use std::path::{Path, PathBuf}; -use acvm::acir::native_types::{WitnessMap, WitnessStack}; +use acvm::acir::native_types::WitnessStack; use nargo::constants::WITNESS_EXT; use super::{create_named_dir, write_to_file}; use crate::errors::FilesystemError; pub(crate) fn save_witness_to_dir>( - witnesses: WitnessMap, + witness_stack: 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(WITNESS_EXT); - // TODO(https://github.com/noir-lang/noir/issues/4428) - let witness_stack: WitnessStack = witnesses.into(); let buf: Vec = witness_stack.try_into()?; write_to_file(buf.as_slice(), &witness_path); diff --git a/noir/noir-repo/tooling/nargo_cli/src/cli/prove_cmd.rs b/noir/noir-repo/tooling/nargo_cli/src/cli/prove_cmd.rs index 272f2fa8e5d..b9e4bca9e69 100644 --- a/noir/noir-repo/tooling/nargo_cli/src/cli/prove_cmd.rs +++ b/noir/noir-repo/tooling/nargo_cli/src/cli/prove_cmd.rs @@ -1,4 +1,3 @@ -use acvm::acir::native_types::WitnessStack; use clap::Args; use nargo::constants::{PROVER_INPUT_FILE, VERIFIER_INPUT_FILE}; use nargo::ops::{compile_program, report_errors}; @@ -123,12 +122,14 @@ pub(crate) fn prove_package( let (inputs_map, _) = read_inputs_from_file(&package.root_dir, prover_name, Format::Toml, &compiled_program.abi)?; - let solved_witness = - execute_program(&compiled_program, &inputs_map, foreign_call_resolver_url)?; + let witness_stack = execute_program(&compiled_program, &inputs_map, foreign_call_resolver_url)?; // Write public inputs into Verifier.toml let public_abi = compiled_program.abi.public_abi(); - let (public_inputs, return_value) = public_abi.decode(&solved_witness)?; + // Get the entry point witness for the ABI + let main_witness = + &witness_stack.peek().expect("Should have at least one witness on the stack").witness; + let (public_inputs, return_value) = public_abi.decode(main_witness)?; write_inputs_to_file( &public_inputs, @@ -139,7 +140,7 @@ pub(crate) fn prove_package( Format::Toml, )?; - let proof = backend.prove(&compiled_program.program, WitnessStack::from(solved_witness))?; + let proof = backend.prove(&compiled_program.program, witness_stack)?; if check_proof { let public_inputs = public_abi.encode(&public_inputs, return_value)?;