Skip to content

Commit

Permalink
feat: Goblin Honk Composer/Prover/Verifier (#1220)
Browse files Browse the repository at this point in the history
# Description

The Composer, Prover and Verifier for Goblin Ultra Honk. These are
implemented as instantiations of the existing "Ultra" template classes,
e.g. `GoblinUltraComposer = UltraComposer_<flavor::GoblinUltra>`, etc.

This work makes it possible to create and verify a Goblin Ultra Honk
proof. "Verification" here means confirmation that the ECC op gates have
been correctly incorporated into the circuit (checked via a new
relation) plus genuine verification of the conventional UltraHonk proof.
The _correctness_ of the the ECC op gates is of course not verified, as
that is the role of the other components of the Goblin stack.

The new "op gate consistency" relation simply checks that the op wire
values have been correctly copied into the conventional wires (needed
for copy constraints) and that the op wire polynomials vanish everywhere
outside the range of ECC op gates.

# Checklist:

- [ ] I have reviewed my diff in github, line by line.
- [ ] Every change is related to the PR description.
- [ ] I have
[linked](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue)
this pull request to the issue(s) that it resolves.
- [ ] There are no unexpected formatting changes, superfluous debug
logs, or commented-out code.
- [ ] The branch has been merged or rebased against the head of its
merge target.
- [ ] I'm happy for the PR to be merged at the reviewer's next
convenience.

---------

Co-authored-by: ludamad <[email protected]>
  • Loading branch information
2 people authored and AztecBot committed Jul 27, 2023
1 parent 81e9257 commit 970bb07
Show file tree
Hide file tree
Showing 17 changed files with 1,158 additions and 216 deletions.
65 changes: 65 additions & 0 deletions cpp/src/barretenberg/honk/composer/goblin_ultra_composer.test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#include <cstddef>
#include <cstdint>
#include <gtest/gtest.h>

#include "barretenberg/common/log.hpp"
#include "barretenberg/honk/composer/ultra_composer.hpp"
#include "barretenberg/honk/proof_system/ultra_prover.hpp"
#include "barretenberg/proof_system/circuit_builder/ultra_circuit_builder.hpp"

using namespace proof_system::honk;

namespace test_ultra_honk_composer {

namespace {
auto& engine = numeric::random::get_debug_engine();
}

class GoblinUltraHonkComposerTests : public ::testing::Test {
protected:
static void SetUpTestSuite() { barretenberg::srs::init_crs_factory("../srs_db/ignition"); }
};

/**
* @brief Test proof construction/verification for a circuit with ECC op gates, public inputs, and basic arithmetic
* gates
*
*/
TEST_F(GoblinUltraHonkComposerTests, SimpleCircuit)
{
auto builder = UltraCircuitBuilder();

// Define an arbitrary number of operations/gates
size_t num_ecc_ops = 3;
size_t num_conventional_gates = 10;

// Add some ecc op gates
for (size_t i = 0; i < num_ecc_ops; ++i) {
auto point = g1::affine_one * fr::random_element();
auto scalar = fr::random_element();
builder.queue_ecc_mul_accum(point, scalar);
}

// Add some conventional gates that utlize public inputs
for (size_t i = 0; i < num_conventional_gates; ++i) {
fr a = fr::random_element();
fr b = fr::random_element();
fr c = fr::random_element();
fr d = a + b + c;
uint32_t a_idx = builder.add_public_variable(a);
uint32_t b_idx = builder.add_variable(b);
uint32_t c_idx = builder.add_variable(c);
uint32_t d_idx = builder.add_variable(d);

builder.create_big_add_gate({ a_idx, b_idx, c_idx, d_idx, fr(1), fr(1), fr(1), fr(-1), fr(0) });
}

auto composer = GoblinUltraComposer();
auto prover = composer.create_prover(builder);
auto verifier = composer.create_verifier(builder);
auto proof = prover.construct_proof();
bool verified = verifier.verify_proof(proof);
EXPECT_EQ(verified, true);
}

} // namespace test_ultra_honk_composer
107 changes: 71 additions & 36 deletions cpp/src/barretenberg/honk/composer/ultra_composer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ void UltraComposer_<Flavor>::compute_circuit_size_parameters(CircuitBuilder& cir
lookups_size += table.lookup_gates.size();
}

// Get num conventional gates, num public inputs and num Goblin style ECC op gates
const size_t num_gates = circuit_constructor.num_gates;
num_public_inputs = circuit_constructor.public_inputs.size();
num_ecc_op_gates = circuit_constructor.num_ecc_op_gates;

// minimum circuit size due to the length of lookups plus tables
const size_t minimum_circuit_size_due_to_lookups = tables_size + lookups_size + zero_row_offset;
const size_t minimum_circuit_size_due_to_lookups = tables_size + lookups_size + num_zero_rows;

// number of populated rows in the execution trace
const size_t num_rows_populated_in_execution_trace =
circuit_constructor.num_gates + circuit_constructor.public_inputs.size() + zero_row_offset;
size_t num_rows_populated_in_execution_trace = num_zero_rows + num_ecc_op_gates + num_public_inputs + num_gates;

// The number of gates is max(lookup gates + tables, rows already populated in trace) + 1, where the +1 is due to
// addition of a "zero row" at top of the execution trace to ensure wires and other polys are shiftable.
Expand All @@ -48,40 +50,31 @@ template <UltraFlavor Flavor> void UltraComposer_<Flavor>::compute_witness(Circu
return;
}

// At this point, the wires have been populated with as many values as rows in the execution trace. We need to pad
// with zeros up to the full length, i.e. total_num_gates = already populated rows of execution trace + tables_size.
for (size_t i = 0; i < tables_size; ++i) {
circuit_constructor.w_l.emplace_back(circuit_constructor.zero_idx);
circuit_constructor.w_r.emplace_back(circuit_constructor.zero_idx);
circuit_constructor.w_o.emplace_back(circuit_constructor.zero_idx);
circuit_constructor.w_4.emplace_back(circuit_constructor.zero_idx);
}

// Construct the conventional wire polynomials
auto wire_polynomials = construct_wire_polynomials_base<Flavor>(circuit_constructor, dyadic_circuit_size);

proving_key->w_l = wire_polynomials[0];
proving_key->w_r = wire_polynomials[1];
proving_key->w_o = wire_polynomials[2];
proving_key->w_4 = wire_polynomials[3];

// If Goblin, construct the ECC op queue wire polynomials
if constexpr (IsGoblinFlavor<Flavor>) {
construct_ecc_op_wire_polynomials(wire_polynomials);
}

// Construct the sorted concatenated list polynomials for the lookup argument
polynomial s_1(dyadic_circuit_size);
polynomial s_2(dyadic_circuit_size);
polynomial s_3(dyadic_circuit_size);
polynomial s_4(dyadic_circuit_size);
// TODO(luke): The +1 size for z_lookup is not necessary and can lead to confusion. Resolve.
polynomial z_lookup(dyadic_circuit_size + 1); // Only instantiated in this function; nothing assigned.

// TODO(kesha): Look at this once we figure out how we do ZK (previously we had roots cut out, so just added
// randomness)
// The size of empty space in sorted polynomials
size_t count = dyadic_circuit_size - tables_size - lookups_size;
ASSERT(count > 0); // We need at least 1 row of zeroes for the permutation argument
for (size_t i = 0; i < count; ++i) {
s_1[i] = 0;
s_2[i] = 0;
s_3[i] = 0;
s_4[i] = 0;
}

// The sorted list polynomials have (tables_size + lookups_size) populated entries. We define the index below so
// that these entries are written into the last indices of the polynomials. The values on the first
// dyadic_circuit_size - (tables_size + lookups_size) indices are automatically initialized to zero via the
// polynomial constructor.
size_t s_index = dyadic_circuit_size - tables_size - lookups_size;
ASSERT(s_index > 0); // We need at least 1 row of zeroes for the permutation argument

for (auto& table : circuit_constructor.lookup_tables) {
const fr table_index(table.table_index);
Expand Down Expand Up @@ -120,11 +113,11 @@ template <UltraFlavor Flavor> void UltraComposer_<Flavor>::compute_witness(Circu

for (const auto& entry : lookup_gates) {
const auto components = entry.to_sorted_list_components(table.use_twin_keys);
s_1[count] = components[0];
s_2[count] = components[1];
s_3[count] = components[2];
s_4[count] = table_index;
++count;
s_1[s_index] = components[0];
s_2[s_index] = components[1];
s_3[s_index] = components[2];
s_4[s_index] = table_index;
++s_index;
}
}

Expand All @@ -137,11 +130,10 @@ template <UltraFlavor Flavor> void UltraComposer_<Flavor>::compute_witness(Circu
// Copy memory read/write record data into proving key. Prover needs to know which gates contain a read/write
// 'record' witness on the 4th wire. This wire value can only be fully computed once the first 3 wire polynomials
// have been committed to. The 4th wire on these gates will be a random linear combination of the first 3 wires,
// using the plookup challenge `eta`. We need to update the records with an offset Because we shift the gates by
// the number of public inputs plus an additional offset for a zero row.
auto add_public_inputs_offset = [this](uint32_t gate_index) {
return gate_index + num_public_inputs + zero_row_offset;
};
// using the plookup challenge `eta`. We need to update the records with an offset Because we shift the gates to
// account for everything that comes before them in the execution trace, e.g. public inputs, a zero row, etc.
size_t offset = num_ecc_op_gates + num_public_inputs + num_zero_rows;
auto add_public_inputs_offset = [offset](uint32_t gate_index) { return gate_index + offset; };
proving_key->memory_read_records = std::vector<uint32_t>();
proving_key->memory_write_records = std::vector<uint32_t>();

Expand All @@ -157,6 +149,38 @@ template <UltraFlavor Flavor> void UltraComposer_<Flavor>::compute_witness(Circu
computed_witness = true;
}

/**
* @brief Construct Goblin style ECC op wire polynomials
* @details The Ecc op wire values are assumed to have already been stored in the corresponding block of the
* conventional wire polynomials. The values for the ecc op wire polynomials are set based on those values.
*
* @tparam Flavor
* @param wire_polynomials
*/
template <UltraFlavor Flavor> void UltraComposer_<Flavor>::construct_ecc_op_wire_polynomials(auto& wire_polynomials)
{
std::array<polynomial, Flavor::NUM_WIRES> op_wire_polynomials;
for (auto& poly : op_wire_polynomials) {
poly = polynomial(dyadic_circuit_size);
}

// The ECC op wires are constructed to contain the op data on the appropriate range and to vanish everywhere else.
// The op data is assumed to have already been stored at the correct location in the convetional wires so the data
// can simply be copied over directly.
const size_t op_wire_offset = Flavor::has_zero_row ? 1 : 0;
for (size_t poly_idx = 0; poly_idx < Flavor::NUM_WIRES; ++poly_idx) {
for (size_t i = 0; i < num_ecc_op_gates; ++i) {
size_t idx = i + op_wire_offset;
op_wire_polynomials[poly_idx][idx] = wire_polynomials[poly_idx][idx];
}
}

proving_key->ecc_op_wire_1 = op_wire_polynomials[0];
proving_key->ecc_op_wire_2 = op_wire_polynomials[1];
proving_key->ecc_op_wire_3 = op_wire_polynomials[2];
proving_key->ecc_op_wire_4 = op_wire_polynomials[3];
}

template <UltraFlavor Flavor>
UltraProver_<Flavor> UltraComposer_<Flavor>::create_prover(CircuitBuilder& circuit_constructor)
{
Expand Down Expand Up @@ -258,6 +282,10 @@ std::shared_ptr<typename Flavor::ProvingKey> UltraComposer_<Flavor>::compute_pro

proving_key->contains_recursive_proof = contains_recursive_proof;

if constexpr (IsGoblinFlavor<Flavor>) {
proving_key->num_ecc_op_gates = num_ecc_op_gates;
}

return proving_key;
}

Expand Down Expand Up @@ -308,6 +336,12 @@ std::shared_ptr<typename Flavor::VerificationKey> UltraComposer_<Flavor>::comput
verification_key->lagrange_first = commitment_key->commit(proving_key->lagrange_first);
verification_key->lagrange_last = commitment_key->commit(proving_key->lagrange_last);

// TODO(luke): Similar to the lagrange_first/last polynomials, we dont really need to commit to this polynomial due
// to its simple structure. Handling it in the same way as the lagrange polys for now for simplicity.
if constexpr (IsGoblinFlavor<Flavor>) {
verification_key->lagrange_ecc_op = commitment_key->commit(proving_key->lagrange_ecc_op);
}

// // See `add_recusrive_proof()` for how this recursive data is assigned.
// verification_key->recursive_proof_public_input_indices =
// std::vector<uint32_t>(recursive_proof_public_input_indices.begin(),
Expand All @@ -319,5 +353,6 @@ std::shared_ptr<typename Flavor::VerificationKey> UltraComposer_<Flavor>::comput
}
template class UltraComposer_<honk::flavor::Ultra>;
template class UltraComposer_<honk::flavor::UltraGrumpkin>;
template class UltraComposer_<honk::flavor::GoblinUltra>;

} // namespace proof_system::honk
8 changes: 7 additions & 1 deletion cpp/src/barretenberg/honk/composer/ultra_composer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "barretenberg/honk/proof_system/ultra_prover.hpp"
#include "barretenberg/honk/proof_system/ultra_verifier.hpp"
#include "barretenberg/proof_system/composer/composer_lib.hpp"
#include "barretenberg/proof_system/flavor/flavor.hpp"
#include "barretenberg/srs/factories/file_crs_factory.hpp"

#include <cstddef>
Expand All @@ -22,7 +23,7 @@ template <UltraFlavor Flavor> class UltraComposer_ {
using PCSVerificationKey = typename PCSParams::VerificationKey;

// offset due to placing zero wires at the start of execution trace
static constexpr size_t zero_row_offset = Flavor::has_zero_row ? 1 : 0;
static constexpr size_t num_zero_rows = Flavor::has_zero_row ? 1 : 0;

static constexpr std::string_view NAME_STRING = "UltraHonk";
static constexpr size_t NUM_WIRES = CircuitBuilder::NUM_WIRES;
Expand All @@ -43,6 +44,7 @@ template <UltraFlavor Flavor> class UltraComposer_ {
size_t lookups_size = 0; // total number of lookup gates
size_t tables_size = 0; // total number of table entries
size_t num_public_inputs = 0;
size_t num_ecc_op_gates = 0;

UltraComposer_()
: crs_factory_(barretenberg::srs::get_crs_factory()){};
Expand All @@ -69,6 +71,8 @@ template <UltraFlavor Flavor> class UltraComposer_ {

void compute_witness(CircuitBuilder& circuit_constructor);

void construct_ecc_op_wire_polynomials(auto&);

UltraProver_<Flavor> create_prover(CircuitBuilder& circuit_constructor);
UltraVerifier_<Flavor> create_verifier(const CircuitBuilder& circuit_constructor);

Expand All @@ -81,7 +85,9 @@ template <UltraFlavor Flavor> class UltraComposer_ {
};
extern template class UltraComposer_<honk::flavor::Ultra>;
extern template class UltraComposer_<honk::flavor::UltraGrumpkin>;
extern template class UltraComposer_<honk::flavor::GoblinUltra>;
// TODO(#532): this pattern is weird; is this not instantiating the templates?
using UltraComposer = UltraComposer_<honk::flavor::Ultra>;
using UltraGrumpkinComposer = UltraComposer_<honk::flavor::UltraGrumpkin>;
using GoblinUltraComposer = UltraComposer_<honk::flavor::GoblinUltra>;
} // namespace proof_system::honk
Loading

0 comments on commit 970bb07

Please sign in to comment.