Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
ultra_circuit_builder_nonnative.test.cpp
Go to the documentation of this file.
4
5#include <gtest/gtest.h>
6
7using namespace bb;
8
20class UltraCircuitBuilderNonNative : public ::testing::Test {
21 protected:
23 static inline const uint512_t BINARY_BASIS_MODULUS = uint512_t(1) << (LIMB_BITS * 4);
24
25 // Number of NNF gates produced by a single partial multiplication
26 static constexpr size_t NNF_GATES_PER_PARTIAL_MUL = 4;
27 // Number of padding gates added to NNF block by finalize_circuit(ensure_nonzero=true)
28 static constexpr size_t NNF_ENSURE_NONZERO_PADDING = 2;
29
30 // Generate 4 random field elements (one per limb)
35
36 // Splits a 256-bit integer into 4 68-bit limbs
38 {
40 limbs[0] = input.slice(0, LIMB_BITS).lo;
41 limbs[1] = input.slice(LIMB_BITS * 1, LIMB_BITS * 2).lo;
42 limbs[2] = input.slice(LIMB_BITS * 2, LIMB_BITS * 3).lo;
43 limbs[3] = input.slice(LIMB_BITS * 3, LIMB_BITS * 4).lo;
44 return limbs;
45 }
46
47 // Adds 4 limbs as circuit variables and returns their indices
48 static std::array<uint32_t, 4> get_limb_witness_indices(UltraCircuitBuilder& builder,
49 const std::array<fr, 4>& limbs)
50 {
51 std::array<uint32_t, 4> limb_indices;
52 limb_indices[0] = builder.add_variable(limbs[0]);
53 limb_indices[1] = builder.add_variable(limbs[1]);
54 limb_indices[2] = builder.add_variable(limbs[2]);
55 limb_indices[3] = builder.add_variable(limbs[3]);
56 return limb_indices;
57 }
58
59 // Helper to set up and evaluate a non-native field multiplication
61 const uint256_t& a,
62 const uint256_t& b,
63 const uint256_t& q,
64 const uint256_t& r,
65 const uint256_t& modulus)
66 {
67 // Compute negative modulus: (-p) := 2^T - p
68 auto modulus_limbs = split_into_limbs(BINARY_BASIS_MODULUS - uint512_t(modulus));
69
70 // Add a, b, q, r as circuit variables
75
76 // Prepare inputs for non-native multiplication gadget
78 a_indices, b_indices, q_indices, r_indices, modulus_limbs,
79 };
80 const auto [lo_1_idx, hi_1_idx] = builder.evaluate_non_native_field_multiplication(inputs);
81
82 return { lo_1_idx, hi_1_idx };
83 }
84
85 // Compute quotient and remainder for a * b mod p
87 const uint256_t& b,
88 const uint256_t& modulus)
89 {
90 uint1024_t a_big = uint512_t(a);
91 uint1024_t b_big = uint512_t(b);
92 uint1024_t p_big = uint512_t(modulus);
93
94 uint1024_t q_big = (a_big * b_big) / p_big;
95 uint1024_t r_big = (a_big * b_big) % p_big;
96
97 return { uint256_t(q_big.lo.lo), uint256_t(r_big.lo.lo) };
98 }
99
100 // Type aliases matching builder API for addition/subtraction
103
104 // Data structure for addition/subtraction test inputs
115
116 // Create random limb values for testing
118 {
119 return AddSubData{
121 .y_limbs = random_limbs(),
122 .x_prime = fr::random_element(),
123 .y_prime = fr::random_element(),
124 .x_scales = { fr(1), fr(1), fr(1), fr(1) },
125 .y_scales = { fr(1), fr(1), fr(1), fr(1) },
126 .addconsts = { fr(0), fr(0), fr(0), fr(0) },
127 .addconstp = fr(0),
128 };
129 }
130
131 // Create add_simple tuples from test data and witness indices
134 {
135 // Add witness variables
136 std::array<uint32_t, 4> x_idx, y_idx;
137 for (size_t i = 0; i < 4; i++) {
138 x_idx[i] = builder.add_variable(data.x_limbs[i]);
139 y_idx[i] = builder.add_variable(data.y_limbs[i]);
140 }
141 uint32_t x_p_idx = builder.add_variable(data.x_prime);
142 uint32_t y_p_idx = builder.add_variable(data.y_prime);
143
144 // Build add_simple tuples: ((x_idx, x_scale), (y_idx, y_scale), addconst)
145 add_simple limb0 = { { x_idx[0], data.x_scales[0] }, { y_idx[0], data.y_scales[0] }, data.addconsts[0] };
146 add_simple limb1 = { { x_idx[1], data.x_scales[1] }, { y_idx[1], data.y_scales[1] }, data.addconsts[1] };
147 add_simple limb2 = { { x_idx[2], data.x_scales[2] }, { y_idx[2], data.y_scales[2] }, data.addconsts[2] };
148 add_simple limb3 = { { x_idx[3], data.x_scales[3] }, { y_idx[3], data.y_scales[3] }, data.addconsts[3] };
149 auto limbp = std::make_tuple(x_p_idx, y_p_idx, data.addconstp);
150
151 return { limb0, limb1, limb2, limb3, limbp };
152 }
153
154 // Helper to create partial multiplication inputs and queue the operation
156 const std::array<fr, 4>& a_limbs,
157 const std::array<fr, 4>& b_limbs)
158 {
159 const auto a_indices = get_limb_witness_indices(builder, a_limbs);
160 const auto b_indices = get_limb_witness_indices(builder, b_limbs);
161
162 non_native_partial_multiplication_witnesses<fr> input{ .a = a_indices, .b = b_indices };
163 return builder.queue_partial_non_native_field_multiplication(input);
164 }
165
166 // Compute expected lo_0 and hi_1 values for partial multiplication.
167 //
168 // Given two non-native field elements represented as 4 limbs each:
169 // a = a[0] + a[1]*L + a[2]*L² + a[3]*L³ (where L = 2^LIMB_BITS)
170 // b = b[0] + b[1]*L + b[2]*L² + b[3]*L³
171 //
172 // The product a*b expands via schoolbook multiplication to terms at powers of L:
173 // L⁰: a[0]*b[0]
174 // L¹: a[1]*b[0] + a[0]*b[1]
175 // L²: a[2]*b[0] + a[1]*b[1] + a[0]*b[2]
176 // L³: a[3]*b[0] + a[2]*b[1] + a[1]*b[2] + a[0]*b[3]
177 // (higher powers of L can be ignored)
178 //
179 // The partial products group these terms:
180 // lo_0 = L⁰ and L¹ terms (low 2*LIMB_BITS portion)
181 // hi_1 = L² and L³ terms (high portion, split into hi_0 + remaining for constraint efficiency)
183 {
184 const fr LIMB_SHIFT = fr(uint256_t(1) << LIMB_BITS);
185
186 fr lo_0 = a[0] * b[0] + ((a[1] * b[0] + a[0] * b[1]) * LIMB_SHIFT);
187 fr hi_0 = a[2] * b[0] + a[0] * b[2] + ((a[0] * b[3] + a[3] * b[0]) * LIMB_SHIFT);
188 fr hi_1 = hi_0 + a[1] * b[1] + ((a[1] * b[2] + a[2] * b[1]) * LIMB_SHIFT);
189
190 return { lo_0, hi_1 };
191 }
192};
193
194// Verifies that valid non-native field multiplications pass the circuit checker
196{
197 const size_t num_iterations = 50;
198 for (size_t i = 0; i < num_iterations; i++) {
200
203 uint256_t modulus = fq::modulus;
204
205 auto [q, r] = compute_quotient_remainder(a, b, modulus);
206 const auto [lo_1_idx, hi_1_idx] = create_non_native_multiplication(builder, a, b, q, r, modulus);
207
208 // Range check the carry (output) lo and hi limbs
209 const bool is_low_70_bits = uint256_t(builder.get_variable(lo_1_idx)).get_msb() < 70;
210 const bool is_high_70_bits = uint256_t(builder.get_variable(hi_1_idx)).get_msb() < 70;
211 if (is_low_70_bits && is_high_70_bits) {
212 // Uses more efficient NNF range check if both limbs are < 2^70
213 builder.range_constrain_two_limbs(lo_1_idx, hi_1_idx, 70, 70);
214 } else {
215 // Fallback to default range checks
216 builder.decompose_into_default_range(lo_1_idx, 72);
217 builder.decompose_into_default_range(hi_1_idx, 72);
218 }
219
220 EXPECT_TRUE(CircuitChecker::check(builder));
221 }
222}
223
224// Regression test: carry limb > 2^70 requires fallback to default range checks
225TEST_F(UltraCircuitBuilderNonNative, MultiplicationLargeCarryRegression)
226{
228
229 // Edge case values that produce carry > 2^70
230 uint256_t a = uint256_t("0x00ab1504deacff852326adf4a01099e9340f232e2a631042852fce3c4eb8a51b");
231 uint256_t b = uint256_t("0x1be457323502cfcd85f8cfa54c8c4fea146b9db2a7d86b29d966d61b714ee249");
232 uint256_t q_expected = uint256_t("0x00629b9d576dfc6b5c28a4a254d5e8e3384124f6a898858e95265254a01414d5");
233 uint256_t r_expected = uint256_t("0x2c1590eb70a48dce72f7686bbf79b59bf7926c99bc16aba92e474c65a04ea2a0");
234 uint256_t modulus = fq::modulus;
235
236 // Verify that our expected q, r are correct
237 auto [q_computed, r_computed] = compute_quotient_remainder(a, b, modulus);
238 EXPECT_EQ(q_computed, q_expected);
239 EXPECT_EQ(r_computed, r_expected);
240
241 // This edge case leads to the carry limb being > 2^70, so it used to fail when applying a 2^70 range check
242 // (with range_constrain_two_limbs). Now it should work since we fallback to default range checks in such a case.
243 const auto [lo_1_idx, hi_1_idx] = create_non_native_multiplication(builder, a, b, q_expected, r_expected, modulus);
244
245 // Range check the carry (output) lo and hi limbs
246 const bool is_high_70_bits = uint256_t(builder.get_variable(hi_1_idx)).get_msb() < 70;
247 BB_ASSERT(is_high_70_bits == false); // Regression should hit this case
248
249 // Decompose into default range: these should work even if the limbs are > 2^70
250 builder.decompose_into_default_range(lo_1_idx, 72);
251 builder.decompose_into_default_range(hi_1_idx, 72);
252 EXPECT_TRUE(CircuitChecker::check(builder));
253
254 // Using NNF range check should fail here
255 builder.range_constrain_two_limbs(lo_1_idx, hi_1_idx, 70, 70);
256 EXPECT_FALSE(CircuitChecker::check(builder));
257 EXPECT_EQ(builder.err(), "range_constrain_two_limbs: hi limb.");
258}
259
260// Verifies that the NNF block only contains NNF gates (other selectors are zero)
261TEST_F(UltraCircuitBuilderNonNative, BlockSelectorIsolation)
262{
264
267 uint256_t modulus = fq::modulus;
268
269 auto [q, r] = compute_quotient_remainder(a, b, modulus);
270 const auto [lo_1_idx, hi_1_idx] = create_non_native_multiplication(builder, a, b, q, r, modulus);
271
272 // Range check the carry (output) lo and hi limbs
273 const bool is_low_70_bits = uint256_t(builder.get_variable(lo_1_idx)).get_msb() < 70;
274 const bool is_high_70_bits = uint256_t(builder.get_variable(hi_1_idx)).get_msb() < 70;
275 if (is_low_70_bits && is_high_70_bits) {
276 builder.range_constrain_two_limbs(lo_1_idx, hi_1_idx, 70, 70);
277 } else {
278 builder.decompose_into_default_range(lo_1_idx, 72);
279 builder.decompose_into_default_range(hi_1_idx, 72);
280 }
281
282 EXPECT_TRUE(CircuitChecker::check(builder));
283
284 // Verify that in the NNF block, all non-NNF selectors are zero
285 for (size_t i = 0; i < builder.blocks.nnf.size(); ++i) {
286 EXPECT_EQ(builder.blocks.nnf.q_arith()[i], 0);
287 EXPECT_EQ(builder.blocks.nnf.q_delta_range()[i], 0);
288 EXPECT_EQ(builder.blocks.nnf.q_elliptic()[i], 0);
289 EXPECT_EQ(builder.blocks.nnf.q_lookup()[i], 0);
290 EXPECT_EQ(builder.blocks.nnf.q_poseidon2_external()[i], 0);
291 EXPECT_EQ(builder.blocks.nnf.q_poseidon2_internal()[i], 0);
292 }
293}
294
295// Verifies non-native field addition with various scaling factors
297{
298 // Test with identity scaling (z = x + y)
299 {
301 auto data = create_random_add_sub_data();
302 auto [limb0, limb1, limb2, limb3, limbp] = create_add_sub_inputs(builder, data);
303 builder.evaluate_non_native_field_addition(limb0, limb1, limb2, limb3, limbp);
304 EXPECT_TRUE(CircuitChecker::check(builder));
305 }
306
307 // Test with different scaling factors per limb and non-zero addconstp
308 {
310 auto data = create_random_add_sub_data();
311 data.x_scales = { fr(2), fr(3), fr(5), fr(7) };
312 data.y_scales = { fr(11), fr(13), fr(17), fr(19) };
313 data.addconsts = { fr(100), fr(200), fr(300), fr(400) };
314 data.addconstp = fr(500);
315
316 auto [limb0, limb1, limb2, limb3, limbp] = create_add_sub_inputs(builder, data);
317 builder.evaluate_non_native_field_addition(limb0, limb1, limb2, limb3, limbp);
318 EXPECT_TRUE(CircuitChecker::check(builder));
319 }
320
321 // Test with negative and zero scaling factors
322 {
324 auto data = create_random_add_sub_data();
325 data.x_scales = { fr(-1), fr(0), fr(1), fr(-2) };
326 data.y_scales = { fr(1), fr(-1), fr(0), fr(2) };
327 data.addconsts = { fr(0), fr(-50), fr(50), fr(0) };
328 data.addconstp = fr(-100);
329
330 auto [limb0, limb1, limb2, limb3, limbp] = create_add_sub_inputs(builder, data);
331 builder.evaluate_non_native_field_addition(limb0, limb1, limb2, limb3, limbp);
332 EXPECT_TRUE(CircuitChecker::check(builder));
333 }
334
335 // Edge case: all zeros
336 {
338 AddSubData data{
339 .x_limbs = { fr(0), fr(0), fr(0), fr(0) },
340 .y_limbs = { fr(0), fr(0), fr(0), fr(0) },
341 .x_prime = fr(0),
342 .y_prime = fr(0),
343 .x_scales = { fr(1), fr(1), fr(1), fr(1) },
344 .y_scales = { fr(1), fr(1), fr(1), fr(1) },
345 .addconsts = { fr(0), fr(0), fr(0), fr(0) },
346 .addconstp = fr(0),
347 };
348 auto [limb0, limb1, limb2, limb3, limbp] = create_add_sub_inputs(builder, data);
349 builder.evaluate_non_native_field_addition(limb0, limb1, limb2, limb3, limbp);
350 EXPECT_TRUE(CircuitChecker::check(builder));
351 }
352
353 // Edge case: x == y (doubling)
354 {
356 fr val0 = fr::random_element();
357 fr val1 = fr::random_element();
358 fr val2 = fr::random_element();
359 fr val3 = fr::random_element();
360 fr valp = fr::random_element();
361 AddSubData data{
362 .x_limbs = { val0, val1, val2, val3 },
363 .y_limbs = { val0, val1, val2, val3 },
364 .x_prime = valp,
365 .y_prime = valp,
366 .x_scales = { fr(1), fr(1), fr(1), fr(1) },
367 .y_scales = { fr(1), fr(1), fr(1), fr(1) },
368 .addconsts = { fr(0), fr(0), fr(0), fr(0) },
369 .addconstp = fr(0),
370 };
371 auto [limb0, limb1, limb2, limb3, limbp] = create_add_sub_inputs(builder, data);
372 builder.evaluate_non_native_field_addition(limb0, limb1, limb2, limb3, limbp);
373 EXPECT_TRUE(CircuitChecker::check(builder));
374 }
375}
376
377// Verifies non-native field subtraction with various scaling factors
379{
380 // Test with identity scaling (z = x - y)
381 {
383 auto data = create_random_add_sub_data();
384 auto [limb0, limb1, limb2, limb3, limbp] = create_add_sub_inputs(builder, data);
385 builder.evaluate_non_native_field_subtraction(limb0, limb1, limb2, limb3, limbp);
386 EXPECT_TRUE(CircuitChecker::check(builder));
387 }
388
389 // Test with different scaling factors per limb and non-zero addconstp
390 {
392 auto data = create_random_add_sub_data();
393 data.x_scales = { fr(2), fr(3), fr(5), fr(7) };
394 data.y_scales = { fr(11), fr(13), fr(17), fr(19) };
395 data.addconsts = { fr(100), fr(200), fr(300), fr(400) };
396 data.addconstp = fr(500);
397
398 auto [limb0, limb1, limb2, limb3, limbp] = create_add_sub_inputs(builder, data);
399 builder.evaluate_non_native_field_subtraction(limb0, limb1, limb2, limb3, limbp);
400 EXPECT_TRUE(CircuitChecker::check(builder));
401 }
402
403 // Test with negative and zero scaling factors
404 {
406 auto data = create_random_add_sub_data();
407 data.x_scales = { fr(-1), fr(0), fr(1), fr(-2) };
408 data.y_scales = { fr(1), fr(-1), fr(0), fr(2) };
409 data.addconsts = { fr(0), fr(-50), fr(50), fr(0) };
410 data.addconstp = fr(-100);
411
412 auto [limb0, limb1, limb2, limb3, limbp] = create_add_sub_inputs(builder, data);
413 builder.evaluate_non_native_field_subtraction(limb0, limb1, limb2, limb3, limbp);
414 EXPECT_TRUE(CircuitChecker::check(builder));
415 }
416
417 // Edge case: all zeros
418 {
420 AddSubData data{
421 .x_limbs = { fr(0), fr(0), fr(0), fr(0) },
422 .y_limbs = { fr(0), fr(0), fr(0), fr(0) },
423 .x_prime = fr(0),
424 .y_prime = fr(0),
425 .x_scales = { fr(1), fr(1), fr(1), fr(1) },
426 .y_scales = { fr(1), fr(1), fr(1), fr(1) },
427 .addconsts = { fr(0), fr(0), fr(0), fr(0) },
428 .addconstp = fr(0),
429 };
430 auto [limb0, limb1, limb2, limb3, limbp] = create_add_sub_inputs(builder, data);
431 builder.evaluate_non_native_field_subtraction(limb0, limb1, limb2, limb3, limbp);
432 EXPECT_TRUE(CircuitChecker::check(builder));
433 }
434
435 // Edge case: x == y (result is zero)
436 {
438 fr val0 = fr::random_element();
439 fr val1 = fr::random_element();
440 fr val2 = fr::random_element();
441 fr val3 = fr::random_element();
442 fr valp = fr::random_element();
443 AddSubData data{
444 .x_limbs = { val0, val1, val2, val3 },
445 .y_limbs = { val0, val1, val2, val3 },
446 .x_prime = valp,
447 .y_prime = valp,
448 .x_scales = { fr(1), fr(1), fr(1), fr(1) },
449 .y_scales = { fr(1), fr(1), fr(1), fr(1) },
450 .addconsts = { fr(0), fr(0), fr(0), fr(0) },
451 .addconstp = fr(0),
452 };
453 auto [limb0, limb1, limb2, limb3, limbp] = create_add_sub_inputs(builder, data);
454 builder.evaluate_non_native_field_subtraction(limb0, limb1, limb2, limb3, limbp);
455 EXPECT_TRUE(CircuitChecker::check(builder));
456 }
457}
458
459// Verifies that providing incorrect witnesses to multiplication causes failure
460TEST_F(UltraCircuitBuilderNonNative, MultiplicationInvalidWitnessFailure)
461{
462 // Helper to test that providing incorrect quotient/remainder causes failure
463 auto test_incorrect_qr = [](bool tamper_q, size_t limb_idx) {
465
468 uint256_t modulus = fq::modulus;
469 auto [q, r] = compute_quotient_remainder(a, b, modulus);
470
471 // Tamper with quotient or remainder
472 if (tamper_q) {
473 // Add 1 to a specific limb of q
474 auto q_limbs = split_into_limbs(uint256_t(q));
475 q_limbs[limb_idx] += fr(1);
476 q = uint256_t(q_limbs[0]) + (uint256_t(q_limbs[1]) << LIMB_BITS) +
477 (uint256_t(q_limbs[2]) << (LIMB_BITS * 2)) + (uint256_t(q_limbs[3]) << (LIMB_BITS * 3));
478 } else {
479 // Add 1 to a specific limb of r
480 auto r_limbs = split_into_limbs(uint256_t(r));
481 r_limbs[limb_idx] += fr(1);
482 r = uint256_t(r_limbs[0]) + (uint256_t(r_limbs[1]) << LIMB_BITS) +
483 (uint256_t(r_limbs[2]) << (LIMB_BITS * 2)) + (uint256_t(r_limbs[3]) << (LIMB_BITS * 3));
484 }
485
486 // Now a*b != q*p + r (the multiplication identity is violated)
487 const auto [lo_idx, hi_idx] = create_non_native_multiplication(builder, a, b, q, r, modulus);
488
489 // Add range constraints
490 builder.decompose_into_default_range(lo_idx, 72);
491 builder.decompose_into_default_range(hi_idx, 72);
492
493 EXPECT_FALSE(CircuitChecker::check(builder));
494 };
495
496 // Test tampering with each limb of q and r
497 for (size_t limb = 0; limb < 4; limb++) {
498 test_incorrect_qr(true, limb); // tamper q
499 test_incorrect_qr(false, limb); // tamper r
500 }
501}
502
503// Verifies that queue_partial_non_native_field_multiplication computes correct intermediate values
504TEST_F(UltraCircuitBuilderNonNative, PartialMultiplicationBasic)
505{
506 const size_t num_iterations = 10;
507 for (size_t i = 0; i < num_iterations; i++) {
509
510 std::array<fr, 4> a_limbs = random_limbs();
511 std::array<fr, 4> b_limbs = random_limbs();
512
513 const auto [lo_0_idx, hi_1_idx] = create_and_queue_partial_multiplication(builder, a_limbs, b_limbs);
514
515 // Verify returned witnesses contain expected values
516 auto [expected_lo_0, expected_hi_1] = compute_expected_partial_products(a_limbs, b_limbs);
517 EXPECT_EQ(builder.get_variable(lo_0_idx), expected_lo_0);
518 EXPECT_EQ(builder.get_variable(hi_1_idx), expected_hi_1);
519
520 // Verify circuit passes after finalization (which calls process_non_native_field_multiplications)
521 EXPECT_TRUE(CircuitChecker::check(builder));
522 }
523}
524
525// Verifies that duplicate partial multiplications are deduplicated
526TEST_F(UltraCircuitBuilderNonNative, PartialMultiplicationDeduplication)
527{
529 std::array<fr, 4> a_limbs = random_limbs();
530 std::array<fr, 4> b_limbs = random_limbs();
531
532 // Add witnesses once (to be reused)
533 const auto a_indices = get_limb_witness_indices(builder, a_limbs);
534 const auto b_indices = get_limb_witness_indices(builder, b_limbs);
535
536 // Queue the same multiplication multiple times using the same witness indices
537 const size_t num_duplicates = 5;
539 for (size_t i = 0; i < num_duplicates; i++) {
540 non_native_partial_multiplication_witnesses<fr> input{ .a = a_indices, .b = b_indices };
541 results.push_back(builder.queue_partial_non_native_field_multiplication(input));
542 }
543
544 // Cache should have all entries before finalization
545 EXPECT_EQ(builder.cached_partial_non_native_field_multiplications.size(), num_duplicates);
546
547 // Verify circuit passes (finalization happens internally via copy)
548 EXPECT_TRUE(CircuitChecker::check(builder));
549
550 // Finalize to check deduplication results
551 builder.finalize_circuit(/*ensure_nonzero=*/true);
552
553 // After finalization, cache should be deduplicated to 1 entry
554 EXPECT_EQ(builder.cached_partial_non_native_field_multiplications.size(), 1U);
555
556 // NNF block size should be the same as single multiplication (deduplication worked)
557 EXPECT_EQ(builder.blocks.nnf.size(), NNF_GATES_PER_PARTIAL_MUL + NNF_ENSURE_NONZERO_PADDING);
558
559 // All results should have the same expected values
560 auto [expected_lo_0, expected_hi_1] = compute_expected_partial_products(a_limbs, b_limbs);
561 for (const auto& result : results) {
562 EXPECT_EQ(builder.get_variable(result[0]), expected_lo_0);
563 EXPECT_EQ(builder.get_variable(result[1]), expected_hi_1);
564 }
565}
566
567// Verifies deduplication correctly handles duplicates that result from an assert_equal
568TEST_F(UltraCircuitBuilderNonNative, PartialMultiplicationDedupeAssertEqual)
569{
571 std::array<fr, 4> a_limbs = random_limbs();
572 std::array<fr, 4> b_limbs = random_limbs();
573
574 // Create two sets of witness indices (different indices, same underlying values)
575 const auto a1_indices = get_limb_witness_indices(builder, a_limbs);
576 const auto b1_indices = get_limb_witness_indices(builder, b_limbs);
577 const auto a2_indices = get_limb_witness_indices(builder, a_limbs);
578 const auto b2_indices = get_limb_witness_indices(builder, b_limbs);
579
580 // Without assert_equal, these would be distinct indices and wouldn't deduplicate.
581 // assert_equal should make them behave as duplicates.
582 for (size_t i = 0; i < 4; i++) {
583 builder.assert_equal(a1_indices[i], a2_indices[i]);
584 builder.assert_equal(b1_indices[i], b2_indices[i]);
585 }
586
587 // Queue partial multiplications using both sets of indices
588 non_native_partial_multiplication_witnesses<fr> input1{ .a = a1_indices, .b = b1_indices };
589 non_native_partial_multiplication_witnesses<fr> input2{ .a = a2_indices, .b = b2_indices };
590 builder.queue_partial_non_native_field_multiplication(input1);
591 builder.queue_partial_non_native_field_multiplication(input2);
592
593 // Before finalization, cache has 2 entries
594 EXPECT_EQ(builder.cached_partial_non_native_field_multiplications.size(), 2U);
595
596 // Verify circuit passes
597 EXPECT_TRUE(CircuitChecker::check(builder));
598
599 // Finalize to check deduplication
600 builder.finalize_circuit(/*ensure_nonzero=*/true);
601
602 // After finalization, should be deduplicated to 1 entry
603 EXPECT_EQ(builder.cached_partial_non_native_field_multiplications.size(), 1U);
604
605 // NNF block size should be the same as single multiplication
606 EXPECT_EQ(builder.blocks.nnf.size(), NNF_GATES_PER_PARTIAL_MUL + NNF_ENSURE_NONZERO_PADDING);
607}
608
609// Verifies multiple distinct partial multiplications all produce correct constraints
610TEST_F(UltraCircuitBuilderNonNative, PartialMultiplicationMultipleDistinct)
611{
613
614 const size_t num_multiplications = 5;
615 std::vector<std::array<fr, 4>> a_values(num_multiplications);
616 std::vector<std::array<fr, 4>> b_values(num_multiplications);
617 std::vector<std::array<uint32_t, 2>> results(num_multiplications);
618
619 // Queue multiple distinct partial multiplications
620 for (size_t i = 0; i < num_multiplications; i++) {
621 a_values[i] = random_limbs();
622 b_values[i] = random_limbs();
623 results[i] = create_and_queue_partial_multiplication(builder, a_values[i], b_values[i]);
624 }
625
626 // All entries should be in the cache
627 EXPECT_EQ(builder.cached_partial_non_native_field_multiplications.size(), num_multiplications);
628
629 // Verify circuit passes
630 EXPECT_TRUE(CircuitChecker::check(builder));
631
632 // Finalize to check gate counts
633 builder.finalize_circuit(/*ensure_nonzero=*/true);
634
635 // After finalization, all distinct entries should remain (no deduplication)
636 EXPECT_EQ(builder.cached_partial_non_native_field_multiplications.size(), num_multiplications);
637
638 // NNF block size should scale with number of multiplications
639 EXPECT_EQ(builder.blocks.nnf.size(), num_multiplications * NNF_GATES_PER_PARTIAL_MUL + NNF_ENSURE_NONZERO_PADDING);
640
641 // Verify each result has correct values
642 for (size_t i = 0; i < num_multiplications; i++) {
643 auto [expected_lo_0, expected_hi_1] = compute_expected_partial_products(a_values[i], b_values[i]);
644 EXPECT_EQ(builder.get_variable(results[i][0]), expected_lo_0);
645 EXPECT_EQ(builder.get_variable(results[i][1]), expected_hi_1);
646 }
647}
648
649// Verifies that finalization with empty cache doesn't cause issues
650TEST_F(UltraCircuitBuilderNonNative, ProcessNonNativeFieldMultiplicationsEmptyCache)
651{
653
654 // Create some non-NNF constraints to ensure the circuit isn't completely empty
657 uint32_t a_idx = builder.add_variable(a);
658 uint32_t b_idx = builder.add_variable(b);
659 uint32_t c_idx = builder.add_variable(a + b);
660 builder.create_add_gate({ a_idx, b_idx, c_idx, fr(1), fr(1), fr(-1), fr(0) });
661
662 // No NNF operations queued
663 EXPECT_EQ(builder.cached_partial_non_native_field_multiplications.size(), 0U);
664
665 // Finalization should succeed without issues
666 EXPECT_TRUE(CircuitChecker::check(builder));
667
668 // Finalize to inspect NNF block
669 builder.finalize_circuit(/*ensure_nonzero=*/true);
670
671 // Cache should still be empty
672 EXPECT_EQ(builder.cached_partial_non_native_field_multiplications.size(), 0U);
673
674 // NNF block should only have ensure_nonzero padding (no actual NNF gates)
675 EXPECT_EQ(builder.blocks.nnf.size(), NNF_ENSURE_NONZERO_PADDING);
676}
#define BB_ASSERT(expression,...)
Definition assert.hpp:67
Test suite for UltraCircuitBuilder non-native field methods.
std::tuple< scaled_witness, scaled_witness, fr > add_simple
static std::pair< uint256_t, uint256_t > compute_quotient_remainder(const uint256_t &a, const uint256_t &b, const uint256_t &modulus)
static std::array< uint32_t, 2 > create_and_queue_partial_multiplication(UltraCircuitBuilder &builder, const std::array< fr, 4 > &a_limbs, const std::array< fr, 4 > &b_limbs)
static std::tuple< add_simple, add_simple, add_simple, add_simple, std::tuple< uint32_t, uint32_t, fr > > create_add_sub_inputs(UltraCircuitBuilder &builder, const AddSubData &data)
static std::array< uint32_t, 2 > create_non_native_multiplication(UltraCircuitBuilder &builder, const uint256_t &a, const uint256_t &b, const uint256_t &q, const uint256_t &r, const uint256_t &modulus)
static std::pair< fr, fr > compute_expected_partial_products(const std::array< fr, 4 > &a, const std::array< fr, 4 > &b)
static std::array< fr, 4 > split_into_limbs(const uint512_t &input)
static std::array< uint32_t, 4 > get_limb_witness_indices(UltraCircuitBuilder &builder, const std::array< fr, 4 > &limbs)
static bool check(const Builder &circuit)
Check the witness satisifies the circuit.
static constexpr size_t DEFAULT_NON_NATIVE_FIELD_LIMB_BITS
constexpr uint64_t get_msb() const
constexpr uintx slice(const uint64_t start, const uint64_t end) const
Definition uintx.hpp:82
AluTraceBuilder builder
Definition alu.test.cpp:124
const std::vector< MemoryValue > data
FF a
FF b
AvmProvingInputs inputs
uintx< uint256_t > uint512_t
Definition uintx.hpp:307
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
field< Bn254FrParams > fr
Definition fr.hpp:174
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
static constexpr uint256_t modulus
static field random_element(numeric::RNG *engine=nullptr) noexcept
TEST_F(UltraCircuitBuilderNonNative, Multiplication)