Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
honk_recursive_verifier.test.cpp
Go to the documentation of this file.
12
14
15// Test parameters: <RecursiveFlavor, IO>
16// IO determines the public inputs structure (DefaultIO or RollupIO) for both inner and outer circuits
17template <typename RecursiveFlavor_, typename IO_> struct RecursiveVerifierTestParams {
18 using RecursiveFlavor = RecursiveFlavor_;
19 using IO = IO_;
20};
21
22// Run the recursive verifier tests with conventional Ultra builder and Goblin builder
23// Note: UltraRecursiveFlavor_<UltraCircuitBuilder> + RollupIO covers the rollup case
24using TestConfigs = testing::Types<
34
43template <typename Params> class RecursiveVerifierTest : public testing::Test {
44
45 using RecursiveFlavor = typename Params::RecursiveFlavor;
46 using IO = typename Params::IO;
47
48 // Define types for the inner circuit, i.e. the circuit whose proof will be recursively verified
49 using InnerFlavor = typename RecursiveFlavor::NativeFlavor;
51 using InnerBuilder = typename InnerFlavor::CircuitBuilder;
53 using InnerCommitment = InnerFlavor::Commitment;
54 using InnerFF = InnerFlavor::FF;
56
57 // IO types: InnerIO uses InnerBuilder, OuterIO uses OuterBuilder
61
62 // Defines types for the outer circuit, i.e. the circuit of the recursive verifier
63 using OuterBuilder = typename RecursiveFlavor::CircuitBuilder;
69 using OuterIO = IO;
70
71 // RecursiveVerifier uses IO that matches the test's IO type
74
84 static InnerBuilder create_inner_circuit(size_t log_num_gates = 10)
85 {
87
88 // Create 2^log_n many add gates based on input log num gates
89 const size_t num_gates = (1 << log_num_gates);
90 for (size_t i = 0; i < num_gates; ++i) {
92 uint32_t a_idx = builder.add_variable(a);
93
96 fr d = a + b + c;
97 uint32_t b_idx = builder.add_variable(b);
98 uint32_t c_idx = builder.add_variable(c);
99 uint32_t d_idx = builder.add_variable(d);
100
101 builder.create_big_add_gate({ a_idx, b_idx, c_idx, d_idx, fr(1), fr(1), fr(1), fr(-1), fr(0) });
102 }
103
104 InnerIO::add_default(builder);
105
106 return builder;
107 }
108
109 public:
111
116 static void test_inner_circuit()
117 {
118 auto inner_circuit = create_inner_circuit();
119
120 bool result = CircuitChecker::check(inner_circuit);
121
122 EXPECT_EQ(result, true);
123 }
124
131 {
132 // Create an arbitrary inner circuit
133 auto inner_circuit = create_inner_circuit();
134 OuterBuilder outer_circuit;
135
136 // Compute native verification key
137 auto prover_instance = std::make_shared<InnerProverInstance>(inner_circuit);
138 auto honk_vk = std::make_shared<typename InnerFlavor::VerificationKey>(prover_instance->get_precomputed());
139 auto stdlib_vk_and_hash = std::make_shared<typename RecursiveFlavor::VKAndHash>(outer_circuit, honk_vk);
140 // Instantiate the recursive verifier using the native verification key
141 RecursiveVerifier verifier{ stdlib_vk_and_hash };
142
143 // Spot check some values in the recursive VK to ensure it was constructed correctly
144 EXPECT_EQ(
145 static_cast<uint64_t>(verifier.get_verifier_instance()->vk_and_hash->vk->log_circuit_size.get_value()),
146 honk_vk->log_circuit_size);
147 EXPECT_EQ(
148 static_cast<uint64_t>(verifier.get_verifier_instance()->vk_and_hash->vk->num_public_inputs.get_value()),
149 honk_vk->num_public_inputs);
150 for (auto [vk_poly, native_vk_poly] :
151 zip_view(verifier.get_verifier_instance()->vk_and_hash->vk->get_all(), honk_vk->get_all())) {
152 EXPECT_EQ(vk_poly.get_value(), native_vk_poly);
153 }
154 }
155
163 {
164 // Retrieves the trace blocks (each consisting of a specific gate) from the recursive verifier circuit
165 auto get_blocks = [](size_t inner_size) -> std::tuple<typename OuterBuilder::ExecutionTrace,
167 // Create an arbitrary inner circuit
168 auto inner_circuit = create_inner_circuit(inner_size);
169
170 // Generate a proof over the inner circuit
171 auto inner_prover_instance = std::make_shared<InnerProverInstance>(inner_circuit);
172 auto verification_key =
173 std::make_shared<typename InnerFlavor::VerificationKey>(inner_prover_instance->get_precomputed());
174 InnerProver inner_prover(inner_prover_instance, verification_key);
175 info("test circuit size: ", inner_prover_instance->dyadic_size());
176 auto inner_proof = inner_prover.construct_proof();
177
178 // Create a recursive verification circuit for the proof of the inner circuit
179 OuterBuilder outer_circuit;
180 auto stdlib_vk_and_hash =
181 std::make_shared<typename RecursiveFlavor::VKAndHash>(outer_circuit, verification_key);
182 RecursiveVerifier verifier{ stdlib_vk_and_hash };
183
184 // Convert native proof to stdlib and verify (verifier handles IPA splitting internally)
185 OuterStdlibProof stdlib_inner_proof(outer_circuit, inner_proof);
186 typename RecursiveVerifier::Output verifier_output = verifier.verify_proof(stdlib_inner_proof);
187
188 // IO of outer_circuit
190 inputs.pairing_inputs = verifier_output.points_accumulator;
191 if constexpr (IO::HasIPA) {
192 // Add ipa claim
193 inputs.ipa_claim = verifier_output.ipa_claim;
194
195 // Store ipa_proof
196 outer_circuit.ipa_proof = verifier_output.ipa_proof.get_value();
197 };
198 inputs.set_public();
199
200 auto outer_prover_instance = std::make_shared<OuterProverInstance>(outer_circuit);
201 auto outer_verification_key =
202 std::make_shared<typename OuterFlavor::VerificationKey>(outer_prover_instance->get_precomputed());
203
204 return { outer_circuit.blocks, outer_verification_key };
205 };
206
207 auto [blocks_10, verification_key_10] = get_blocks(10);
208 auto [blocks_14, verification_key_14] = get_blocks(14);
209
210 compare_ultra_blocks_and_verification_keys<OuterFlavor>({ blocks_10, blocks_14 },
211 { verification_key_10, verification_key_14 });
212 }
213
219 {
220 // Create an arbitrary inner circuit
221 auto inner_circuit = create_inner_circuit();
222
223 // Generate a proof over the inner circuit
224 auto prover_instance = std::make_shared<InnerProverInstance>(inner_circuit);
225 auto verification_key =
226 std::make_shared<typename InnerFlavor::VerificationKey>(prover_instance->get_precomputed());
227 InnerProver inner_prover(prover_instance, verification_key);
228 auto inner_proof = inner_prover.construct_proof();
229
230 // Create a recursive verification circuit for the proof of the inner circuit
231 OuterBuilder outer_circuit;
232 auto stdlib_vk_and_hash =
233 std::make_shared<typename RecursiveFlavor::VKAndHash>(outer_circuit, verification_key);
234 auto recursive_transcript = std::make_shared<typename RecursiveFlavor::Transcript>();
235 recursive_transcript->enable_manifest();
236 RecursiveVerifier verifier{ stdlib_vk_and_hash, recursive_transcript };
237
238 OuterStdlibProof stdlib_inner_proof(outer_circuit, inner_proof);
239 VerifierOutput output = verifier.verify_proof(stdlib_inner_proof);
240
241 // IO of outer_circuit
243 inputs.pairing_inputs = output.points_accumulator;
244 if constexpr (IO::HasIPA) {
245 // Add ipa claim
246 inputs.ipa_claim = output.ipa_claim;
247
248 // Store ipa_proof
249 outer_circuit.ipa_proof = output.ipa_proof.get_value();
250 };
251 inputs.set_public();
252
253 // Check for a failure flag in the recursive verifier circuit
254 EXPECT_EQ(outer_circuit.failed(), false) << outer_circuit.err();
255
256 // Check 1: Perform native verification then perform the pairing on the outputs of the recursive
257 // verifier and check that the result agrees.
258 auto vk_and_hash = std::make_shared<typename InnerFlavor::VKAndHash>(verification_key);
260 native_transcript->enable_manifest();
261 InnerVerifier native_verifier(vk_and_hash, native_transcript);
262 // inner_proof already contains combined honk + IPA for rollup flavors
263 bool native_result = native_verifier.verify_proof(inner_proof).result;
264
265 bool result = output.points_accumulator.check();
266 info("input pairing points result: ", result);
267 EXPECT_EQ(result, native_result);
268
269 // Check 2: Ensure that the underlying native and recursive verification algorithms agree by ensuring
270 // the manifests produced by each agree.
271 auto recursive_manifest = verifier.get_transcript()->get_manifest();
272 auto native_manifest = native_verifier.get_transcript()->get_manifest();
273 for (size_t i = 0; i < recursive_manifest.size(); ++i) {
274 EXPECT_EQ(recursive_manifest[i], native_manifest[i]);
275 }
276
277 // Check 3: Construct and verify a proof of the recursive verifier circuit
278 {
279 auto prover_instance = std::make_shared<OuterProverInstance>(outer_circuit);
280 auto verification_key =
281 std::make_shared<typename OuterFlavor::VerificationKey>(prover_instance->get_precomputed());
282 info("Recursive Verifier: num gates = ", outer_circuit.get_num_finalized_gates());
283 OuterProver prover(prover_instance, verification_key);
284 // construct_proof() already returns combined proof (honk + IPA) for rollup flavors
285 auto proof = prover.construct_proof();
286 auto outer_vk_and_hash = std::make_shared<typename OuterFlavor::VKAndHash>(verification_key);
287 OuterVerifier verifier(outer_vk_and_hash);
288 bool result = verifier.verify_proof(proof).result;
289 ASSERT_TRUE(result);
290 }
291 // Check the size of the recursive verifier
293 const auto expected_gate_count = std::get<0>(acir_format::HONK_RECURSION_CONSTANTS<RecursiveFlavor>());
294 ASSERT_EQ(outer_circuit.get_num_finalized_gates(), expected_gate_count)
295 << "MegaZKHonk Recursive verifier changed in Ultra gate count! Update this value if you "
296 "are sure this is expected.";
297 }
298 }
299
300 enum class TamperType {
301 MODIFY_SUMCHECK_UNIVARIATE, // Tests sumcheck round consistency constraint (circuit FAIL)
302 MODIFY_SUMCHECK_EVAL, // Tests final relation check constraint (circuit FAIL)
303 MODIFY_KZG_WITNESS, // Tests pairing check (circuit PASS, pairing FAIL)
304 MODIFY_LIBRA_EVAL, // Tests Libra consistency constraint (circuit FAIL, ZK only)
305 END
306 };
307
308 static void tamper_honk_proof(InnerProver& inner_prover,
309 typename InnerFlavor::Transcript::Proof& inner_proof,
311 {
312 using FF = InnerFF;
313 static constexpr size_t FIRST_WITNESS_INDEX = InnerFlavor::NUM_PRECOMPUTED_ENTITIES;
314
315 StructuredProof<InnerFlavor> structured_proof;
316 const auto num_public_inputs = inner_prover.num_public_inputs();
317 const size_t log_n = InnerFlavor::USE_PADDING ? InnerFlavor::VIRTUAL_LOG_N : inner_prover.log_dyadic_size();
318 structured_proof.deserialize(inner_prover.get_transcript()->test_get_proof_data(), num_public_inputs, log_n);
319
320 switch (type) {
322 FF delta = FF::random_element();
323 structured_proof.sumcheck_univariates[0].value_at(0) += delta;
324 structured_proof.sumcheck_univariates[0].value_at(1) -= delta;
325 break;
326 }
328 structured_proof.sumcheck_evaluations[FIRST_WITNESS_INDEX] = FF::random_element();
329 break;
331 structured_proof.kzg_w_comm = structured_proof.kzg_w_comm * FF::random_element();
332 break;
334 if constexpr (InnerFlavor::HasZK) {
335 structured_proof.libra_quotient_eval = FF::random_element();
336 }
337 break;
338 case TamperType::END:
339 break;
340 }
341
342 structured_proof.serialize(inner_prover.get_transcript()->test_get_proof_data(), log_n);
343 inner_prover.get_transcript()->test_set_proof_parsing_state(
345 inner_proof = inner_prover.export_proof();
346 }
347
349 {
350 for (size_t idx = 0; idx < static_cast<size_t>(TamperType::END); idx++) {
351 TamperType tamper_type = static_cast<TamperType>(idx);
352
353 if (tamper_type == TamperType::MODIFY_LIBRA_EVAL && !InnerFlavor::HasZK) {
354 continue;
355 }
356
357 // Create an arbitrary inner circuit
358 auto inner_circuit = create_inner_circuit();
359
360 // Generate a proof over the inner circuit
361 auto prover_instance = std::make_shared<InnerProverInstance>(inner_circuit);
362 auto inner_verification_key =
363 std::make_shared<typename InnerFlavor::VerificationKey>(prover_instance->get_precomputed());
364 InnerProver inner_prover(prover_instance, inner_verification_key);
365 auto inner_proof = inner_prover.construct_proof();
366
367 // Tamper with the proof to be verified
368 tamper_honk_proof(inner_prover, inner_proof, tamper_type);
369
370 // Create a recursive verification circuit for the tampered proof
371 OuterBuilder outer_circuit;
372 auto stdlib_vk_and_hash =
373 std::make_shared<typename RecursiveFlavor::VKAndHash>(outer_circuit, inner_verification_key);
374 RecursiveVerifier verifier{ stdlib_vk_and_hash };
375 OuterStdlibProof stdlib_inner_proof(outer_circuit, inner_proof);
376 VerifierOutput output = verifier.verify_proof(stdlib_inner_proof);
377
378 if (tamper_type == TamperType::MODIFY_KZG_WITNESS) {
379 // Expected to result in pairing failure but no circuit constraint violations
380 EXPECT_TRUE(CircuitChecker::check(outer_circuit));
381 EXPECT_FALSE(output.points_accumulator.check());
382 } else {
383 // All other tamper types should cause a circuit constraint violation
384 EXPECT_FALSE(CircuitChecker::check(outer_circuit));
385 }
386 }
387 }
388
399 {
400 // Create an arbitrary inner circuit
401 auto inner_circuit = create_inner_circuit();
402
403 // Generate a proof over the inner circuit
404 auto prover_instance = std::make_shared<InnerProverInstance>(inner_circuit);
405 auto verification_key =
406 std::make_shared<typename InnerFlavor::VerificationKey>(prover_instance->get_precomputed());
407 InnerProver inner_prover(prover_instance, verification_key);
408 auto inner_proof = inner_prover.construct_proof();
409
410 // Create a recursive verification circuit for the proof of the inner circuit
411 OuterBuilder outer_circuit;
412 auto stdlib_vk_and_hash =
413 std::make_shared<typename RecursiveFlavor::VKAndHash>(outer_circuit, verification_key);
414 RecursiveVerifier verifier{ stdlib_vk_and_hash };
415
416 // Fix witness for VK fields to ensure they're properly constrained
417 verifier.get_verifier_instance()->vk_and_hash->vk->num_public_inputs.fix_witness();
418 verifier.get_verifier_instance()->vk_and_hash->vk->pub_inputs_offset.fix_witness();
419 verifier.get_verifier_instance()->vk_and_hash->vk->log_circuit_size.fix_witness();
420
421 OuterStdlibProof stdlib_inner_proof(outer_circuit, inner_proof);
422 VerifierOutput output = verifier.verify_proof(stdlib_inner_proof);
423 auto pairing_points = output.points_accumulator;
424
425 // The pairing points are public outputs from the recursive verifier that will be verified externally via a
426 // pairing check. While they are computed within the circuit (via batch_mul for P0 and negation for P1), their
427 // output coordinates may not appear in multiple constraint gates. Calling fix_witness() adds explicit
428 // constraints on these values. Without these constraints, the StaticAnalyzer detects unconstrained variables
429 // (coordinate limbs) that appear in only one gate. This ensures the pairing point coordinates are properly
430 // constrained within the circuit itself, rather than relying solely on them being public outputs.
431 pairing_points.fix_witness();
432
433 // For RollupIO: Fix the IPA claim's bigfield elements (challenge and evaluation).
434 // When reconstructed from public inputs, bigfield::construct_from_limbs creates a prime_basis_limb
435 // that's computed as a linear combination of the binary limbs. Since the IPA claim is just propagated, this
436 // prime_basis_limb appears in only one gate.
437 if constexpr (IO::HasIPA) {
438 output.ipa_claim.opening_pair.challenge.fix_witness();
439 output.ipa_claim.opening_pair.evaluation.fix_witness();
440 }
441
442 info("Recursive Verifier: num gates = ", outer_circuit.get_num_finalized_gates_inefficient());
443
444 // Check for a failure flag in the recursive verifier circuit
445 EXPECT_EQ(outer_circuit.failed(), false) << outer_circuit.err();
446
447 outer_circuit.finalize_circuit(false);
448
449 // Run static analysis to detect unconstrained variables
450 // Use the appropriate analyzer based on the outer builder type
451 using Analyzer =
453 auto graph = Analyzer(outer_circuit);
454 auto [cc, variables_in_one_gate] = graph.analyze_circuit(/*filter_cc=*/true);
455
456 // We expect exactly one connected component (all variables properly connected)
457 EXPECT_EQ(cc.size(), 1);
458
459 // Expected variables in one gate:
460 size_t expected_unconstrained = 0;
461 EXPECT_EQ(variables_in_one_gate.size(), expected_unconstrained);
462 }
463};
464
466
468{
469 TestFixture::test_inner_circuit();
470}
471
472HEAVY_TYPED_TEST(RecursiveVerifierTest, RecursiveVerificationKey)
473{
474 TestFixture::test_recursive_verification_key_creation();
475}
476
477HEAVY_TYPED_TEST(RecursiveVerifierTest, SingleRecursiveVerification)
478{
479 TestFixture::test_recursive_verification();
480};
481
483{
484 using RecursiveFlavor = typename TypeParam::RecursiveFlavor;
485 if constexpr (IsAnyOf<RecursiveFlavor,
489 TestFixture::test_independent_vk_hash();
490 } else {
491 GTEST_SKIP() << "Not built for this parameter";
492 }
493};
494
495HEAVY_TYPED_TEST(RecursiveVerifierTest, SingleRecursiveVerificationFailure)
496{
497 TestFixture::test_recursive_verification_fails();
498};
499
506HEAVY_TYPED_TEST(RecursiveVerifierTest, GraphAnalysisOfRecursiveVerifier)
507{
508 TestFixture::test_recursive_verification_with_graph_analysis();
509};
510
511#ifdef DISABLE_HEAVY_TESTS
512// Null test
513TEST(RecursiveVerifierTest, DoNothingTestToEnsureATestExists) {}
514#endif
515} // namespace bb::stdlib::recursion::honk
The recursive counterpart to MegaZKFlavor.
Contains all the information required by a Honk prover to create a proof, constructed from a finalize...
static bool check(const Builder &circuit)
Check the witness satisifies the circuit.
size_t num_public_inputs() const
const std::shared_ptr< Transcript > & get_transcript() const
size_t log_dyadic_size() const
Proof export_proof()
Export the complete proof, including IPA proof for rollup circuits.
The recursive counterpart to the "native" Ultra flavor.
const std::shared_ptr< Instance > & get_verifier_instance() const
Get the verifier instance (for accessing VK and witness commitments in Chonk/Goblin)
typename Flavor::VerificationKey VerificationKey
std::conditional_t< IsRecursive, stdlib::recursion::honk::UltraRecursiveVerifierOutput< Builder >, UltraVerifierOutput< Flavor > > Output
const std::shared_ptr< Transcript > & get_transcript() const
Get the transcript (for accessing manifest in tests)
Output verify_proof(const Proof &proof)
Perform ultra verification.
The recursive counterpart to UltraZKFlavor.
A simple wrapper around a vector of stdlib field elements representing a proof.
Definition proof.hpp:19
Manages the data that is propagated on the public inputs of an application/function circuit.
Test suite for recursive verification of Honk proofs for both Ultra and Mega arithmetisation.
static void test_recursive_verification()
Construct a recursive verification circuit for the proof of an inner circuit then call check_circuit ...
static InnerBuilder create_inner_circuit(size_t log_num_gates=10)
Create a non-trivial arbitrary inner circuit, the proof of which will be recursively verified.
std::conditional_t< IO::HasIPA, bb::RollupIO, bb::DefaultIO > NativeIO
static void tamper_honk_proof(InnerProver &inner_prover, typename InnerFlavor::Transcript::Proof &inner_proof, TamperType type)
static void test_inner_circuit()
Create inner circuit and call check_circuit on it.
static void test_recursive_verification_key_creation()
Instantiate a recursive verification key from the native verification key produced by the inner cicui...
static void test_independent_vk_hash()
Ensures that the recursive verifier circuit for two inner circuits of different size is the same as t...
std::conditional_t< IO::HasIPA, RollupIO, DefaultIO< InnerBuilder > > InnerIO
std::conditional_t< IsMegaBuilder< OuterBuilder >, MegaFlavor, UltraFlavor > OuterFlavor
static void test_recursive_verification_with_graph_analysis()
Test recursive verification with static graph analysis to detect unconstrained variables.
typename RecursiveVerifier::VerificationKey VerificationKey
The data that is propagated on the public inputs of a rollup circuit.
#define info(...)
Definition log.hpp:93
AluTraceBuilder builder
Definition alu.test.cpp:124
FF a
FF b
Base class templates shared across Honk flavors.
AvmProvingInputs inputs
AvmFlavorSettings::FF FF
Definition field.hpp:10
std::filesystem::path bb_crs_path()
void init_file_crs_factory(const std::filesystem::path &path)
testing::Types< RecursiveVerifierTestParams< MegaRecursiveFlavor_< MegaCircuitBuilder >, DefaultIO< MegaCircuitBuilder > >, RecursiveVerifierTestParams< MegaRecursiveFlavor_< UltraCircuitBuilder >, DefaultIO< UltraCircuitBuilder > >, RecursiveVerifierTestParams< UltraRecursiveFlavor_< UltraCircuitBuilder >, DefaultIO< UltraCircuitBuilder > >, RecursiveVerifierTestParams< UltraRecursiveFlavor_< UltraCircuitBuilder >, RollupIO >, RecursiveVerifierTestParams< UltraRecursiveFlavor_< MegaCircuitBuilder >, DefaultIO< MegaCircuitBuilder > >, RecursiveVerifierTestParams< UltraZKRecursiveFlavor_< UltraCircuitBuilder >, DefaultIO< UltraCircuitBuilder > >, RecursiveVerifierTestParams< UltraZKRecursiveFlavor_< MegaCircuitBuilder >, DefaultIO< MegaCircuitBuilder > >, RecursiveVerifierTestParams< MegaZKRecursiveFlavor_< MegaCircuitBuilder >, DefaultIO< MegaCircuitBuilder > >, RecursiveVerifierTestParams< MegaZKRecursiveFlavor_< UltraCircuitBuilder >, DefaultIO< UltraCircuitBuilder > > > TestConfigs
TYPED_TEST_SUITE(RecursiveVerifierTest, TestConfigs)
field< Bn254FrParams > fr
Definition fr.hpp:155
TEST(BoomerangMegaCircuitBuilder, BasicCircuit)
StaticAnalyzer_< bb::fr, bb::MegaCircuitBuilder > MegaStaticAnalyzer
Definition graph.hpp:189
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
Full Honk proof layout (used by UltraVerifier).
Test utility for deserializing/serializing proof data into typed structures.
static field random_element(numeric::RNG *engine=nullptr) noexcept
An object storing two EC points that represent the inputs to a pairing check.
Output type for recursive ultra verification.
#define HEAVY_TYPED_TEST(x, y)
Definition test.hpp:11