Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Linking circuits with the databus #7707

Merged
merged 17 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion barretenberg/cpp/src/barretenberg/aztec_ivc/aztec_ivc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ void AztecIVC::accumulate(ClientCircuit& circuit, const std::shared_ptr<Verifica
ASSERT(circuit_count % 2 == 0); // ensure this is a kernel

for (auto& [proof, vkey] : verification_queue) {
// Perform folding recursive verification
FoldingRecursiveVerifier verifier{ &circuit, { verifier_accumulator, { vkey } } };
auto verifier_accum = verifier.verify_folding_proof(proof);
verifier_accumulator = std::make_shared<VerifierInstance>(verifier_accum->get_value());
info("Num gates = ", circuit.get_num_gates());

// Perform databus commitment consistency checks and propagate return data commitments via the public inputs
bus_depot.execute(verifier.instances);
}
verification_queue.clear();
}
Expand All @@ -45,6 +48,11 @@ void AztecIVC::accumulate(ClientCircuit& circuit, const std::shared_ptr<Verifica
instance_vk = std::make_shared<VerificationKey>(prover_instance->proving_key);
}

// Store whether the present circuit is a kernel (Note: the aztec architecture dictates that every second circuit
// is a kernel. This check can triggered/replaced by the presence of the recursive folding verify opcode once it is
// introduced into noir).
instance_vk->databus_propagation_data.is_kernel = (circuit_count % 2 == 0);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: comment on why every 2nd circuit is a kernel could be nice

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, left more explanation


// If this is the first circuit simply initialize the prover and verifier accumulator instances
if (circuit_count == 1) {
fold_output.accumulator = prover_instance;
Expand Down
6 changes: 6 additions & 0 deletions barretenberg/cpp/src/barretenberg/aztec_ivc/aztec_ivc.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "barretenberg/protogalaxy/decider_verifier.hpp"
#include "barretenberg/protogalaxy/protogalaxy_prover.hpp"
#include "barretenberg/protogalaxy/protogalaxy_verifier.hpp"
#include "barretenberg/stdlib/primitives/databus/databus.hpp"
#include "barretenberg/sumcheck/instance/instances.hpp"
#include "barretenberg/ultra_honk/decider_prover.hpp"
#include <algorithm>
Expand Down Expand Up @@ -45,6 +46,8 @@ class AztecIVC {
using FoldingRecursiveVerifier =
bb::stdlib::recursion::honk::ProtoGalaxyRecursiveVerifier_<RecursiveVerifierInstances>;

using DataBusDepot = stdlib::DataBusDepot<ClientCircuit>;

// A full proof for the IVC scheme
struct Proof {
FoldProof folding_proof; // final fold proof
Expand Down Expand Up @@ -78,6 +81,9 @@ class AztecIVC {
// Set of pairs of {fold_proof, verification_key} to be recursively verified
std::vector<FoldingVerifierInputs> verification_queue;

// Management of linking databus commitments between circuits in the IVC
DataBusDepot bus_depot;

// A flag indicating whether or not to construct a structured trace in the ProverInstance
TraceStructure trace_structure = TraceStructure::NONE;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,10 @@ TEST_F(AztecIVCTests, Basic)

/**
* @brief Check that the IVC fails to verify if an intermediate fold proof is invalid
* @details When accumulating 4 circuits, there are 3 fold proofs to verify (the first two are recursively verfied and
* the 3rd is verified as part of the IVC proof). Check that if any of one of these proofs is invalid, the IVC will fail
* @details When accumulating 4 circuits, there are 3 fold proofs to verify (the first two are recursively verfied
and
* the 3rd is verified as part of the IVC proof). Check that if any of one of these proofs is invalid, the IVC will
fail
* to verify.
*
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#pragma once
#include "../circuit_builders/circuit_builders_fwd.hpp"
#include "../field/field.hpp"
#include "barretenberg/stdlib/honk_recursion/verifier/recursive_instances.hpp"
#include "barretenberg/stdlib/primitives/curves/bn254.hpp"
#include "barretenberg/stdlib_circuit_builders/databus.hpp"

namespace bb::stdlib {
Expand Down Expand Up @@ -54,4 +56,196 @@ template <typename Builder> class databus {
bus_vector secondary_calldata{ BusId::SECONDARY_CALLDATA };
bus_vector return_data{ BusId::RETURNDATA };
};

/**
* @brief Class for managing the linking circuit input/output via the databus
*
* @tparam Builder
*/
template <class Builder> class DataBusDepot {
public:
using Curve = stdlib::bn254<Builder>;
using Commitment = typename Curve::Group;
using Fr = typename Curve::ScalarField;
using Fq = typename Curve::BaseField;

using RecursiveFlavor = MegaRecursiveFlavor_<Builder>;
using RecursiveVerifierInstances = bb::stdlib::recursion::honk::RecursiveVerifierInstances_<RecursiveFlavor, 2>;

static constexpr size_t NUM_FR_LIMBS_PER_FQ = Fq::NUM_LIMBS;
static constexpr size_t NUM_FR_LIMBS_PER_COMMITMENT = NUM_FR_LIMBS_PER_FQ * 2;

/**
* @brief Execute circuit logic to establish proper transfer of databus data between circuits
* @details The databus mechanism establishes the transfer of data between two circuits (i-1 and i) in a third
* circuit (i+1) via commitment equality checks of the form [R_{i-1}] = [C_i]. In practice, circuit (i+1) is given
* access to [R_{i-1}] via the public inputs of \pi_i, and it has access to [C_i] directly from \pi_i. The
* consistency checks in circuit (i+1) are thus of the form \pi_i.public_inputs.[R_{i-1}] = \pi_i.[C_i]. This method
* peforms the two primary operations required for these checks: (1) extract commitments [R] from proofs received as
* private witnesses and propagate them to the next circuit via adding them to the public inputs. (2) Assert
* equality of commitments.
*
* In Aztec private function execution, this mechanism is used as follows. Kernel circuit K_{i+1} must in general
* perform two databus consistency checks: (1) that the return_data of app circuit A_{i} was calldata to K_{i}, and
* (2) that the return_data of K_{i-1} was calldata to K_{i}. (Note that kernel circuits have two databus calldata
* columns). The relevant databus column commitments are extracted from non-accumulator verifier instances (which
* contain all witness polynomial commitments extracted from a proof in oink).
*
* @param instances Completed verifier instances corresponding to prover instances that have been folded
*/
void execute(RecursiveVerifierInstances& instances)
{
// Upon completion of folding recursive verfication, the verifier contains two completed verifier instances
// which store data from a fold proof. The first is the instance into which we're folding and the second
// corresponds to an instance being folded.
auto inst_1 = instances[0]; // instance into which we're folding (an accumulator, except on the initial round)
auto inst_2 = instances[1]; // instance that has been folded

// The first folding round is a special case in that it folds an instance into a non-accumulator instance. The
// fold proof thus contains two oink proofs. The first oink proof (stored in first instance) contains the return
// data R_0' from the first app, and its calldata counterpart C_0' in the kernel will be contained in the second
// oink proof (stored in second instance). In this special case, we can check directly that \pi_0.R_0' =
// \pi_0.C_0', without having had to propagate the return data commitment via the public inputs.
if (!inst_1->is_accumulator) {
// Assert equality of \pi_0.R_0' and \pi_0.C_0'
auto& app_return_data = inst_1->witness_commitments.return_data; // \pi_0.R_0'
auto& secondary_calldata = inst_2->witness_commitments.secondary_calldata; // \pi_0.C_0'
assert_equality_of_commitments(app_return_data, secondary_calldata); // assert equality R_0' == C_0'
}

// Define aliases for members in the second (non-accumulator) instance
bool is_kernel_instance = inst_2->verification_key->databus_propagation_data.is_kernel;
auto& propagation_data = inst_2->verification_key->databus_propagation_data;
auto& public_inputs = inst_2->public_inputs;
auto& commitments = inst_2->witness_commitments;

// Assert equality between return data commitments propagated via the public inputs and the corresponding
// calldata commitment
if (is_kernel_instance) { // only kernels can contain commitments propagated via public inputs
if (propagation_data.contains_app_return_data_commitment) {
// Assert equality between the app return data commitment and the kernel secondary calldata commitment
size_t start_idx = propagation_data.app_return_data_public_input_idx;
Commitment app_return_data = reconstruct_commitment_from_public_inputs(public_inputs, start_idx);
assert_equality_of_commitments(app_return_data, commitments.secondary_calldata);
}

if (propagation_data.contains_kernel_return_data_commitment) {
// Assert equality between the previous kernel return data commitment and the kernel calldata commitment
size_t start_idx = propagation_data.kernel_return_data_public_input_idx;
Commitment kernel_return_data = reconstruct_commitment_from_public_inputs(public_inputs, start_idx);
assert_equality_of_commitments(kernel_return_data, commitments.calldata);
}
}

// Propagate the return data commitment via the public inputs mechanism
propagate_commitment_via_public_inputs(commitments.return_data, is_kernel_instance);
};

/**
* @brief Set the witness indices for a commitment to public
* @details Indicate the presence of the propagated commitment by setting the corresponding flag and index in the
* public inputs. A distinction is made between kernel and app return data so consistency can be checked against the
* correct calldata entry later on.
*
* @param commitment
* @param is_kernel Indicates whether the return data being propagated is from a kernel or an app
*/
void propagate_commitment_via_public_inputs(Commitment& commitment, bool is_kernel = false)
{
auto context = commitment.get_context();

// Set flag indicating propagation of return data; save the index at which it will be stored in public inputs
size_t start_idx = context->public_inputs.size();
if (is_kernel) {
context->databus_propagation_data.contains_kernel_return_data_commitment = true;
context->databus_propagation_data.kernel_return_data_public_input_idx = start_idx;
} else {
context->databus_propagation_data.contains_app_return_data_commitment = true;
context->databus_propagation_data.app_return_data_public_input_idx = start_idx;
}

// Set public the witness indices corresponding to the limbs of the point coordinates
for (auto& index : get_witness_indices_for_commitment(commitment)) {
context->set_public_input(index);
}
}

/**
* @brief Reconstruct a commitment from limbs stored in public inputs
*
* @param public_inputs Vector of public inputs in which a propagated return data commitment is stored
* @param return_data_commitment_limbs_start_idx Start index for range where commitment limbs are stored
* @return Commitment
*/
Commitment reconstruct_commitment_from_public_inputs(const std::span<Fr> public_inputs,
size_t& return_data_commitment_limbs_start_idx)
{
// Extract from the public inputs the limbs needed reconstruct a commitment
std::span<Fr, NUM_FR_LIMBS_PER_COMMITMENT> return_data_commitment_limbs{
public_inputs.data() + return_data_commitment_limbs_start_idx, NUM_FR_LIMBS_PER_COMMITMENT
};
return reconstruct_commitment_from_fr_limbs(return_data_commitment_limbs);
}

private:
/**
* @brief Reconstruct a commitment (point) from the Fr limbs of the coordinates (Fq, Fq)
*
* @param limbs
* @return Commitment
*/
Commitment reconstruct_commitment_from_fr_limbs(std::span<Fr, NUM_FR_LIMBS_PER_COMMITMENT> limbs)
{
std::span<Fr, NUM_FR_LIMBS_PER_FQ> x_limbs{ limbs.data(), NUM_FR_LIMBS_PER_FQ };
std::span<Fr, NUM_FR_LIMBS_PER_FQ> y_limbs{ limbs.data() + NUM_FR_LIMBS_PER_FQ, NUM_FR_LIMBS_PER_FQ };
const Fq x = reconstruct_fq_from_fr_limbs(x_limbs);
const Fq y = reconstruct_fq_from_fr_limbs(y_limbs);

return Commitment(x, y);
}

/**
* @brief Reconstruct a bn254 Fq from four limbs represented as bn254 Fr's
*
* @param limbs
* @return Fq
*/
Fq reconstruct_fq_from_fr_limbs(std::span<Fr, NUM_FR_LIMBS_PER_FQ>& limbs)
{
const Fr l0 = limbs[0];
const Fr l1 = limbs[1];
const Fr l2 = limbs[2];
const Fr l3 = limbs[3];
l0.create_range_constraint(Fq::NUM_LIMB_BITS, "l0");
l1.create_range_constraint(Fq::NUM_LIMB_BITS, "l1");
l2.create_range_constraint(Fq::NUM_LIMB_BITS, "l2");
l3.create_range_constraint(Fq::NUM_LAST_LIMB_BITS, "l3");
return Fq(l0, l1, l2, l3, /*can_overflow=*/false);
}

void assert_equality_of_commitments(Commitment& P0, Commitment& P1)
{
P0.x.assert_equal(P1.x);
P0.y.assert_equal(P1.y);
}

/**
* @brief Get the witness indices for a commitment (biggroup)
*
* @param point A biggroup element
* @return std::array<uint32_t, NUM_FR_LIMBS_PER_COMMITMENT>
*/
std::array<uint32_t, NUM_FR_LIMBS_PER_COMMITMENT> get_witness_indices_for_commitment(Commitment& point)
{
return { point.x.binary_basis_limbs[0].element.normalize().witness_index,
point.x.binary_basis_limbs[1].element.normalize().witness_index,
point.x.binary_basis_limbs[2].element.normalize().witness_index,
point.x.binary_basis_limbs[3].element.normalize().witness_index,
point.y.binary_basis_limbs[0].element.normalize().witness_index,
point.y.binary_basis_limbs[1].element.normalize().witness_index,
point.y.binary_basis_limbs[2].element.normalize().witness_index,
point.y.binary_basis_limbs[3].element.normalize().witness_index };
}
};

} // namespace bb::stdlib
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,29 @@ struct BusVector {
using DataBus = std::array<BusVector, 3>;
enum class BusId { CALLDATA, SECONDARY_CALLDATA, RETURNDATA };

/**
* @brief Data indicating the presence of databus return data commitments in the public inputs of the circuit
* @details The databus mechanism establishes the transfer of data between two circuits (i-1 and i) in a third circuit
* (i+1) via commitment equality checks of the form [R_{i-1}] = [C_i]. The return data commitment \pi_{i-1}.[R_{i-1}] is
* a private witness of circuit i, which extracts it and propagates it to the next circuit via the traditional public
* inputs mechanism. (I.e. the private witnesses corresponding to the commitment [R_{i-1}] are set to public). Since
* commitment [C_i] is part of the proof \pi_i, circuit i+1 can perform the required consistency check via
* \pi_i.public_inputs.[R_{i-1}] = \pi_i.[C_i].
*
*/
struct DatabusPropagationData {
// Flags indicating whether the public inputs contain commitment(s) to app/kernel return data
bool contains_app_return_data_commitment = false;
bool contains_kernel_return_data_commitment = false;

// The start index of the return data commitments (if present) in the public inputs. Note: a start index is all
// that's needed here since the commitents are represented by a fixed number of witnesses and are contiguous in the
// public inputs by construction.
size_t app_return_data_public_input_idx = 0;
size_t kernel_return_data_public_input_idx = 0;

// Is this a kernel circuit (used to determine when databus consistency checks can be appended to a circuit in IVC)
bool is_kernel = false;
};

} // namespace bb
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ template <typename FF> void MegaCircuitBuilder_<FF>::finalize_circuit()
*
* @param in Structure containing variables and witness selectors
*/
// TODO(#423): This function adds valid (but arbitrary) gates to ensure that the circuit which includes
// them will not result in any zero-polynomials. It also ensures that the first coefficient of the wire
// polynomials is zero, which is required for them to be shiftable.
// TODO(https://github.com/AztecProtocol/barretenberg/issues/1066): This function adds valid (but arbitrary) gates to
// ensure that the circuit which includes them will not result in any zero-polynomials. It also ensures that the first
// coefficient of the wire polynomials is zero, which is required for them to be shiftable.
template <typename FF> void MegaCircuitBuilder_<FF>::add_gates_to_ensure_all_polys_are_non_zero()
{
// Most polynomials are handled via the conventional Ultra method
Expand All @@ -34,20 +34,25 @@ template <typename FF> void MegaCircuitBuilder_<FF>::add_gates_to_ensure_all_pol
// All that remains is to handle databus related and poseidon2 related polynomials. In what follows we populate the
// calldata with some mock data then constuct a single calldata read gate

// Define a single dummy value to add to all databus columns. Note: This value must be equal across all columns in
// order for inter-circuit databus commitment checks to pass in IVC settings. These dummy gates can be deleted with
// all of the others when (https://github.com/AztecProtocol/barretenberg/issues/1066) is resolved.
FF databus_dummy_value = 25;

// Create an arbitrary calldata read gate
add_public_calldata(this->add_variable(25)); // ensure there is at least one entry in calldata
add_public_calldata(this->add_variable(databus_dummy_value)); // add one entry in calldata
auto raw_read_idx = static_cast<uint32_t>(get_calldata().size()) - 1; // read data that was just added
auto read_idx = this->add_variable(raw_read_idx);
read_calldata(read_idx);

// Create an arbitrary secondary_calldata read gate
add_public_secondary_calldata(this->add_variable(25)); // ensure there is at least one entry in secondary_calldata
add_public_secondary_calldata(this->add_variable(databus_dummy_value)); // add one entry in secondary_calldata
raw_read_idx = static_cast<uint32_t>(get_secondary_calldata().size()) - 1; // read data that was just added
read_idx = this->add_variable(raw_read_idx);
read_secondary_calldata(read_idx);

// Create an arbitrary return data read gate
add_public_return_data(this->add_variable(17)); // ensure there is at least one entry in return data
add_public_return_data(this->add_variable(databus_dummy_value)); // add one entry in return data
raw_read_idx = static_cast<uint32_t>(get_return_data().size()) - 1; // read data that was just added
read_idx = this->add_variable(raw_read_idx);
read_return_data(read_idx);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ template <typename FF> class MegaCircuitBuilder_ : public UltraCircuitBuilder_<M
ecc_op_tuple queue_ecc_mul_accum(const g1::affine_element& point, const FF& scalar);
ecc_op_tuple queue_ecc_eq();

// Metadata for propagating databus return data commitments via the public input mechanism
DatabusPropagationData databus_propagation_data;

private:
ecc_op_tuple populate_ecc_op_wires(const UltraOp& ultra_op);
void set_goblin_ecc_op_code_constant_variables();
Expand Down
Loading
Loading