Skip to content

Commit 970bb07

Browse files
ledwards2225ludamad
authored and
AztecBot
committed
feat: Goblin Honk Composer/Prover/Verifier (#1220)
# 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]>
1 parent 81e9257 commit 970bb07

17 files changed

+1158
-216
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#include <cstddef>
2+
#include <cstdint>
3+
#include <gtest/gtest.h>
4+
5+
#include "barretenberg/common/log.hpp"
6+
#include "barretenberg/honk/composer/ultra_composer.hpp"
7+
#include "barretenberg/honk/proof_system/ultra_prover.hpp"
8+
#include "barretenberg/proof_system/circuit_builder/ultra_circuit_builder.hpp"
9+
10+
using namespace proof_system::honk;
11+
12+
namespace test_ultra_honk_composer {
13+
14+
namespace {
15+
auto& engine = numeric::random::get_debug_engine();
16+
}
17+
18+
class GoblinUltraHonkComposerTests : public ::testing::Test {
19+
protected:
20+
static void SetUpTestSuite() { barretenberg::srs::init_crs_factory("../srs_db/ignition"); }
21+
};
22+
23+
/**
24+
* @brief Test proof construction/verification for a circuit with ECC op gates, public inputs, and basic arithmetic
25+
* gates
26+
*
27+
*/
28+
TEST_F(GoblinUltraHonkComposerTests, SimpleCircuit)
29+
{
30+
auto builder = UltraCircuitBuilder();
31+
32+
// Define an arbitrary number of operations/gates
33+
size_t num_ecc_ops = 3;
34+
size_t num_conventional_gates = 10;
35+
36+
// Add some ecc op gates
37+
for (size_t i = 0; i < num_ecc_ops; ++i) {
38+
auto point = g1::affine_one * fr::random_element();
39+
auto scalar = fr::random_element();
40+
builder.queue_ecc_mul_accum(point, scalar);
41+
}
42+
43+
// Add some conventional gates that utlize public inputs
44+
for (size_t i = 0; i < num_conventional_gates; ++i) {
45+
fr a = fr::random_element();
46+
fr b = fr::random_element();
47+
fr c = fr::random_element();
48+
fr d = a + b + c;
49+
uint32_t a_idx = builder.add_public_variable(a);
50+
uint32_t b_idx = builder.add_variable(b);
51+
uint32_t c_idx = builder.add_variable(c);
52+
uint32_t d_idx = builder.add_variable(d);
53+
54+
builder.create_big_add_gate({ a_idx, b_idx, c_idx, d_idx, fr(1), fr(1), fr(1), fr(-1), fr(0) });
55+
}
56+
57+
auto composer = GoblinUltraComposer();
58+
auto prover = composer.create_prover(builder);
59+
auto verifier = composer.create_verifier(builder);
60+
auto proof = prover.construct_proof();
61+
bool verified = verifier.verify_proof(proof);
62+
EXPECT_EQ(verified, true);
63+
}
64+
65+
} // namespace test_ultra_honk_composer

cpp/src/barretenberg/honk/composer/ultra_composer.cpp

+71-36
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,16 @@ void UltraComposer_<Flavor>::compute_circuit_size_parameters(CircuitBuilder& cir
2121
lookups_size += table.lookup_gates.size();
2222
}
2323

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

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

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

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

51-
// At this point, the wires have been populated with as many values as rows in the execution trace. We need to pad
52-
// with zeros up to the full length, i.e. total_num_gates = already populated rows of execution trace + tables_size.
53-
for (size_t i = 0; i < tables_size; ++i) {
54-
circuit_constructor.w_l.emplace_back(circuit_constructor.zero_idx);
55-
circuit_constructor.w_r.emplace_back(circuit_constructor.zero_idx);
56-
circuit_constructor.w_o.emplace_back(circuit_constructor.zero_idx);
57-
circuit_constructor.w_4.emplace_back(circuit_constructor.zero_idx);
58-
}
59-
53+
// Construct the conventional wire polynomials
6054
auto wire_polynomials = construct_wire_polynomials_base<Flavor>(circuit_constructor, dyadic_circuit_size);
6155

6256
proving_key->w_l = wire_polynomials[0];
6357
proving_key->w_r = wire_polynomials[1];
6458
proving_key->w_o = wire_polynomials[2];
6559
proving_key->w_4 = wire_polynomials[3];
6660

61+
// If Goblin, construct the ECC op queue wire polynomials
62+
if constexpr (IsGoblinFlavor<Flavor>) {
63+
construct_ecc_op_wire_polynomials(wire_polynomials);
64+
}
65+
66+
// Construct the sorted concatenated list polynomials for the lookup argument
6767
polynomial s_1(dyadic_circuit_size);
6868
polynomial s_2(dyadic_circuit_size);
6969
polynomial s_3(dyadic_circuit_size);
7070
polynomial s_4(dyadic_circuit_size);
71-
// TODO(luke): The +1 size for z_lookup is not necessary and can lead to confusion. Resolve.
72-
polynomial z_lookup(dyadic_circuit_size + 1); // Only instantiated in this function; nothing assigned.
73-
74-
// TODO(kesha): Look at this once we figure out how we do ZK (previously we had roots cut out, so just added
75-
// randomness)
76-
// The size of empty space in sorted polynomials
77-
size_t count = dyadic_circuit_size - tables_size - lookups_size;
78-
ASSERT(count > 0); // We need at least 1 row of zeroes for the permutation argument
79-
for (size_t i = 0; i < count; ++i) {
80-
s_1[i] = 0;
81-
s_2[i] = 0;
82-
s_3[i] = 0;
83-
s_4[i] = 0;
84-
}
71+
72+
// The sorted list polynomials have (tables_size + lookups_size) populated entries. We define the index below so
73+
// that these entries are written into the last indices of the polynomials. The values on the first
74+
// dyadic_circuit_size - (tables_size + lookups_size) indices are automatically initialized to zero via the
75+
// polynomial constructor.
76+
size_t s_index = dyadic_circuit_size - tables_size - lookups_size;
77+
ASSERT(s_index > 0); // We need at least 1 row of zeroes for the permutation argument
8578

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

121114
for (const auto& entry : lookup_gates) {
122115
const auto components = entry.to_sorted_list_components(table.use_twin_keys);
123-
s_1[count] = components[0];
124-
s_2[count] = components[1];
125-
s_3[count] = components[2];
126-
s_4[count] = table_index;
127-
++count;
116+
s_1[s_index] = components[0];
117+
s_2[s_index] = components[1];
118+
s_3[s_index] = components[2];
119+
s_4[s_index] = table_index;
120+
++s_index;
128121
}
129122
}
130123

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

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

152+
/**
153+
* @brief Construct Goblin style ECC op wire polynomials
154+
* @details The Ecc op wire values are assumed to have already been stored in the corresponding block of the
155+
* conventional wire polynomials. The values for the ecc op wire polynomials are set based on those values.
156+
*
157+
* @tparam Flavor
158+
* @param wire_polynomials
159+
*/
160+
template <UltraFlavor Flavor> void UltraComposer_<Flavor>::construct_ecc_op_wire_polynomials(auto& wire_polynomials)
161+
{
162+
std::array<polynomial, Flavor::NUM_WIRES> op_wire_polynomials;
163+
for (auto& poly : op_wire_polynomials) {
164+
poly = polynomial(dyadic_circuit_size);
165+
}
166+
167+
// The ECC op wires are constructed to contain the op data on the appropriate range and to vanish everywhere else.
168+
// The op data is assumed to have already been stored at the correct location in the convetional wires so the data
169+
// can simply be copied over directly.
170+
const size_t op_wire_offset = Flavor::has_zero_row ? 1 : 0;
171+
for (size_t poly_idx = 0; poly_idx < Flavor::NUM_WIRES; ++poly_idx) {
172+
for (size_t i = 0; i < num_ecc_op_gates; ++i) {
173+
size_t idx = i + op_wire_offset;
174+
op_wire_polynomials[poly_idx][idx] = wire_polynomials[poly_idx][idx];
175+
}
176+
}
177+
178+
proving_key->ecc_op_wire_1 = op_wire_polynomials[0];
179+
proving_key->ecc_op_wire_2 = op_wire_polynomials[1];
180+
proving_key->ecc_op_wire_3 = op_wire_polynomials[2];
181+
proving_key->ecc_op_wire_4 = op_wire_polynomials[3];
182+
}
183+
160184
template <UltraFlavor Flavor>
161185
UltraProver_<Flavor> UltraComposer_<Flavor>::create_prover(CircuitBuilder& circuit_constructor)
162186
{
@@ -258,6 +282,10 @@ std::shared_ptr<typename Flavor::ProvingKey> UltraComposer_<Flavor>::compute_pro
258282

259283
proving_key->contains_recursive_proof = contains_recursive_proof;
260284

285+
if constexpr (IsGoblinFlavor<Flavor>) {
286+
proving_key->num_ecc_op_gates = num_ecc_op_gates;
287+
}
288+
261289
return proving_key;
262290
}
263291

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

339+
// TODO(luke): Similar to the lagrange_first/last polynomials, we dont really need to commit to this polynomial due
340+
// to its simple structure. Handling it in the same way as the lagrange polys for now for simplicity.
341+
if constexpr (IsGoblinFlavor<Flavor>) {
342+
verification_key->lagrange_ecc_op = commitment_key->commit(proving_key->lagrange_ecc_op);
343+
}
344+
311345
// // See `add_recusrive_proof()` for how this recursive data is assigned.
312346
// verification_key->recursive_proof_public_input_indices =
313347
// std::vector<uint32_t>(recursive_proof_public_input_indices.begin(),
@@ -319,5 +353,6 @@ std::shared_ptr<typename Flavor::VerificationKey> UltraComposer_<Flavor>::comput
319353
}
320354
template class UltraComposer_<honk::flavor::Ultra>;
321355
template class UltraComposer_<honk::flavor::UltraGrumpkin>;
356+
template class UltraComposer_<honk::flavor::GoblinUltra>;
322357

323358
} // namespace proof_system::honk

cpp/src/barretenberg/honk/composer/ultra_composer.hpp

+7-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "barretenberg/honk/proof_system/ultra_prover.hpp"
44
#include "barretenberg/honk/proof_system/ultra_verifier.hpp"
55
#include "barretenberg/proof_system/composer/composer_lib.hpp"
6+
#include "barretenberg/proof_system/flavor/flavor.hpp"
67
#include "barretenberg/srs/factories/file_crs_factory.hpp"
78

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

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

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

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

7072
void compute_witness(CircuitBuilder& circuit_constructor);
7173

74+
void construct_ecc_op_wire_polynomials(auto&);
75+
7276
UltraProver_<Flavor> create_prover(CircuitBuilder& circuit_constructor);
7377
UltraVerifier_<Flavor> create_verifier(const CircuitBuilder& circuit_constructor);
7478

@@ -81,7 +85,9 @@ template <UltraFlavor Flavor> class UltraComposer_ {
8185
};
8286
extern template class UltraComposer_<honk::flavor::Ultra>;
8387
extern template class UltraComposer_<honk::flavor::UltraGrumpkin>;
88+
extern template class UltraComposer_<honk::flavor::GoblinUltra>;
8489
// TODO(#532): this pattern is weird; is this not instantiating the templates?
8590
using UltraComposer = UltraComposer_<honk::flavor::Ultra>;
8691
using UltraGrumpkinComposer = UltraComposer_<honk::flavor::UltraGrumpkin>;
92+
using GoblinUltraComposer = UltraComposer_<honk::flavor::GoblinUltra>;
8793
} // namespace proof_system::honk

0 commit comments

Comments
 (0)