diff --git a/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_changed.sh b/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_changed.sh index 12a9b4dfba8b..e2a295232b80 100755 --- a/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_changed.sh +++ b/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_changed.sh @@ -13,8 +13,8 @@ cd .. # - Generate a hash for versioning: sha256sum bb-chonk-inputs.tar.gz # - Upload the compressed results: aws s3 cp bb-chonk-inputs.tar.gz s3://aztec-ci-artifacts/protocol/bb-chonk-inputs-[hash(0:8)].tar.gz # Note: In case of the "Test suite failed to run ... Unexpected token 'with' " error, need to run: docker pull aztecprotocol/build:3.0 -pinned_short_hash="bd98634a" -pinned_chonk_inputs_url="https://aztec-ci-artifacts.s3.us-east-2.amazonaws.com/protocol/bb-chonk-inputs-${pinned_short_hash}.tar.gz" +pinned_short_hash="abdb6bae" +pinned_chonk_inputs_url="https://aztec-ci-artifacts.s3.us-east-2.amazonaws.com/protocol/bb-civc-inputs-${pinned_short_hash}.tar.gz" function compress_and_upload { # 1) Compress the results diff --git a/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_format.test.cpp b/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_format.test.cpp index c97cb61ce74e..3053946f77b2 100644 --- a/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_format.test.cpp +++ b/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_format.test.cpp @@ -4,6 +4,7 @@ #include "acir_format.hpp" #include "acir_format_mocks.hpp" +#include "acir_to_constraint_buf.hpp" #include "barretenberg/common/streams.hpp" #include "barretenberg/op_queue/ecc_op_queue.hpp" @@ -341,3 +342,82 @@ TEST_F(AcirFormatTests, TestBigAdd) EXPECT_TRUE(CircuitChecker::check(builder)); } + +// Helper function to convert a uint256_t to a 32-byte vector in big-endian format +std::vector to_bytes_be(uint256_t value) +{ + std::vector bytes(32, 0); + for (size_t i = 0; i < 32; i++) { + bytes[31 - i] = static_cast(value & 0xFF); + value >>= 8; + } + return bytes; +} + +/** + * @brief Test for bug fix where expressions with distinct witnesses requiring more than one width-4 gate + * were incorrectly processed when they initially appeared to fit in width-3 gates + * + * @details This test verifies the fix for a bug in handle_arithmetic where an expression with: + * - 1 mul term using witnesses (w0 * w1) + * - 3 additional linear terms using distinct witnesses (w2, w3, w4) + * + * Such expressions have ≤3 linear combinations and ≤1 mul term, appearing to fit in a + * poly_triple (width-3) gate. However, with all 5 witnesses distinct, serialize_arithmetic_gate + * correctly returns all zeros, indicating it cannot fit in a width-3 gate. + * + * The bug: old code would check if poly_triple was all zeros, and if so, directly add to + * quad_constraints via serialize_mul_quad_gate. But it did this inside the initial + * might_fit_in_polytriple check, so it would never properly go through the mul_quad processing + * logic that handles the general case with >4 witnesses. + * + * The fix: now uses a needs_to_be_parsed_as_mul_quad flag that is set when poly_triple fails, + * and processes through the proper mul_quad logic path, which splits into multiple gates. + * + * Expression: w0 * w1 + w2 + w3 + w4 = 10 + * With witnesses: w0=0, w1=1, w2=2, w3=3, w4=4 + * Evaluation: 0*1 + 2 + 3 + 4 = 9, but we set q_c = -9, so constraint is: 9 - 9 = 0 + */ +TEST_F(AcirFormatTests, TestArithmeticGateWithDistinctWitnessesRegression) +{ + // Create an ACIR expression: w0 * w1 + w2 + w3 + w4 - 9 = 0 + // This has 1 mul term and 3 linear terms with all 5 distinct witnesses (requires multiple width-4 gates) + Acir::Expression expr{ .mul_terms = { std::make_tuple( + to_bytes_be(1), Acir::Witness{ .value = 0 }, Acir::Witness{ .value = 1 }) }, + .linear_combinations = { std::make_tuple(to_bytes_be(1), Acir::Witness{ .value = 2 }), + std::make_tuple(to_bytes_be(1), Acir::Witness{ .value = 3 }), + std::make_tuple(to_bytes_be(1), Acir::Witness{ .value = 4 }) }, + .q_c = to_bytes_be(static_cast(fr(-9))) }; + + Acir::Opcode::AssertZero assert_zero{ .value = expr }; + + // Create an ACIR circuit with this opcode + Acir::Circuit circuit{ + .current_witness_index = 5, + .opcodes = { Acir::Opcode{ .value = assert_zero } }, + .return_values = {}, + }; + + Acir::Program program{ .functions = { circuit } }; + + // Serialize the program to bytes + auto program_bytes = program.bincodeSerialize(); + + // Process through circuit_buf_to_acir_format (this calls handle_arithmetic internally) + AcirFormat constraint_system = circuit_buf_to_acir_format(std::move(program_bytes)); + + // The key assertion: this expression should end up in big_quad_constraints, not poly_triple_constraints + // or single quad_constraints, because it needs 5 witness slots (all distinct) + EXPECT_EQ(constraint_system.poly_triple_constraints.size(), 0); + EXPECT_EQ(constraint_system.quad_constraints.size(), 0); + EXPECT_EQ(constraint_system.big_quad_constraints.size(), 1); + + // Now verify the constraint system with valid witness assignments + // We need: w0 * w1 + w2 + w3 + w4 = 9 + // Use values: w0=0, w1=1, w2=2, w3=3, w4=4, so 0*1 + 2 + 3 + 4 = 9 + WitnessVector witness{ 0, 1, 2, 3, 4 }; + AcirProgram acir_program{ constraint_system, witness }; + auto builder = create_circuit(acir_program); + + EXPECT_TRUE(CircuitChecker::check(builder)); +} diff --git a/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_to_constraint_buf.cpp b/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_to_constraint_buf.cpp index 84ef21dfb8c7..b0304fbf1149 100644 --- a/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_to_constraint_buf.cpp +++ b/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_to_constraint_buf.cpp @@ -459,7 +459,9 @@ std::pair is_assert_equal(Acir::Opcode::AssertZero const& ar void handle_arithmetic(Acir::Opcode::AssertZero const& arg, AcirFormat& af, size_t opcode_index) { // If the expression fits in a polytriple, we use it. - if (arg.value.linear_combinations.size() <= 3 && arg.value.mul_terms.size() <= 1) { + bool might_fit_in_polytriple = arg.value.linear_combinations.size() <= 3 && arg.value.mul_terms.size() <= 1; + bool needs_to_be_parsed_as_mul_quad = !might_fit_in_polytriple; + if (might_fit_in_polytriple) { poly_triple pt = serialize_arithmetic_gate(arg.value); auto assert_equal = is_assert_equal(arg, pt, af); @@ -502,15 +504,14 @@ void handle_arithmetic(Acir::Opcode::AssertZero const& arg, AcirFormat& af, size // gate. This is the case if the linear terms are all distinct witness from the multiplication term. In that // case, the serialize_arithmetic_gate() function will return a poly_triple with all 0's, and we use a width-4 // gate instead. We could probably always use a width-4 gate in fact. - if (pt == poly_triple{ 0, 0, 0, 0, 0, 0, 0, 0 }) { - af.quad_constraints.push_back(serialize_mul_quad_gate(arg.value)); - af.original_opcode_indices.quad_constraints.push_back(opcode_index); - - } else { + if (pt != poly_triple{ 0, 0, 0, 0, 0, 0, 0, 0 }) { af.poly_triple_constraints.push_back(pt); af.original_opcode_indices.poly_triple_constraints.push_back(opcode_index); + } else { + needs_to_be_parsed_as_mul_quad = true; } - } else { + } + if (needs_to_be_parsed_as_mul_quad) { std::vector> mul_quads; // We try to use a single mul_quad gate to represent the expression. if (arg.value.mul_terms.size() <= 1) {