Skip to content

Commit

Permalink
feat(acvm): Execute multiple circuits (#5380)
Browse files Browse the repository at this point in the history
Resolves noir-lang/noir#4428

This is a followup to
#5341 which does the
initial ACIR generation work for multiple ACIR functions.

Execution is now done by moving `execute_circuit` to be part of a
stateful `ProgramExecutor` that builds a witness stack for every
completed `execute_circuit` call. An initial `execute_program` function
instantiates the `ProgramExecutor` and starts execution on our `main`
entry point circuit. When a `Call` opcode is reached we pause execution
and recursively call `execute_circuit`, which then returns the solved
witness for that call. We then resolve the outputs of that execution by
reading the return witnesses from the inner solved witness. We then push
the nested call solved witness onto the witness stack and continue
execution of our main ACVM instance. This is quite similar to the
process used by foreign calls with Brillig, except it is now done with
the main ACVM rather than the contained Brillig VM.

This witness stack and program (list of `Circuit` functions) then gets
passed to the backend. For now, I have only done an additive
`prove_and_verify_ultra_honk_program` to show the process working for
the basic non-inlined ACIR programs supplied here. I wanted to leave any
WASM exports or ACVM JS changes for a follow-up PR as they are quite a
bit of changes on their own.

---------

Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com>
Co-authored-by: Tom French <tom@tomfren.ch>
Co-authored-by: jfecher <jfecher11@gmail.com>
  • Loading branch information
4 people authored Mar 29, 2024
1 parent a979150 commit bb71920
Show file tree
Hide file tree
Showing 25 changed files with 480 additions and 130 deletions.
2 changes: 2 additions & 0 deletions barretenberg/acir_tests/Dockerfile.bb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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
73 changes: 61 additions & 12 deletions barretenberg/cpp/src/barretenberg/bb/main.cpp
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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<acir_format::AcirFormat> 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
*
Expand Down Expand Up @@ -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 <IsUltraFlavor Flavor> bool proveAndVerifyHonk(const std::string& bytecodePath, const std::string& witnessPath)
template <IsUltraFlavor Flavor>
bool proveAndVerifyHonkAcirFormat(acir_format::AcirFormat constraint_system, acir_format::WitnessVector witness)
{
using Builder = Flavor::CircuitBuilder;
using Prover = UltraProver_<Flavor>;
using Verifier = UltraVerifier_<Flavor>;
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<Builder>(constraint_system, 0, witness);

Expand All @@ -165,6 +168,49 @@ template <IsUltraFlavor Flavor> 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 <IsUltraFlavor Flavor> 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<Flavor>(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 <IsUltraFlavor Flavor>
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<Flavor>(constraint_system, witness)) {
return false;
}
witness_stack.pop_back();
}
return true;
}

/**
* @brief Proves and Verifies an ACIR circuit
*
Expand Down Expand Up @@ -576,6 +622,9 @@ int main(int argc, char* argv[])
if (command == "prove_and_verify_goblin_ultra_honk") {
return proveAndVerifyHonk<GoblinUltraFlavor>(bytecode_path, witness_path) ? 0 : 1;
}
if (command == "prove_and_verify_ultra_honk_program") {
return proveAndVerifyHonkProgram<UltraFlavor>(bytecode_path, witness_path) ? 0 : 1;
}
if (command == "prove_and_verify_goblin") {
return proveAndVerifyGoblin(bytecode_path, witness_path) ? 0 : 1;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "recursion_constraint.hpp"
#include "schnorr_verify.hpp"
#include "sha256_constraint.hpp"
#include <utility>

namespace acir_format {

Expand Down Expand Up @@ -93,6 +94,7 @@ struct AcirFormat {
};

using WitnessVector = std::vector<fr, ContainerSlabAllocator<fr>>;
using WitnessVectorStack = std::vector<std::pair<uint32_t, WitnessVector>>;

template <typename Builder = UltraCircuitBuilder>
Builder create_circuit(const AcirFormat& constraint_system, size_t size_hint = 0, WitnessVector const& witness = {});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "barretenberg/plonk_honk_shared/arithmetization/gate_data.hpp"
#include "serde/index.hpp"
#include <iterator>
#include <utility>

namespace acir_format {

Expand Down Expand Up @@ -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<uint8_t> 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;
Expand Down Expand Up @@ -406,24 +403,29 @@ AcirFormat circuit_buf_to_acir_format(std::vector<uint8_t> const& buf)
return af;
}

AcirFormat circuit_buf_to_acir_format(std::vector<uint8_t> 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<uint8_t> 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.
Expand All @@ -437,4 +439,48 @@ WitnessVector witness_buf_to_witness_data(std::vector<uint8_t> 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<uint8_t> 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<AcirFormat> program_buf_to_acir_format(std::vector<uint8_t> const& buf)
{
auto program = Program::Program::bincodeDeserialize(buf);

std::vector<AcirFormat> 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<uint8_t> 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
14 changes: 14 additions & 0 deletions noir/noir-repo/acvm-repo/acir/src/native_types/witness_stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<WitnessMap> for WitnessStack {
fn from(witness: WitnessMap) -> Self {
let stack = vec![StackItem { index: 0, witness }];
Expand Down
Loading

0 comments on commit bb71920

Please sign in to comment.