Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
ultra_honk.test.cpp
Go to the documentation of this file.
1#include "ultra_honk.test.hpp"
4
5#include <gtest/gtest.h>
6
7using namespace bb;
8
10
11#ifdef STARKNET_GARAGA_FLAVORS
12using FlavorTypes = testing::Types<UltraFlavor,
16 UltraStarknetFlavor,
17 UltraStarknetZKFlavor>;
18#else
19using FlavorTypes = testing::Types<UltraFlavor, UltraZKFlavor, UltraKeccakFlavor, UltraKeccakZKFlavor>;
20#endif
31TYPED_TEST(UltraHonkTests, ProofLengthCheck)
32{
33 using Flavor = TypeParam;
35 using IO = typename TestFixture::IO;
36 using Proof = typename Flavor::Transcript::Proof;
37
38 auto builder = Builder{};
39 IO::add_default(builder);
40 // Construct a UH proof and ensure its size matches expectation; if not, the constant may need to be updated
42 auto verification_key = std::make_shared<typename Flavor::VerificationKey>(prover_instance->get_precomputed());
43 UltraProver_<Flavor> prover(prover_instance, verification_key);
44 Proof ultra_proof = prover.construct_proof();
45 const size_t virtual_log_n = Flavor::USE_PADDING ? CONST_PROOF_SIZE_LOG_N : prover_instance->log_dyadic_size();
46 size_t expected_proof_length =
47 ProofLength::Honk<Flavor>::LENGTH_WITHOUT_PUB_INPUTS(virtual_log_n) + IO::PUBLIC_INPUTS_SIZE;
48 EXPECT_EQ(ultra_proof.size(), expected_proof_length);
49}
50
58TYPED_TEST(UltraHonkTests, ANonZeroPolynomialIsAGoodPolynomial)
59{
60 auto circuit_builder = UltraCircuitBuilder();
61 TestFixture::set_default_pairing_points_and_ipa_claim_and_proof(circuit_builder);
62
63 auto prover_instance = std::make_shared<typename TestFixture::ProverInstance>(circuit_builder);
64 auto verification_key = std::make_shared<typename TypeParam::VerificationKey>(prover_instance->get_precomputed());
65 typename TestFixture::Prover prover(prover_instance, verification_key);
66 auto proof = prover.construct_proof();
67 auto& polynomials = prover_instance->polynomials;
68
69 auto ensure_non_zero = [](auto& polynomial) {
70 bool has_non_zero_coefficient = false;
71 for (auto& coeff : polynomial.coeffs()) {
72 has_non_zero_coefficient |= !coeff.is_zero();
73 }
74 ASSERT_TRUE(has_non_zero_coefficient);
75 };
76
77 for (auto& poly : polynomials.get_selectors()) {
78 ensure_non_zero(poly);
79 }
80
81 for (auto& poly : polynomials.get_tables()) {
82 ensure_non_zero(poly);
83 }
84
85 for (auto& poly : polynomials.get_wires()) {
86 ensure_non_zero(poly);
87 }
88}
89
95{
97 size_t num_gates = 10;
98
99 // Add some arbitrary arithmetic gates that utilize public inputs
101 TestFixture::set_default_pairing_points_and_ipa_claim_and_proof(builder);
102
103 TestFixture::prove_and_verify(builder, /*expected_result=*/true);
104}
105
106TYPED_TEST(UltraHonkTests, TestNoLookupProof)
107{
108 auto circuit_builder = UltraCircuitBuilder();
109
110 for (size_t i = 0; i < 16; ++i) {
111 for (size_t j = 0; j < 16; ++j) {
112 uint64_t left = static_cast<uint64_t>(j);
113 uint64_t right = static_cast<uint64_t>(i);
114 uint32_t left_idx = circuit_builder.add_variable(fr(left));
115 uint32_t right_idx = circuit_builder.add_variable(fr(right));
116 uint32_t result_idx = circuit_builder.add_variable(fr(left ^ right));
117
118 uint32_t add_idx =
119 circuit_builder.add_variable(fr(left) + fr(right) + circuit_builder.get_variable(result_idx));
120 circuit_builder.create_big_add_gate(
121 { left_idx, right_idx, result_idx, add_idx, fr(1), fr(1), fr(1), fr(-1), fr(0) });
122 }
123 }
124 TestFixture::set_default_pairing_points_and_ipa_claim_and_proof(circuit_builder);
125
126 TestFixture::prove_and_verify(circuit_builder, /*expected_result=*/true);
127}
128
129TYPED_TEST(UltraHonkTests, TestEllipticGate)
130{
131 typedef grumpkin::g1::affine_element affine_element;
132 typedef grumpkin::g1::element element;
133 auto circuit_builder = UltraCircuitBuilder();
134
135 affine_element p1 = affine_element::random_element();
136 affine_element p2 = affine_element::random_element();
137
138 affine_element p3(element(p1) + element(p2));
139
140 uint32_t x1 = circuit_builder.add_variable(p1.x);
141 uint32_t y1 = circuit_builder.add_variable(p1.y);
142 uint32_t x2 = circuit_builder.add_variable(p2.x);
143 uint32_t y2 = circuit_builder.add_variable(p2.y);
144 uint32_t x3 = circuit_builder.add_variable(p3.x);
145 uint32_t y3 = circuit_builder.add_variable(p3.y);
146
147 circuit_builder.create_ecc_add_gate({ x1, y1, x2, y2, x3, y3, /*is_addition=*/true });
148
149 p3 = affine_element(element(p1) + element(p2));
150 x3 = circuit_builder.add_variable(p3.x);
151 y3 = circuit_builder.add_variable(p3.y);
152 circuit_builder.create_ecc_add_gate({ x1, y1, x2, y2, x3, y3, /*is_addition=*/true });
153
154 p3 = affine_element(element(p1) - element(p2));
155 x3 = circuit_builder.add_variable(p3.x);
156 y3 = circuit_builder.add_variable(p3.y);
157 circuit_builder.create_ecc_add_gate({ x1, y1, x2, y2, x3, y3, /*is_addition=*/false });
158
159 TestFixture::set_default_pairing_points_and_ipa_claim_and_proof(circuit_builder);
160
161 TestFixture::prove_and_verify(circuit_builder, /*expected_result=*/true);
162}
163
164TYPED_TEST(UltraHonkTests, NonNativeFieldMultiplication)
165{
166 using fq = fq;
167 auto circuit_builder = UltraCircuitBuilder();
168
171 uint256_t modulus = fq::modulus;
172
175 uint1024_t p_big = uint512_t(uint256_t(modulus));
176
177 uint1024_t q_big = (a_big * b_big) / p_big;
178 uint1024_t r_big = (a_big * b_big) % p_big;
179
180 uint256_t q(q_big.lo.lo);
181 uint256_t r(r_big.lo.lo);
182
183 const auto split_into_limbs = [&](const uint512_t& input) {
184 constexpr size_t NUM_BITS = 68;
185 std::array<fr, 4> limbs;
186 limbs[0] = input.slice(0, NUM_BITS).lo;
187 limbs[1] = input.slice(NUM_BITS * 1, NUM_BITS * 2).lo;
188 limbs[2] = input.slice(NUM_BITS * 2, NUM_BITS * 3).lo;
189 limbs[3] = input.slice(NUM_BITS * 3, NUM_BITS * 4).lo;
190 return limbs;
191 };
192
193 const auto get_limb_witness_indices = [&](const std::array<fr, 4>& limbs) {
194 std::array<uint32_t, 4> limb_indices;
195 limb_indices[0] = circuit_builder.add_variable(limbs[0]);
196 limb_indices[1] = circuit_builder.add_variable(limbs[1]);
197 limb_indices[2] = circuit_builder.add_variable(limbs[2]);
198 limb_indices[3] = circuit_builder.add_variable(limbs[3]);
199 return limb_indices;
200 };
201 const uint512_t BINARY_BASIS_MODULUS = uint512_t(1) << (68 * 4);
202 auto modulus_limbs = split_into_limbs(BINARY_BASIS_MODULUS - uint512_t(modulus));
203
204 const auto a_indices = get_limb_witness_indices(split_into_limbs(uint256_t(a)));
205 const auto b_indices = get_limb_witness_indices(split_into_limbs(uint256_t(b)));
206 const auto q_indices = get_limb_witness_indices(split_into_limbs(uint256_t(q)));
207 const auto r_indices = get_limb_witness_indices(split_into_limbs(uint256_t(r)));
208
210 a_indices, b_indices, q_indices, r_indices, modulus_limbs,
211 };
212 const auto [lo_1_idx, hi_1_idx] = circuit_builder.evaluate_non_native_field_multiplication(inputs);
213
214 // Range constrain the lo and hi carry outputs
215 const bool is_low_70_bits = uint256_t(circuit_builder.get_variable(lo_1_idx)).get_msb() < 70;
216 const bool is_high_70_bits = uint256_t(circuit_builder.get_variable(hi_1_idx)).get_msb() < 70;
217 if (is_low_70_bits && is_high_70_bits) {
218 // Uses more efficient NNF range check if both limbs are < 2^70
219 circuit_builder.range_constrain_two_limbs(lo_1_idx, hi_1_idx, 70, 70);
220 } else {
221 // Fallback to default range checks
222 circuit_builder.create_limbed_range_constraint(lo_1_idx, 72);
223 circuit_builder.create_limbed_range_constraint(hi_1_idx, 72);
224 }
225
226 TestFixture::set_default_pairing_points_and_ipa_claim_and_proof(circuit_builder);
227
228 TestFixture::prove_and_verify(circuit_builder, /*expected_result=*/true);
229}
230
231TYPED_TEST(UltraHonkTests, RangeChecksOnDuplicates)
232{
233 auto circuit_builder = UltraCircuitBuilder();
234
235 uint32_t a = circuit_builder.add_variable(fr(100));
236 uint32_t b = circuit_builder.add_variable(fr(100));
237 uint32_t c = circuit_builder.add_variable(fr(100));
238 uint32_t d = circuit_builder.add_variable(fr(100));
239
240 circuit_builder.assert_equal(a, b);
241 circuit_builder.assert_equal(a, c);
242 circuit_builder.assert_equal(a, d);
243
244 circuit_builder.create_small_range_constraint(a, 1000);
245 circuit_builder.create_small_range_constraint(b, 1001);
246 circuit_builder.create_small_range_constraint(c, 999);
247 circuit_builder.create_small_range_constraint(d, 1000);
248
249 circuit_builder.create_big_add_gate(
250 {
251 a,
252 b,
253 c,
254 d,
255 0,
256 0,
257 0,
258 0,
259 0,
260 },
261 false);
262
263 TestFixture::set_default_pairing_points_and_ipa_claim_and_proof(circuit_builder);
264
265 TestFixture::prove_and_verify(circuit_builder, /*expected_result=*/true);
266}
267
268// Ensure copy constraints added on variables smaller than 2^14, which have been previously
269// range constrained, do not break the set equivalence checks because of indices mismatch.
270// 2^14 is DEFAULT_PLOOKUP_RANGE_BITNUM i.e. the maximum size before a variable gets sliced
271// before range constraints are applied to it.
272TYPED_TEST(UltraHonkTests, RangeConstraintSmallVariable)
273{
274 auto circuit_builder = UltraCircuitBuilder();
275
276 uint16_t mask = (1 << 8) - 1;
277 int a = engine.get_random_uint16() & mask;
278 uint32_t a_idx = circuit_builder.add_variable(fr(a));
279 uint32_t b_idx = circuit_builder.add_variable(fr(a));
280 ASSERT_NE(a_idx, b_idx);
281 uint32_t c_idx = circuit_builder.add_variable(fr(a));
282 ASSERT_NE(c_idx, b_idx);
283 circuit_builder.create_dyadic_range_constraint(b_idx, 8, "bad range");
284 circuit_builder.assert_equal(a_idx, b_idx);
285 circuit_builder.create_dyadic_range_constraint(c_idx, 8, "bad range");
286 circuit_builder.assert_equal(a_idx, c_idx);
287
288 TestFixture::set_default_pairing_points_and_ipa_claim_and_proof(circuit_builder);
289
290 TestFixture::prove_and_verify(circuit_builder, /*expected_result=*/true);
291}
292
299TYPED_TEST(UltraHonkTests, NativeVKHashMismatchDetected)
300{
301 using Flavor = TypeParam;
302 using IO = typename TestFixture::IO;
303 using Builder = typename Flavor::CircuitBuilder;
304 using Prover = UltraProver_<Flavor>;
307 using VKAndHash = typename Flavor::VKAndHash;
308 using Verifier = UltraVerifier_<Flavor, IO>;
309
310 // Create a simple circuit
313 this->set_default_pairing_points_and_ipa_claim_and_proof(builder);
314
315 // Create prover instance and VK
316 auto prover_instance = std::make_shared<ProverInstance>(builder);
317 auto vk = std::make_shared<VerificationKey>(prover_instance->get_precomputed());
318
319 // Create prover and prove
320 Prover prover(prover_instance, vk);
321 auto proof = prover.construct_proof();
322 auto vk_and_hash = std::make_shared<VKAndHash>(vk);
323
324 // Corrupt the stored hash
325 vk_and_hash->hash = fr::random_element();
326
327 // Verification should fail with BB_ASSERT_EQ detecting the mismatch
328 Verifier verifier(vk_and_hash);
329 EXPECT_THROW_WITH_MESSAGE(verifier.verify_proof(proof), "VK Hash Mismatch");
330}
331
337TYPED_TEST(UltraHonkTests, TooShortProofRejected)
338{
339 using Flavor = TypeParam;
340 using IO = typename TestFixture::IO;
341 using Builder = typename Flavor::CircuitBuilder;
342 using Prover = UltraProver_<Flavor>;
345 using VKAndHash = typename Flavor::VKAndHash;
346 using Verifier = UltraVerifier_<Flavor, IO>;
347 using Proof = typename Flavor::Transcript::Proof;
348
349 // Create a simple circuit and produce a valid proof
352 this->set_default_pairing_points_and_ipa_claim_and_proof(builder);
353
354 auto prover_instance = std::make_shared<ProverInstance>(builder);
355 auto vk = std::make_shared<VerificationKey>(prover_instance->get_precomputed());
356
357 Prover prover(prover_instance, vk);
358 auto proof = prover.construct_proof();
359
360 // Truncate the proof by removing the last 10 elements
361 Proof truncated_proof(proof.begin(), proof.end() - 10);
362
363 auto vk_and_hash = std::make_shared<VKAndHash>(vk);
364 Verifier verifier(vk_and_hash);
365 EXPECT_THROW_WITH_MESSAGE(verifier.verify_proof(truncated_proof), "Proof size too small");
366}
367
373TYPED_TEST(UltraHonkTests, TooLongProofRejected)
374{
375 using Flavor = TypeParam;
376 using IO = typename TestFixture::IO;
377 using Builder = typename Flavor::CircuitBuilder;
378 using Prover = UltraProver_<Flavor>;
381 using VKAndHash = typename Flavor::VKAndHash;
382 using Verifier = UltraVerifier_<Flavor, IO>;
383 using Proof = typename Flavor::Transcript::Proof;
384 using FF = typename Flavor::FF;
385
386 // Create a simple circuit and produce a valid proof
389 this->set_default_pairing_points_and_ipa_claim_and_proof(builder);
390
391 auto prover_instance = std::make_shared<ProverInstance>(builder);
392 auto vk = std::make_shared<VerificationKey>(prover_instance->get_precomputed());
393
394 Prover prover(prover_instance, vk);
395 auto proof = prover.construct_proof();
396
397 // Append extra elements to the proof
398 Proof extended_proof(proof);
399 for (size_t i = 0; i < 10; i++) {
400 extended_proof.push_back(FF::random_element());
401 }
402
403 auto vk_and_hash = std::make_shared<VKAndHash>(vk);
404 Verifier verifier(vk_and_hash);
405 EXPECT_THROW_WITH_MESSAGE(verifier.verify_proof(extended_proof), "num_public_inputs mismatch");
406}
407
418TYPED_TEST(UltraHonkTests, DyadicSizeJumpsToProtectMaskingArea)
419{
420 using Flavor = TypeParam;
421 if constexpr (!Flavor::HasZK) {
422 GTEST_SKIP() << "Masking area only exists for ZK flavors";
423 } else {
424 using Builder = typename Flavor::CircuitBuilder;
426
427 // Determine the baseline dyadic size (pairing points + finalization overhead, no user gates)
428 Builder baseline_builder;
429 this->set_default_pairing_points_and_ipa_claim_and_proof(baseline_builder);
430 auto baseline_instance = std::make_shared<ProverInstance>(baseline_builder);
431 const size_t baseline_dyadic = baseline_instance->dyadic_size();
432
433 // Add gates one at a time until the dyadic size doubles
434 size_t prev_dyadic = 0;
435 size_t prev_final_active_idx = 0;
436 bool found_jump = false;
437 for (size_t num_extra_gates = 0; num_extra_gates <= baseline_dyadic; num_extra_gates++) {
439 if (num_extra_gates > 0) {
441 }
442 this->set_default_pairing_points_and_ipa_claim_and_proof(builder);
443
444 auto prover_instance = std::make_shared<ProverInstance>(builder);
445
446 const size_t dyadic_size = prover_instance->dyadic_size();
447 const size_t final_active_idx = prover_instance->get_final_active_wire_idx();
448 const size_t first_masked_row = dyadic_size - NUM_MASKED_ROWS;
449
450 // Invariant (1): lagrange_last must be strictly before the masking area
451 ASSERT_LT(final_active_idx, first_masked_row)
452 << "lagrange_last (at " << final_active_idx << ") overlaps masking area (starting at "
453 << first_masked_row << ") with num_extra_gates=" << num_extra_gates;
454
455 // Invariant (2): sufficient headroom for disabled rows
456 ASSERT_GE(dyadic_size - final_active_idx - 1, static_cast<size_t>(NUM_DISABLED_ROWS_IN_SUMCHECK));
457
458 if (prev_dyadic != 0 && dyadic_size > prev_dyadic) {
459 // Invariant (3): dyadic size should exactly double
460 EXPECT_EQ(dyadic_size, 2 * prev_dyadic);
461 // The previous circuit was tightly packed
462 EXPECT_LE(prev_dyadic - prev_final_active_idx - 1,
463 2 * static_cast<size_t>(NUM_DISABLED_ROWS_IN_SUMCHECK));
464
465 // Prove and verify at the tightest packing (right before the jump)
466 Builder tight_builder;
467 MockCircuits::add_arithmetic_gates(tight_builder, num_extra_gates - 1);
468 this->set_default_pairing_points_and_ipa_claim_and_proof(tight_builder);
469 auto tight_instance = std::make_shared<ProverInstance>(tight_builder);
470 this->prove_and_verify(tight_instance, /*expected_result=*/true);
471
472 found_jump = true;
473 break;
474 }
475
476 prev_dyadic = dyadic_size;
477 prev_final_active_idx = final_active_idx;
478 }
479
480 EXPECT_TRUE(found_jump) << "should have found a dyadic size jump within " << baseline_dyadic << " extra gates";
481 }
482}
#define EXPECT_THROW_WITH_MESSAGE(code, expectedMessageRegex)
Definition assert.hpp:193
static constexpr bool HasZK
typename Curve::ScalarField FF
ECCVMCircuitBuilder CircuitBuilder
FixedVKAndHash_< PrecomputedEntities< Commitment >, BF, ECCVMHardcodedVKAndHash > VerificationKey
The verification key stores commitments to the precomputed polynomials used by the verifier.
static constexpr bool USE_PADDING
static void add_arithmetic_gates_with_public_inputs(Builder &builder, const size_t num_gates=4)
Add a specified number of arithmetic gates (with public inputs) to the provided circuit.
static void add_arithmetic_gates(Builder &builder, const size_t num_gates=4)
Add a specified number of arithmetic gates to the provided circuit.
Base Native verification key class.
Definition flavor.hpp:135
Contains all the information required by a Honk prover to create a proof, constructed from a finalize...
Child class of UltraFlavor that runs with ZK Sumcheck.
element class. Implements ecc group arithmetic using Jacobian coordinates See https://hyperelliptic....
Definition element.hpp:33
virtual uint16_t get_random_uint16()=0
constexpr uint64_t get_msb() const
AluTraceBuilder builder
Definition alu.test.cpp:124
FF a
FF b
numeric::RNG & engine
testing::Types< UltraFlavor, UltraKeccakFlavor, MegaFlavor > FlavorTypes
AvmProvingInputs inputs
uintx< uint256_t > uint512_t
Definition uintx.hpp:306
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
field< Bn254FqParams > fq
Definition fq.hpp:153
TYPED_TEST_SUITE(CommitmentKeyTest, Curves)
field< Bn254FrParams > fr
Definition fr.hpp:155
TYPED_TEST(CommitmentKeyTest, CommitToZeroPoly)
UltraCircuitBuilder_< UltraExecutionTraceBlocks > UltraCircuitBuilder
VerifierCommitmentKey< Curve > vk
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
static constexpr size_t LENGTH_WITHOUT_PUB_INPUTS(size_t log_n)
static constexpr uint256_t modulus
static field random_element(numeric::RNG *engine=nullptr) noexcept
An object storing two EC points that represent the inputs to a pairing check.
void ensure_non_zero(auto &polynomial)