Skip to content

Commit 92f72e4

Browse files
vezenovmkevaundray
andauthored
chore(dsl): Abstract nested aggregation object from ACIR (AztecProtocol#3765)
The nested aggregation object in a RecursionConstraint is currently not used at the Noir level at all. We want to enable having the user specify whether the proof they want to verify is itself a recursive proof, but we also do not want to have an explicit field on the opcode determining this as this would be a barretenberg leakage into the ACVM recursion opcode. We instead move to having the `proof` field in the recursion constraint adhere to a barretenberg specific structure, where the expected proof should be stripped of its public inputs, except in the case where we have a nested proof. When setting up the barretenberg circuit from ACIR we can then determine how the `nested_aggregation_object` constant indices should be set from the size of the proof object. Until the recursive verifier can move to an implementation where the nested aggregation object does not have to be a circuit constant we need the user to specify whether they want to aggregation over a nested proof. If we move to not requiring these circuit constants we can have the proof inputs to the recursive aggregation builtin be the same for both use cases. # Checklist: Remove the checklist to signal you've completed it. Enable auto-merge if the PR is ready to merge. - [ ] If the pull request requires a cryptography review (e.g. cryptographic algorithm implementations) I have added the 'crypto' tag. - [ ] I have reviewed my diff in github, line by line and removed unexpected formatting changes, testing logs, or commented-out code. - [ ] 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 relevant issues (if any exist). --------- Co-authored-by: kevaundray <kevtheappdev@gmail.com>
1 parent 5545ee6 commit 92f72e4

File tree

4 files changed

+92
-34
lines changed

4 files changed

+92
-34
lines changed

barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_format.cpp

+50-16
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "barretenberg/dsl/acir_format/pedersen.hpp"
44
#include "barretenberg/dsl/acir_format/recursion_constraint.hpp"
55
#include "barretenberg/proof_system/circuit_builder/ultra_circuit_builder.hpp"
6+
#include <cstddef>
67

78
namespace acir_format {
89

@@ -158,24 +159,56 @@ void build_constraints(Builder& builder, acir_format const& constraint_system, b
158159
// These are set and modified whenever we encounter a recursion opcode
159160
//
160161
// These should not be set by the caller
161-
// TODO: Check if this is always the case. ie I won't receive a proof that will set the first
162-
// TODO input_aggregation_object to be non-zero.
163-
// TODO: if not, we can add input_aggregation_object to the proof too for all recursive proofs
164-
// TODO: This might be the case for proof trees where the proofs are created on different machines
162+
// TODO(maxim): Check if this is always the case. ie I won't receive a proof that will set the first
163+
// TODO(maxim): input_aggregation_object to be non-zero.
164+
// TODO(maxim): if not, we can add input_aggregation_object to the proof too for all recursive proofs
165+
// TODO(maxim): This might be the case for proof trees where the proofs are created on different machines
165166
std::array<uint32_t, RecursionConstraint::AGGREGATION_OBJECT_SIZE> current_input_aggregation_object = {
166167
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
167168
};
168169
std::array<uint32_t, RecursionConstraint::AGGREGATION_OBJECT_SIZE> current_output_aggregation_object = {
169170
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
170171
};
171172

173+
// Get the size of proof with no public inputs prepended to it
174+
// This is used while processing recursion constraints to determine whether
175+
// the proof we are verifying contains a recursive proof itself
176+
auto proof_size_no_pub_inputs = recursion_proof_size_without_public_inputs();
177+
172178
// Add recursion constraints
173-
for (size_t i = 0; i < constraint_system.recursion_constraints.size(); ++i) {
174-
auto& constraint = constraint_system.recursion_constraints[i];
179+
for (auto constraint : constraint_system.recursion_constraints) {
180+
// A proof passed into the constraint should be stripped of its public inputs, except in the case where a
181+
// proof contains an aggregation object itself. We refer to this as the `nested_aggregation_object`. The
182+
// verifier circuit requires that the indices to a nested proof aggregation state are a circuit constant.
183+
// The user tells us they how they want these constants set by keeping the nested aggregation object
184+
// attached to the proof as public inputs. As this is the only object that can prepended to the proof if the
185+
// proof is above the expected size (with public inputs stripped)
186+
std::array<uint32_t, RecursionConstraint::AGGREGATION_OBJECT_SIZE> nested_aggregation_object = {};
187+
// If the proof has public inputs attached to it, we should handle setting the nested aggregation object
188+
if (constraint.proof.size() > proof_size_no_pub_inputs) {
189+
// The public inputs attached to a proof should match the aggregation object in size
190+
ASSERT(constraint.proof.size() - proof_size_no_pub_inputs ==
191+
RecursionConstraint::AGGREGATION_OBJECT_SIZE);
192+
for (size_t i = 0; i < RecursionConstraint::AGGREGATION_OBJECT_SIZE; ++i) {
193+
// Set the nested aggregation object indices to the current size of the public inputs
194+
// This way we know that the nested aggregation object indices will always be the last
195+
// indices of the public inputs
196+
nested_aggregation_object[i] = static_cast<uint32_t>(constraint.public_inputs.size());
197+
// Attach the nested aggregation object to the end of the public inputs to fill in
198+
// the slot where the nested aggregation object index will point into
199+
constraint.public_inputs.emplace_back(constraint.proof[i]);
200+
}
201+
// Remove the aggregation object so that they can be handled as normal public inputs
202+
// in they way taht the recursion constraint expects
203+
constraint.proof.erase(constraint.proof.begin(),
204+
constraint.proof.begin() +
205+
static_cast<std::ptrdiff_t>(RecursionConstraint::AGGREGATION_OBJECT_SIZE));
206+
}
207+
175208
current_output_aggregation_object = create_recursion_constraints(builder,
176209
constraint,
177210
current_input_aggregation_object,
178-
constraint.nested_aggregation_object,
211+
nested_aggregation_object,
179212
has_valid_witness_assignments);
180213
current_input_aggregation_object = current_output_aggregation_object;
181214
}
@@ -241,25 +274,26 @@ void create_circuit_with_witness(Builder& builder, acir_format const& constraint
241274

242275
/**
243276
* @brief Apply an offset to the indices stored in the wires
244-
* @details This method is needed due to the following: Noir constructs "wires" as indices into a "witness" vector. This
245-
* is analogous to the wires and variables vectors in bberg builders. Were it not for the addition of constant variables
246-
* in the constructors of a builder (e.g. zero), we would simply have noir.wires = builder.wires and noir.witness =
247-
* builder.variables. To account for k-many constant variables in the first entries of the variables array, we have
248-
* something like variables = variables.append(noir.witness). Accordingly, the indices in noir.wires have to be
249-
* incremented to account for the offset at which noir.wires was placed into variables.
277+
* @details This method is needed due to the following: Noir constructs "wires" as indices into a "witness" vector.
278+
* This is analogous to the wires and variables vectors in bberg builders. Were it not for the addition of constant
279+
* variables in the constructors of a builder (e.g. zero), we would simply have noir.wires = builder.wires and
280+
* noir.witness = builder.variables. To account for k-many constant variables in the first entries of the variables
281+
* array, we have something like variables = variables.append(noir.witness). Accordingly, the indices in noir.wires
282+
* have to be incremented to account for the offset at which noir.wires was placed into variables.
250283
*
251284
* @tparam Builder
252285
* @param builder
253286
*/
254287
template <typename Builder> void apply_wire_index_offset(Builder& builder)
255288
{
256-
// For now, noir has a hard coded witness index offset = 1. Once this is removed, this pre-applied offset goes away
289+
// For now, noir has a hard coded witness index offset = 1. Once this is removed, this pre-applied offset goes
290+
// away
257291
const uint32_t pre_applied_noir_offset = 1;
258292
auto offset = static_cast<uint32_t>(builder.num_vars_added_in_constructor - pre_applied_noir_offset);
259293
info("Applying offset = ", offset);
260294

261-
// Apply the offset to the indices stored the wires that were generated from acir. (Do not apply the offset to those
262-
// values that were added in the builder constructor).
295+
// Apply the offset to the indices stored the wires that were generated from acir. (Do not apply the offset to
296+
// those values that were added in the builder constructor).
263297
size_t start_index = builder.num_vars_added_in_constructor;
264298
for (auto& wire : builder.wires) {
265299
for (size_t idx = start_index; idx < wire.size(); ++idx) {

barretenberg/cpp/src/barretenberg/dsl/acir_format/recursion_constraint.cpp

+8-5
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,6 @@ std::array<uint32_t, RecursionConstraint::AGGREGATION_OBJECT_SIZE> create_recurs
3333
Builder& builder,
3434
const RecursionConstraint& input,
3535
std::array<uint32_t, RecursionConstraint::AGGREGATION_OBJECT_SIZE> input_aggregation_object,
36-
// TODO: does this need to be a part of the recursion opcode?
37-
// TODO: or can we figure it out from the vk?
38-
// TODO: either way we could probably have the user explicitly provide it
39-
// TODO: in Noir.
40-
// Note: this is not being used in Noir at the moment
4136
std::array<uint32_t, RecursionConstraint::AGGREGATION_OBJECT_SIZE> nested_aggregation_object,
4237
bool has_valid_witness_assignments)
4338
{
@@ -141,6 +136,7 @@ std::array<uint32_t, RecursionConstraint::AGGREGATION_OBJECT_SIZE> create_recurs
141136
std::shared_ptr<verification_key_ct> vkey = verification_key_ct::from_field_elements(
142137
&builder, key_fields, inner_proof_contains_recursive_proof, nested_aggregation_indices);
143138
vkey->program_width = noir_recursive_settings::program_width;
139+
144140
Transcript_ct transcript(&builder, manifest, proof_fields, input.public_inputs.size());
145141
aggregation_state_ct result = proof_system::plonk::stdlib::recursion::verify_proof_<bn254, noir_recursive_settings>(
146142
&builder, vkey, transcript, previous_aggregation);
@@ -358,6 +354,13 @@ std::vector<barretenberg::fr> export_dummy_transcript_in_recursion_format(const
358354
return fields;
359355
}
360356

357+
size_t recursion_proof_size_without_public_inputs()
358+
{
359+
const auto manifest = Composer::create_manifest(0);
360+
auto dummy_transcript = export_dummy_transcript_in_recursion_format(manifest, false);
361+
return dummy_transcript.size();
362+
}
363+
361364
G1AsFields export_g1_affine_element_as_fields(const barretenberg::g1::affine_element& group_element)
362365
{
363366
const uint256_t x = group_element.x;

barretenberg/cpp/src/barretenberg/dsl/acir_format/recursion_constraint.hpp

+7-5
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,14 @@ struct RecursionConstraint {
5353
std::vector<uint32_t> proof;
5454
std::vector<uint32_t> public_inputs;
5555
uint32_t key_hash;
56-
// TODO:This is now unused, but we keep it here for backwards compatibility
56+
// TODO(maxim):This is now unused, but we keep it here for backwards compatibility
5757
std::array<uint32_t, AGGREGATION_OBJECT_SIZE> input_aggregation_object;
58-
// TODO: This is now unused, but we keep it here for backwards compatibility
58+
// TODO(maxim): This is now unused, but we keep it here for backwards compatibility
5959
std::array<uint32_t, AGGREGATION_OBJECT_SIZE> output_aggregation_object;
60-
// TODO: This is currently not being used on the Noir level at all
61-
// TODO: we don't have a way to specify that the proof we are creating contains a
62-
// TODO: aggregation object (ie it is also verifying a proof)
60+
// TODO(maxim): This is currently not being used on the Noir level at all,
61+
// TODO(maxim): but we keep it here for backwards compatibility
62+
// TODO(maxim): The object is now currently contained by the `proof` field
63+
// TODO(maxim): and is handled when serializing ACIR to a barretenberg circuit
6364
std::array<uint32_t, AGGREGATION_OBJECT_SIZE> nested_aggregation_object;
6465

6566
friend bool operator==(RecursionConstraint const& lhs, RecursionConstraint const& rhs) = default;
@@ -79,6 +80,7 @@ std::vector<barretenberg::fr> export_dummy_key_in_recursion_format(const Polynom
7980
std::vector<barretenberg::fr> export_transcript_in_recursion_format(const transcript::StandardTranscript& transcript);
8081
std::vector<barretenberg::fr> export_dummy_transcript_in_recursion_format(const transcript::Manifest& manifest,
8182
const bool contains_recursive_proof);
83+
size_t recursion_proof_size_without_public_inputs();
8284

8385
// In order to interact with a recursive aggregation state inside of a circuit, we need to represent its internal G1
8486
// elements as field elements. This happens in multiple locations when creating a recursion constraint. The struct and

barretenberg/cpp/src/barretenberg/dsl/acir_format/recursion_constraint.test.cpp

+27-8
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ Builder create_outer_circuit(std::vector<Builder>& inner_circuits)
139139
auto inner_verifier = inner_composer.create_verifier(inner_circuit);
140140

141141
const bool has_nested_proof = inner_verifier.key->contains_recursive_proof;
142-
const size_t num_inner_public_inputs = inner_circuit.get_public_inputs().size();
143142

143+
const size_t num_inner_public_inputs = inner_circuit.get_public_inputs().size();
144144
transcript::StandardTranscript transcript(inner_proof.proof_data,
145145
Composer::create_manifest(num_inner_public_inputs),
146146
transcript::HashType::PedersenBlake3s,
@@ -149,11 +149,16 @@ Builder create_outer_circuit(std::vector<Builder>& inner_circuits)
149149
std::vector<barretenberg::fr> proof_witnesses = export_transcript_in_recursion_format(transcript);
150150
// - Save the public inputs so that we can set their values.
151151
// - Then truncate them from the proof because the ACIR API expects proofs without public inputs
152-
153152
std::vector<barretenberg::fr> inner_public_input_values(
154153
proof_witnesses.begin(), proof_witnesses.begin() + static_cast<std::ptrdiff_t>(num_inner_public_inputs));
155-
proof_witnesses.erase(proof_witnesses.begin(),
156-
proof_witnesses.begin() + static_cast<std::ptrdiff_t>(num_inner_public_inputs));
154+
155+
// We want to make sure that we do not remove the nested aggregation object in the case of the proof we want to
156+
// recursively verify contains a recursive proof itself. We are safe to keep all the inner public inputs
157+
// as in these tests the outer circuits do not have public inputs themselves
158+
if (!has_nested_proof) {
159+
proof_witnesses.erase(proof_witnesses.begin(),
160+
proof_witnesses.begin() + static_cast<std::ptrdiff_t>(num_inner_public_inputs));
161+
}
157162

158163
const std::vector<barretenberg::fr> key_witnesses = export_key_in_recursion_format(inner_verifier.key);
159164

@@ -187,8 +192,13 @@ Builder create_outer_circuit(std::vector<Builder>& inner_circuits)
187192
for (size_t i = 0; i < key_size; ++i) {
188193
key_indices.emplace_back(static_cast<uint32_t>(i + key_indices_start_idx));
189194
}
190-
for (size_t i = 0; i < num_inner_public_inputs; ++i) {
191-
inner_public_inputs.push_back(static_cast<uint32_t>(i + public_input_start_idx));
195+
// In the case of a nested proof we keep the nested aggregation object attached to the proof,
196+
// thus we do not explicitly have to keep the public inputs while setting up the initial recursion constraint.
197+
// They will later be attached as public inputs when creating the circuit.
198+
if (!has_nested_proof) {
199+
for (size_t i = 0; i < num_inner_public_inputs; ++i) {
200+
inner_public_inputs.push_back(static_cast<uint32_t>(i + public_input_start_idx));
201+
}
192202
}
193203

194204
RecursionConstraint recursion_constraint{
@@ -201,21 +211,30 @@ Builder create_outer_circuit(std::vector<Builder>& inner_circuits)
201211
.nested_aggregation_object = nested_aggregation_object,
202212
};
203213
recursion_constraints.push_back(recursion_constraint);
214+
204215
for (size_t i = 0; i < proof_indices_start_idx - witness_offset; ++i) {
205216
witness.emplace_back(0);
206217
}
207218
for (const auto& wit : proof_witnesses) {
208219
witness.emplace_back(wit);
209220
}
221+
210222
for (const auto& wit : key_witnesses) {
211223
witness.emplace_back(wit);
212224
}
225+
213226
// Set the values for the inner public inputs
214227
// Note: this is confusing, but we minus one here due to the fact that the
215228
// witness values have not taken into account that zero is taken up by the zero_idx
216-
for (size_t i = 0; i < num_inner_public_inputs; ++i) {
217-
witness[inner_public_inputs[i] - 1] = inner_public_input_values[i];
229+
//
230+
// We once again have to check whether we have a nested proof, because if we do have one
231+
// then we could get a segmentation fault as `inner_public_inputs` was never filled with values.
232+
if (!has_nested_proof) {
233+
for (size_t i = 0; i < num_inner_public_inputs; ++i) {
234+
witness[inner_public_inputs[i] - 1] = inner_public_input_values[i];
235+
}
218236
}
237+
219238
witness_offset = key_indices_start_idx + key_witnesses.size();
220239
circuit_idx++;
221240
}

0 commit comments

Comments
 (0)