From 68011ccc15ab58f9a352da12d2a29942306df0a0 Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Mon, 5 Jan 2026 13:17:41 +0000 Subject: [PATCH 01/24] Issue 1 --- .../barretenberg/stdlib/hash/poseidon2/sponge/sponge.hpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/sponge/sponge.hpp b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/sponge/sponge.hpp index e657e67f6295..d4907175e696 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/sponge/sponge.hpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/sponge/sponge.hpp @@ -114,9 +114,10 @@ template class FieldSponge { field_t output = sponge.squeeze(); // The final state consists of 4 elements, we only use the first element, which means that the remaining - // 3 witnesses are only used in a single gate. - for (const auto& elem : sponge.state) { - mark_witness_as_used(elem); + // 3 witnesses are only used in a single gate. We only mark these 3 as used, leaving the output unmarked + // so that circuit static analyzer can detect if the caller forgets to use the output. + for (size_t i = 1; i < t; i++) { + mark_witness_as_used(sponge.state[i]); } return output; } From 2dd4197163cdc462936acf258bb46b3ac63d64b9 Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Mon, 5 Jan 2026 13:45:16 +0000 Subject: [PATCH 02/24] Issue 2 --- .../poseidon2/poseidon2_cpp_params.sage | 3 +- .../crypto/poseidon2/poseidon2_params.hpp | 4 ++- .../poseidon2/poseidon2_permutation.hpp | 6 ++-- .../relations/poseidon2_internal_relation.hpp | 36 ++++++++++--------- .../ultra_relation_consistency.test.cpp | 9 ++--- .../stdlib/hash/poseidon2/README.md | 4 +++ .../relations/poseidon2_perm_impl.hpp | 20 ++++++----- 7 files changed, 48 insertions(+), 34 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_cpp_params.sage b/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_cpp_params.sage index 685acd21e35b..56e8d74687c1 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_cpp_params.sage +++ b/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_cpp_params.sage @@ -673,7 +673,8 @@ print(" static constexpr size_t rounds_p = {};".format(R_P_FIXED)) print(" static constexpr size_t sbox_size = {};".format(FIELD_SIZE)) # Efficient partial matrix (diagonal - 1) -print("static constexpr std::array internal_matrix_diagonal = {") +# These are D_i - 1 where D_i are the actual diagonal values of M_I +print("static constexpr std::array internal_matrix_diagonal_minus_one = {") for val in MATRIX_PARTIAL_DIAGONAL_M_1: to_hex(val) print("};") diff --git a/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_params.hpp b/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_params.hpp index d8894d7dc70a..5695e7e45aee 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_params.hpp +++ b/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_params.hpp @@ -21,7 +21,9 @@ struct Poseidon2Bn254ScalarFieldParams { static constexpr size_t rounds_f = 8; static constexpr size_t rounds_p = 56; static constexpr size_t sbox_size = 254; - static constexpr std::array internal_matrix_diagonal = { + // We store D_i - 1, as the algorithm computes: result[i] = internal_matrix_diagonal_minus_one[i] * x[i] + sum + // which equals: (D_i - 1) * x[i] + (x[0] + x[1] + x[2] + x[3]) = D_i * x[i] + (sum of other elements) + static constexpr std::array internal_matrix_diagonal_minus_one = { FF(std::string("0x10dc6e9c006ea38b04b1e03b4bd9490c0d03f98929ca1d7fb56821fd19d3b6e7")), FF(std::string("0x0c28145b6a44df3e0149b3d0a30b3bb599df9756d4dd9b84a86b38cfb45a740b")), FF(std::string("0x00544b8338791518b2c7645a50392798b21f75bb60e3596170067d00141cac15")), diff --git a/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_permutation.hpp b/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_permutation.hpp index 568102e7dee4..7a0f65da2d43 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_permutation.hpp +++ b/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_permutation.hpp @@ -39,7 +39,7 @@ template class Poseidon2Permutation { using MatrixDiagonal = std::array; using RoundConstantsContainer = std::array; - static constexpr MatrixDiagonal internal_matrix_diagonal = Params::internal_matrix_diagonal; + static constexpr MatrixDiagonal internal_matrix_diagonal_minus_one = Params::internal_matrix_diagonal_minus_one; static constexpr RoundConstantsContainer round_constants = Params::round_constants; static constexpr void matrix_multiplication_4x4(State& input) @@ -85,12 +85,14 @@ template class Poseidon2Permutation { static constexpr void matrix_multiplication_internal(State& input) { // for t = 4 + // Computes: result[i] = (D_i - 1) * input[i] + sum = D_i * input[i] + (sum of other elements) + // where D_i are the actual diagonal values and internal_matrix_diagonal_minus_one[i] = D_i - 1 auto sum = input[0]; for (size_t i = 1; i < t; ++i) { sum += input[i]; } for (size_t i = 0; i < t; ++i) { - input[i] *= internal_matrix_diagonal[i]; + input[i] *= internal_matrix_diagonal_minus_one[i]; input[i] += sum; } } diff --git a/barretenberg/cpp/src/barretenberg/relations/poseidon2_internal_relation.hpp b/barretenberg/cpp/src/barretenberg/relations/poseidon2_internal_relation.hpp index 06ec09b41afb..1c96b27d7d11 100644 --- a/barretenberg/cpp/src/barretenberg/relations/poseidon2_internal_relation.hpp +++ b/barretenberg/cpp/src/barretenberg/relations/poseidon2_internal_relation.hpp @@ -21,11 +21,13 @@ template class Poseidon2InternalRelationImpl { 7, // internal poseidon2 round sub-relation for fourth value }; - static constexpr fr D1 = crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal[0]; // decremented by 1 - static constexpr fr D2 = crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal[1]; // decremented by 1 - static constexpr fr D3 = crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal[2]; // decremented by 1 - static constexpr fr D4 = crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal[3]; // decremented by 1 - static constexpr fr D1_plus_1 = fr{ 1 } + D1; + // Internal matrix diagonal values minus one: these are D_i - 1 where D_i are the actual diagonal entries of M_I. + // The internal round computes: v[i] = (D_i - 1) * u[i] + sum = D_i * u[i] + (sum of other elements) + static constexpr fr D1_minus_1 = crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal_minus_one[0]; + static constexpr fr D2_minus_1 = crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal_minus_one[1]; + static constexpr fr D3_minus_1 = crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal_minus_one[2]; + static constexpr fr D4_minus_1 = crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal_minus_one[3]; + static constexpr fr D1 = fr{ 1 } + D1_minus_1; /** * @brief Returns true if the contribution from all subrelations for the provided inputs is identically zero * @@ -43,10 +45,10 @@ template class Poseidon2InternalRelationImpl { * \f[ * M_I = * \begin{bmatrix} - * D_1 + 1 & 1 & 1 & 1 \\ - * 1 & D_2 + 1 & 1 & 1 \\ - * 1 & 1 & D_3 + 1 & 1 \\ - * 1 & 1 & 1 & D_4 + 1 + * D_1 & 1 & 1 & 1 \\ + * 1 & D_2 & 1 & 1 \\ + * 1 & 1 & D_3 & 1 \\ + * 1 & 1 & 1 & D_4 * \end{bmatrix}, * \quad * \text{where } D_i \text{ are the diagonal entries of } M_I. @@ -136,26 +138,26 @@ template class Poseidon2InternalRelationImpl { const auto partial_sum = w_2 + w_3 + w_4; const auto scaled_u1 = u1 * q_pos_by_scaling; - // Row 1: - barycentric_term = scaled_u1 * D1_plus_1; + // Row 1: v_1 = D_1 * u_1 + u_2 + u_3 + u_4 + barycentric_term = scaled_u1 * D1; auto monomial_term = partial_sum - w_1_shift; barycentric_term += Accumulator(monomial_term * q_pos_by_scaling_m); std::get<0>(evals) += barycentric_term; - // Row 2: - auto v2_m = w_2 * D2 + partial_sum - w_2_shift; + // Row 2: v_2 = u_1 + D_2 * u_2 + u_3 + u_4 = u_1 + (D_2 - 1) * u_2 + u_2 + u_3 + u_4 + auto v2_m = w_2 * D2_minus_1 + partial_sum - w_2_shift; barycentric_term = Accumulator(v2_m * q_pos_by_scaling_m); barycentric_term += scaled_u1; std::get<1>(evals) += barycentric_term; - // Row 3: - auto v3_m = w_3 * D3 + partial_sum - w_3_shift; + // Row 3: v_3 = u_1 + u_2 + D_3 * u_3 + u_4 = u_1 + u_2 + (D_3 - 1) * u_3 + u_3 + u_4 + auto v3_m = w_3 * D3_minus_1 + partial_sum - w_3_shift; barycentric_term = Accumulator(v3_m * q_pos_by_scaling_m); barycentric_term += scaled_u1; std::get<2>(evals) += barycentric_term; - // Row 4: - auto v4_m = w_4 * D4 + partial_sum - w_4_shift; + // Row 4: v_4 = u_1 + u_2 + u_3 + D_4 * u_4 = u_1 + u_2 + u_3 + (D_4 - 1) * u_4 + u_4 + auto v4_m = w_4 * D4_minus_1 + partial_sum - w_4_shift; barycentric_term = Accumulator(v4_m * q_pos_by_scaling_m); barycentric_term += scaled_u1; std::get<3>(evals) += barycentric_term; diff --git a/barretenberg/cpp/src/barretenberg/relations/ultra_relation_consistency.test.cpp b/barretenberg/cpp/src/barretenberg/relations/ultra_relation_consistency.test.cpp index 0120ecbf4b13..d1739dc43d6f 100644 --- a/barretenberg/cpp/src/barretenberg/relations/ultra_relation_consistency.test.cpp +++ b/barretenberg/cpp/src/barretenberg/relations/ultra_relation_consistency.test.cpp @@ -640,14 +640,15 @@ TEST_F(UltraRelationConsistency, Poseidon2InternalRelation) u1 *= v1; // multiply with internal matrix + // Uses D_i - 1 values: result[i] = (D_i - 1) * x[i] + sum = D_i * x[i] + (sum of other elements) auto sum = u1 + w_2 + w_3 + w_4; - auto t0 = u1 * crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal[0]; + auto t0 = u1 * crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal_minus_one[0]; t0 += sum; - auto t1 = w_2 * crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal[1]; + auto t1 = w_2 * crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal_minus_one[1]; t1 += sum; - auto t2 = w_3 * crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal[2]; + auto t2 = w_3 * crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal_minus_one[2]; t2 += sum; - auto t3 = w_4 * crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal[3]; + auto t3 = w_4 * crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal_minus_one[3]; t3 += sum; expected_values[0] = q_poseidon2_internal * (t0 - w_1_shift); diff --git a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/README.md b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/README.md index ee780cb08547..1c32fa69269c 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/README.md +++ b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/README.md @@ -109,6 +109,10 @@ M_I = \end{bmatrix} \f] +**Implementation note:** The code stores `internal_matrix_diagonal_minus_one[i] = D_i - 1` (not the actual diagonal values \f$D_i\f$). +This is because the algorithm computes \f$v_i = (D_i - 1) \cdot u_i + \text{sum}\f$ where \f$\text{sum} = u_1 + u_2 + u_3 + u_4\f$, +which equals \f$D_i \cdot u_i + (\text{sum of other elements})\f$. + ### Constants The constants are generated using the sage [script authored by Markus Schofnegger](https://github.com/HorizenLabs/poseidon2/blob/main/poseidon2_rust_params.sage) from Horizen Labs. diff --git a/barretenberg/cpp/src/barretenberg/vm2/optimized/relations/poseidon2_perm_impl.hpp b/barretenberg/cpp/src/barretenberg/vm2/optimized/relations/poseidon2_perm_impl.hpp index a8f92d765794..d1b7454e408b 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/optimized/relations/poseidon2_perm_impl.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2/optimized/relations/poseidon2_perm_impl.hpp @@ -142,14 +142,16 @@ void optimized_poseidon2_permImpl::accumulate(ContainerOverSubrelations& ev state[0] = state[0] * state[0] * state[0] * state[0] * state[0]; // A^5 }; - // The partial round uses the internal matrix diagonal values - const auto internal_matrix_mul = [](std::array& state, - const std::array& internal_matrix_diagonal) { - auto sum = state[0] + state[1] + state[2] + state[3]; - for (size_t i = 0; i < 4; ++i) { - state[i] = state[i] * internal_matrix_diagonal[i] + sum; - } - }; + // The partial round uses the internal matrix diagonal minus one values. + // Computes: result[i] = (D_i - 1) * state[i] + sum = D_i * state[i] + (sum of other elements) + const auto internal_matrix_mul = + [](std::array& state, + const std::array& internal_matrix_diagonal_minus_one) { + auto sum = state[0] + state[1] + state[2] + state[3]; + for (size_t i = 0; i < 4; ++i) { + state[i] = state[i] * internal_matrix_diagonal_minus_one[i] + sum; + } + }; //========================================= // Start Accumulation Relations @@ -265,7 +267,7 @@ void optimized_poseidon2_permImpl::accumulate(ContainerOverSubrelations& ev constexpr size_t relation_offset = START_RELATION_OF_PERM + (i * 4); partial_round_add_constant(state, Poseidon2Params::round_constants[i][0]); partial_round_s_box(state); - internal_matrix_mul(state, Poseidon2Params::internal_matrix_diagonal); + internal_matrix_mul(state, Poseidon2Params::internal_matrix_diagonal_minus_one); // Set the state 0 { using Accumulator = typename std::tuple_element_t; From 1dddf5419145e1400f24f8374bbbc274bab16a05 Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Tue, 6 Jan 2026 17:00:08 +0000 Subject: [PATCH 03/24] Issue 3 --- .../commitment_schemes/ipa/ipa.test.cpp | 12 ++++---- .../commitment_schemes/kzg/kzg.test.cpp | 24 +++++++-------- .../shplonk/shplemini.test.cpp | 24 +++++++-------- .../shplonk/shplonk.test.cpp | 8 ++--- .../small_subgroup_ipa.test.cpp | 10 +++---- .../shplemini.test.cpp | 2 +- .../shplonk.test.cpp | 2 +- .../eccvm/eccvm_transcript.test.cpp | 2 +- .../flavor/ultra_starknet_zk_flavor.hpp | 4 +-- .../src/barretenberg/flavor/ultra_flavor.hpp | 4 +-- .../flavor/ultra_keccak_zk_flavor.hpp | 4 +-- .../barretenberg/flavor/ultra_zk_flavor.hpp | 4 +-- .../row_disabling_polynomial.test.cpp | 10 +++---- .../barretenberg/sumcheck/sumcheck.test.cpp | 12 ++++---- .../barretenberg/transcript/transcript.hpp | 30 ++++++++++--------- .../ultra_honk/mega_transcript.test.cpp | 2 +- .../ultra_honk/ultra_transcript.test.cpp | 2 +- 17 files changed, 79 insertions(+), 77 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/commitment_schemes/ipa/ipa.test.cpp b/barretenberg/cpp/src/barretenberg/commitment_schemes/ipa/ipa.test.cpp index 24699f5f528f..5d5962994c58 100644 --- a/barretenberg/cpp/src/barretenberg/commitment_schemes/ipa/ipa.test.cpp +++ b/barretenberg/cpp/src/barretenberg/commitment_schemes/ipa/ipa.test.cpp @@ -281,7 +281,7 @@ TEST_F(IPATest, ShpleminiIPAWithoutShift) mle_opening_point, ck); - auto prover_transcript = NativeTranscript::prover_init_empty(); + auto prover_transcript = NativeTranscript::test_prover_init_empty(); // Run the full prover PCS protocol: // Compute: @@ -293,7 +293,7 @@ TEST_F(IPATest, ShpleminiIPAWithoutShift) const auto opening_claim = ShplonkProver::prove(ck, prover_opening_claims, prover_transcript); PCS::compute_opening_proof(ck, opening_claim, prover_transcript); - auto verifier_transcript = NativeTranscript::verifier_init_empty(prover_transcript); + auto verifier_transcript = NativeTranscript::test_verifier_init_empty(prover_transcript); std::array padding_indicator_array; std::ranges::fill(padding_indicator_array, Fr{ 1 }); @@ -320,7 +320,7 @@ TEST_F(IPATest, ShpleminiIPAWithShift) /*num_to_be_shifted*/ 1, mle_opening_point, ck); - auto prover_transcript = NativeTranscript::prover_init_empty(); + auto prover_transcript = NativeTranscript::test_prover_init_empty(); // Run the full prover PCS protocol: @@ -332,7 +332,7 @@ TEST_F(IPATest, ShpleminiIPAWithShift) const auto opening_claim = ShplonkProver::prove(ck, prover_opening_claims, prover_transcript); PCS::compute_opening_proof(ck, opening_claim, prover_transcript); - auto verifier_transcript = NativeTranscript::verifier_init_empty(prover_transcript); + auto verifier_transcript = NativeTranscript::test_verifier_init_empty(prover_transcript); std::array padding_indicator_array; std::ranges::fill(padding_indicator_array, Fr{ 1 }); @@ -361,7 +361,7 @@ TEST_F(IPATest, ShpleminiIPAShiftsRemoval) mle_opening_point, ck); - auto prover_transcript = NativeTranscript::prover_init_empty(); + auto prover_transcript = NativeTranscript::test_prover_init_empty(); // Run the full prover PCS protocol: @@ -387,7 +387,7 @@ TEST_F(IPATest, ShpleminiIPAShiftsRemoval) // since commitments to poly2, poly3 and their shifts are the same group elements, we simply combine the scalar // multipliers of commitment2 and commitment3 in one place and remove the entries of the commitments and scalars // vectors corresponding to the "shifted" commitment - auto verifier_transcript = NativeTranscript::verifier_init_empty(prover_transcript); + auto verifier_transcript = NativeTranscript::test_verifier_init_empty(prover_transcript); std::array padding_indicator_array; std::ranges::fill(padding_indicator_array, Fr{ 1 }); diff --git a/barretenberg/cpp/src/barretenberg/commitment_schemes/kzg/kzg.test.cpp b/barretenberg/cpp/src/barretenberg/commitment_schemes/kzg/kzg.test.cpp index 733f141a1c56..72618a877c84 100644 --- a/barretenberg/cpp/src/barretenberg/commitment_schemes/kzg/kzg.test.cpp +++ b/barretenberg/cpp/src/barretenberg/commitment_schemes/kzg/kzg.test.cpp @@ -41,11 +41,11 @@ class KZGTest : public CommitmentTest { auto opening_claim = OpeningClaim{ opening_pair, commitment }; - auto prover_transcript = NativeTranscript::prover_init_empty(); + auto prover_transcript = NativeTranscript::test_prover_init_empty(); PCS::compute_opening_proof(ck, { witness, opening_pair }, prover_transcript); - auto verifier_transcript = NativeTranscript::verifier_init_empty(prover_transcript); + auto verifier_transcript = NativeTranscript::test_verifier_init_empty(prover_transcript); const auto pairing_points = PCS::reduce_verify(opening_claim, verifier_transcript); EXPECT_EQ(vk.pairing_check(pairing_points[0], pairing_points[1]), true); @@ -83,12 +83,12 @@ TEST_F(KZGTest, WrongEvaluationFails) const Fr wrong_evaluation = evaluation + Fr::random_element(); // Prove with the wrong evaluation Commitment commitment = ck.commit(witness); - auto prover_transcript = NativeTranscript::prover_init_empty(); + auto prover_transcript = NativeTranscript::test_prover_init_empty(); PCS::compute_opening_proof(ck, { witness, { challenge, wrong_evaluation } }, prover_transcript); auto opening_claim = OpeningClaim{ { challenge, wrong_evaluation }, commitment }; // Run the verifier - auto verifier_transcript = NativeTranscript::verifier_init_empty(prover_transcript); + auto verifier_transcript = NativeTranscript::test_verifier_init_empty(prover_transcript); auto pairing_point = PCS::reduce_verify(opening_claim, verifier_transcript); // Make sure that the pairing check fails EXPECT_EQ(vk.pairing_check(pairing_point[0], pairing_point[1]), false); @@ -162,11 +162,11 @@ TEST_F(KZGTest, SingleInLagrangeBasis) auto opening_pair = OpeningPair{ challenge, evaluation }; auto opening_claim = OpeningClaim{ opening_pair, commitment }; - auto prover_transcript = NativeTranscript::prover_init_empty(); + auto prover_transcript = NativeTranscript::test_prover_init_empty(); PCS::compute_opening_proof(ck, { witness_polynomial, opening_pair }, prover_transcript); - auto verifier_transcript = NativeTranscript::verifier_init_empty(prover_transcript); + auto verifier_transcript = NativeTranscript::test_verifier_init_empty(prover_transcript); auto pairing_points = PCS::reduce_verify(opening_claim, verifier_transcript); EXPECT_EQ(vk.pairing_check(pairing_points[0], pairing_points[1]), true); @@ -183,7 +183,7 @@ TEST_F(KZGTest, ShpleminiKzgWithShift) mle_opening_point, ck); - auto prover_transcript = NativeTranscript::prover_init_empty(); + auto prover_transcript = NativeTranscript::test_prover_init_empty(); // Run the full prover PCS protocol: @@ -204,7 +204,7 @@ TEST_F(KZGTest, ShpleminiKzgWithShift) // Run the full verifier PCS protocol with genuine opening claims (genuine commitment, genuine evaluation) - auto verifier_transcript = NativeTranscript::verifier_init_empty(prover_transcript); + auto verifier_transcript = NativeTranscript::test_verifier_init_empty(prover_transcript); // Gemini verifier output: // - claim: d+1 commitments to Fold_{r}^(0), Fold_{-r}^(0), Fold^(l), d+1 evaluations a_0_pos, a_l, l = 0:d-1 @@ -238,7 +238,7 @@ TEST_F(KZGTest, ShpleminiKzgWithShiftAndInterleaving) /*num_interleaved*/ 3, /*num_to_be_interleaved*/ 2); - auto prover_transcript = NativeTranscript::prover_init_empty(); + auto prover_transcript = NativeTranscript::test_prover_init_empty(); // Run the full prover PCS protocol: @@ -259,7 +259,7 @@ TEST_F(KZGTest, ShpleminiKzgWithShiftAndInterleaving) // Run the full verifier PCS protocol with genuine opening claims (genuine commitment, genuine evaluation) - auto verifier_transcript = NativeTranscript::verifier_init_empty(prover_transcript); + auto verifier_transcript = NativeTranscript::test_verifier_init_empty(prover_transcript); // Gemini verifier output: // - claim: d+1 commitments to Fold_{r}^(0), Fold_{-r}^(0), Fold^(l), d+1 evaluations a_0_pos, a_l, l = 0:d-1 @@ -294,7 +294,7 @@ TEST_F(KZGTest, ShpleminiKzgShiftsRemoval) mle_opening_point, ck); - auto prover_transcript = NativeTranscript::prover_init_empty(); + auto prover_transcript = NativeTranscript::test_prover_init_empty(); // Run the full prover PCS protocol: @@ -315,7 +315,7 @@ TEST_F(KZGTest, ShpleminiKzgShiftsRemoval) // Run the full verifier PCS protocol with genuine opening claims (genuine commitment, genuine evaluation) - auto verifier_transcript = NativeTranscript::verifier_init_empty(prover_transcript); + auto verifier_transcript = NativeTranscript::test_verifier_init_empty(prover_transcript); // the index of the first commitment to a polynomial to be shifted in the union of unshifted_commitments and // shifted_commitments. in our case, it is poly2 const size_t to_be_shifted_commitments_start = 2; diff --git a/barretenberg/cpp/src/barretenberg/commitment_schemes/shplonk/shplemini.test.cpp b/barretenberg/cpp/src/barretenberg/commitment_schemes/shplonk/shplemini.test.cpp index 9012d6f4c7a3..04dcb4c83eab 100644 --- a/barretenberg/cpp/src/barretenberg/commitment_schemes/shplonk/shplemini.test.cpp +++ b/barretenberg/cpp/src/barretenberg/commitment_schemes/shplonk/shplemini.test.cpp @@ -278,7 +278,7 @@ TYPED_TEST(ShpleminiTest, ShpleminiZKNoSumcheckOpenings) using CK = typename TypeParam::CommitmentKey; // Initialize transcript and commitment key - auto prover_transcript = TypeParam::Transcript::prover_init_empty(); + auto prover_transcript = TypeParam::Transcript::test_prover_init_empty(); // SmallSubgroupIPAProver requires at least CURVE::SUBGROUP_SIZE + 3 elements in the ck. static constexpr size_t log_subgroup_size = static_cast(numeric::get_msb(Curve::SUBGROUP_SIZE)); @@ -323,7 +323,7 @@ TYPED_TEST(ShpleminiTest, ShpleminiZKNoSumcheckOpenings) } // Initialize verifier's transcript - auto verifier_transcript = NativeTranscript::verifier_init_empty(prover_transcript); + auto verifier_transcript = NativeTranscript::test_verifier_init_empty(prover_transcript); // Start populating Verifier's array of Libra commitments std::array libra_commitments = {}; @@ -392,7 +392,7 @@ TYPED_TEST(ShpleminiTest, ShpleminiZKWithSumcheckOpenings) // Generate Sumcheck challenge std::vector challenge = this->random_evaluation_point(this->log_n); - auto prover_transcript = TypeParam::Transcript::prover_init_empty(); + auto prover_transcript = TypeParam::Transcript::test_prover_init_empty(); // Generate masking polynomials for Sumcheck Round Univariates ZKSumcheckData zk_sumcheck_data(this->log_n, prover_transcript, ck); @@ -431,7 +431,7 @@ TYPED_TEST(ShpleminiTest, ShpleminiZKWithSumcheckOpenings) } // Initialize verifier's transcript - auto verifier_transcript = NativeTranscript::verifier_init_empty(prover_transcript); + auto verifier_transcript = NativeTranscript::test_verifier_init_empty(prover_transcript); std::array libra_commitments = {}; libra_commitments[0] = @@ -518,7 +518,7 @@ TYPED_TEST(ShpleminiTest, HighDegreeAttackAccept) MockClaimGenerator mock_claims( this->n, std::vector{ std::move(poly) }, std::vector{ claimed_multilinear_eval }, ck); - auto prover_transcript = NativeTranscript::prover_init_empty(); + auto prover_transcript = NativeTranscript::test_prover_init_empty(); // Run Shplemini prover const auto opening_claim = @@ -532,7 +532,7 @@ TYPED_TEST(ShpleminiTest, HighDegreeAttackAccept) } // Verifier side - auto verifier_transcript = NativeTranscript::verifier_init_empty(prover_transcript); + auto verifier_transcript = NativeTranscript::test_verifier_init_empty(prover_transcript); std::vector padding_indicator_array(small_log_n, Fr{ 1 }); @@ -585,7 +585,7 @@ TYPED_TEST(ShpleminiTest, HighDegreeAttackReject) MockClaimGenerator mock_claims( big_n, std::vector{ std::move(poly) }, std::vector{ claimed_multilinear_eval }, ck); - auto prover_transcript = NativeTranscript::prover_init_empty(); + auto prover_transcript = NativeTranscript::test_prover_init_empty(); // Run Shplemini prover const auto opening_claim = ShpleminiProver::prove(big_n, mock_claims.polynomial_batcher, u, ck, prover_transcript); @@ -598,7 +598,7 @@ TYPED_TEST(ShpleminiTest, HighDegreeAttackReject) } // Verifier side - auto verifier_transcript = NativeTranscript::verifier_init_empty(prover_transcript); + auto verifier_transcript = NativeTranscript::test_verifier_init_empty(prover_transcript); std::vector padding_indicator_array(small_log_n, Fr{ 1 }); @@ -637,7 +637,7 @@ TYPED_TEST(ShpleminiTest, LibraConsistencyCheckFailsOnCorruptedEvaluation) using CK = typename TypeParam::CommitmentKey; // Initialize transcript and commitment key - auto prover_transcript = TypeParam::Transcript::prover_init_empty(); + auto prover_transcript = TypeParam::Transcript::test_prover_init_empty(); // SmallSubgroupIPAProver requires at least CURVE::SUBGROUP_SIZE + 3 elements in the ck. static constexpr size_t log_subgroup_size = static_cast(numeric::get_msb(Curve::SUBGROUP_SIZE)); @@ -685,7 +685,7 @@ TYPED_TEST(ShpleminiTest, LibraConsistencyCheckFailsOnCorruptedEvaluation) } // Initialize verifier's transcript - auto verifier_transcript = NativeTranscript::verifier_init_empty(prover_transcript); + auto verifier_transcript = NativeTranscript::test_verifier_init_empty(prover_transcript); // Start populating Verifier's array of Libra commitments std::array libra_commitments = {}; @@ -742,7 +742,7 @@ void run_libra_tampering_test(ShpleminiTest* test, using Commitment = typename Curve::AffineElement; using CK = typename TypeParam::CommitmentKey; - auto prover_transcript = TypeParam::Transcript::prover_init_empty(); + auto prover_transcript = TypeParam::Transcript::test_prover_init_empty(); static constexpr size_t log_subgroup_size = static_cast(numeric::get_msb(Curve::SUBGROUP_SIZE)); CK ck = create_commitment_key(std::max(test->n, 1ULL << (log_subgroup_size + 1))); @@ -777,7 +777,7 @@ void run_libra_tampering_test(ShpleminiTest* test, KZG::compute_opening_proof(test->ck(), opening_claim, prover_transcript); } - auto verifier_transcript = NativeTranscript::verifier_init_empty(prover_transcript); + auto verifier_transcript = NativeTranscript::test_verifier_init_empty(prover_transcript); std::array libra_commitments = {}; libra_commitments[0] = diff --git a/barretenberg/cpp/src/barretenberg/commitment_schemes/shplonk/shplonk.test.cpp b/barretenberg/cpp/src/barretenberg/commitment_schemes/shplonk/shplonk.test.cpp index 0eb8a8636ca6..bdf7d8c453e3 100644 --- a/barretenberg/cpp/src/barretenberg/commitment_schemes/shplonk/shplonk.test.cpp +++ b/barretenberg/cpp/src/barretenberg/commitment_schemes/shplonk/shplonk.test.cpp @@ -24,7 +24,7 @@ TYPED_TEST(ShplonkTest, ShplonkSimple) using ShplonkProver = ShplonkProver_; using ShplonkVerifier = ShplonkVerifier_; - auto prover_transcript = NativeTranscript::prover_init_empty(); + auto prover_transcript = NativeTranscript::test_prover_init_empty(); // Generate two random (unrelated) polynomials of two different sizes, as well as their evaluations at a (single // but different) random point and their commitments. @@ -37,7 +37,7 @@ TYPED_TEST(ShplonkTest, ShplonkSimple) this->verify_opening_pair(batched_opening_claim.opening_pair, batched_opening_claim.polynomial); // Initialize verifier transcript from prover transcript - auto verifier_transcript = NativeTranscript::verifier_init_empty(prover_transcript); + auto verifier_transcript = NativeTranscript::test_verifier_init_empty(prover_transcript); // Execute the shplonk verifier functionality const auto batched_verifier_claim = ShplonkVerifier::reduce_verification( @@ -53,7 +53,7 @@ TYPED_TEST(ShplonkTest, ExportBatchClaimAndVerify) using ShplonkProver = ShplonkProver_; using ShplonkVerifier = ShplonkVerifier_; - auto prover_transcript = NativeTranscript::prover_init_empty(); + auto prover_transcript = NativeTranscript::test_prover_init_empty(); // Generate two random (unrelated) polynomials of two different sizes and a random linear combinations auto setup = this->generate_claim_data({ MAX_POLY_DEGREE, MAX_POLY_DEGREE / 2 }); @@ -72,7 +72,7 @@ TYPED_TEST(ShplonkTest, ExportBatchClaimAndVerify) } // Shplonk verification - auto verifier_transcript = NativeTranscript::verifier_init_empty(prover_transcript); + auto verifier_transcript = NativeTranscript::test_verifier_init_empty(prover_transcript); // Execute the shplonk verifier functionality auto verifier_opening_claims = ClaimData::verifier_opening_claims(setup); diff --git a/barretenberg/cpp/src/barretenberg/commitment_schemes/small_subgroup_ipa/small_subgroup_ipa.test.cpp b/barretenberg/cpp/src/barretenberg/commitment_schemes/small_subgroup_ipa/small_subgroup_ipa.test.cpp index e5d8290033be..d05339dca8ed 100644 --- a/barretenberg/cpp/src/barretenberg/commitment_schemes/small_subgroup_ipa/small_subgroup_ipa.test.cpp +++ b/barretenberg/cpp/src/barretenberg/commitment_schemes/small_subgroup_ipa/small_subgroup_ipa.test.cpp @@ -62,7 +62,7 @@ TYPED_TEST(SmallSubgroupIPATest, ProverComputationsCorrectness) static constexpr size_t log_subgroup_size = static_cast(numeric::get_msb(SUBGROUP_SIZE)); CK ck = create_commitment_key(std::max(this->circuit_size, 1ULL << (log_subgroup_size + 1))); - auto prover_transcript = TypeParam::Transcript::prover_init_empty(); + auto prover_transcript = TypeParam::Transcript::test_prover_init_empty(); ZKData zk_sumcheck_data(this->log_circuit_size, prover_transcript, ck); std::vector multivariate_challenge = this->generate_random_vector(this->log_circuit_size); @@ -179,7 +179,7 @@ TYPED_TEST(SmallSubgroupIPATest, LibraEvaluationsConsistency) using ZKData = ZKSumcheckData; using CK = typename TypeParam::CommitmentKey; - auto prover_transcript = TypeParam::Transcript::prover_init_empty(); + auto prover_transcript = TypeParam::Transcript::test_prover_init_empty(); // SmallSubgroupIPAProver requires at least CURVE::SUBGROUP_SIZE + 3 elements in the ck. static constexpr size_t log_subgroup_size = static_cast(numeric::get_msb(Curve::SUBGROUP_SIZE)); @@ -216,7 +216,7 @@ TYPED_TEST(SmallSubgroupIPATest, LibraEvaluationsConsistencyFailure) using ZKData = ZKSumcheckData; using CK = typename TypeParam::CommitmentKey; - auto prover_transcript = TypeParam::Transcript::prover_init_empty(); + auto prover_transcript = TypeParam::Transcript::test_prover_init_empty(); // SmallSubgroupIPAProver requires at least CURVE::SUBGROUP_SIZE + 3 elements in the ck. static constexpr size_t log_subgroup_size = static_cast(numeric::get_msb(Curve::SUBGROUP_SIZE)); @@ -264,7 +264,7 @@ TYPED_TEST(SmallSubgroupIPATest, TranslationMaskingTermConsistency) using Prover = SmallSubgroupIPAProver; using CK = typename TypeParam::CommitmentKey; - auto prover_transcript = TypeParam::Transcript::prover_init_empty(); + auto prover_transcript = TypeParam::Transcript::test_prover_init_empty(); // Must satisfy num_wires * NUM_DISABLED_ROWS_IN_SUMCHECK + 1 < SUBGROUP_SIZE const size_t num_wires = 5; @@ -316,7 +316,7 @@ TYPED_TEST(SmallSubgroupIPATest, TranslationMaskingTermConsistencyFailure) using Prover = SmallSubgroupIPAProver; using CK = typename TypeParam::CommitmentKey; - auto prover_transcript = TypeParam::Transcript::prover_init_empty(); + auto prover_transcript = TypeParam::Transcript::test_prover_init_empty(); // Must satisfy num_wires * NUM_DISABLED_ROWS_IN_SUMCHECK + 1 < SUBGROUP_SIZE const size_t num_wires = 5; diff --git a/barretenberg/cpp/src/barretenberg/commitment_schemes_recursion/shplemini.test.cpp b/barretenberg/cpp/src/barretenberg/commitment_schemes_recursion/shplemini.test.cpp index 9bc40033bf15..ad584af3977c 100644 --- a/barretenberg/cpp/src/barretenberg/commitment_schemes_recursion/shplemini.test.cpp +++ b/barretenberg/cpp/src/barretenberg/commitment_schemes_recursion/shplemini.test.cpp @@ -118,7 +118,7 @@ template class ShpleminiRecursionTest : public CommitmentTest u_challenge = random_challenge_vector(log_circuit_size); MockClaimGen mock_claims(N, num_polys, num_shifted, u_challenge, commitment_key); - auto prover_transcript = NativeTranscript::prover_init_empty(); + auto prover_transcript = NativeTranscript::test_prover_init_empty(); // Initialize polys outside of `if` as they are used inside RefVector ClaimBatcher members. Polynomial squashed_unshifted(N); Polynomial squashed_shifted(Polynomial::shiftable(N)); diff --git a/barretenberg/cpp/src/barretenberg/commitment_schemes_recursion/shplonk.test.cpp b/barretenberg/cpp/src/barretenberg/commitment_schemes_recursion/shplonk.test.cpp index bd4f723c573f..ff6d6b74b6a0 100644 --- a/barretenberg/cpp/src/barretenberg/commitment_schemes_recursion/shplonk.test.cpp +++ b/barretenberg/cpp/src/barretenberg/commitment_schemes_recursion/shplonk.test.cpp @@ -53,7 +53,7 @@ TYPED_TEST(ShplonkRecursionTest, Simple) using StdlibProof = stdlib::Proof; // Prover transcript - auto prover_transcript = NativeTranscript::prover_init_empty(); + auto prover_transcript = NativeTranscript::test_prover_init_empty(); // Test data auto setup = this->generate_claim_data({ MAX_POLY_DEGREE, MAX_POLY_DEGREE / 2 }); diff --git a/barretenberg/cpp/src/barretenberg/eccvm/eccvm_transcript.test.cpp b/barretenberg/cpp/src/barretenberg/eccvm/eccvm_transcript.test.cpp index 673e38fc7087..5836c59dd019 100644 --- a/barretenberg/cpp/src/barretenberg/eccvm/eccvm_transcript.test.cpp +++ b/barretenberg/cpp/src/barretenberg/eccvm/eccvm_transcript.test.cpp @@ -391,7 +391,7 @@ TEST_F(ECCVMTranscriptTests, VerifierManifestConsistency) TEST_F(ECCVMTranscriptTests, ChallengeGenerationTest) { // initialized with random value sent to verifier - auto transcript = Flavor::Transcript::prover_init_empty(); + auto transcript = Flavor::Transcript::test_prover_init_empty(); // test a bunch of challenges std::vector challenge_labels{ "a", "b", "c", "d", "e", "f" }; auto challenges = transcript->template get_challenges(challenge_labels); diff --git a/barretenberg/cpp/src/barretenberg/ext/starknet/flavor/ultra_starknet_zk_flavor.hpp b/barretenberg/cpp/src/barretenberg/ext/starknet/flavor/ultra_starknet_zk_flavor.hpp index 31ed20ad5d17..56a958434f4f 100644 --- a/barretenberg/cpp/src/barretenberg/ext/starknet/flavor/ultra_starknet_zk_flavor.hpp +++ b/barretenberg/cpp/src/barretenberg/ext/starknet/flavor/ultra_starknet_zk_flavor.hpp @@ -35,13 +35,13 @@ class UltraStarknetZKFlavor : public UltraKeccakZKFlavor { static std::shared_ptr prover_init_empty() { - auto transcript = Base::prover_init_empty(); + auto transcript = Base::test_prover_init_empty(); return std::static_pointer_cast(transcript); }; static std::shared_ptr verifier_init_empty(const std::shared_ptr& transcript) { - auto verifier_transcript = Base::verifier_init_empty(transcript); + auto verifier_transcript = Base::test_verifier_init_empty(transcript); return std::static_pointer_cast(verifier_transcript); }; diff --git a/barretenberg/cpp/src/barretenberg/flavor/ultra_flavor.hpp b/barretenberg/cpp/src/barretenberg/flavor/ultra_flavor.hpp index cf8211cadc3c..d744ef47cf30 100644 --- a/barretenberg/cpp/src/barretenberg/flavor/ultra_flavor.hpp +++ b/barretenberg/cpp/src/barretenberg/flavor/ultra_flavor.hpp @@ -394,13 +394,13 @@ class UltraFlavor { static std::shared_ptr prover_init_empty() { - auto transcript = Base::prover_init_empty(); + auto transcript = Base::test_prover_init_empty(); return std::static_pointer_cast(transcript); }; static std::shared_ptr verifier_init_empty(const std::shared_ptr& transcript) { - auto verifier_transcript = Base::verifier_init_empty(transcript); + auto verifier_transcript = Base::test_verifier_init_empty(transcript); return std::static_pointer_cast(verifier_transcript); }; diff --git a/barretenberg/cpp/src/barretenberg/flavor/ultra_keccak_zk_flavor.hpp b/barretenberg/cpp/src/barretenberg/flavor/ultra_keccak_zk_flavor.hpp index 3d99d83631f1..8a6b660da657 100644 --- a/barretenberg/cpp/src/barretenberg/flavor/ultra_keccak_zk_flavor.hpp +++ b/barretenberg/cpp/src/barretenberg/flavor/ultra_keccak_zk_flavor.hpp @@ -106,13 +106,13 @@ class UltraKeccakZKFlavor : public UltraKeccakFlavor { static std::shared_ptr prover_init_empty() { - auto transcript = Base::prover_init_empty(); + auto transcript = Base::test_prover_init_empty(); return std::static_pointer_cast(transcript); }; static std::shared_ptr verifier_init_empty(const std::shared_ptr& transcript) { - auto verifier_transcript = Base::verifier_init_empty(transcript); + auto verifier_transcript = Base::test_verifier_init_empty(transcript); return std::static_pointer_cast(verifier_transcript); }; diff --git a/barretenberg/cpp/src/barretenberg/flavor/ultra_zk_flavor.hpp b/barretenberg/cpp/src/barretenberg/flavor/ultra_zk_flavor.hpp index bb3de293daab..3e58d59e5de1 100644 --- a/barretenberg/cpp/src/barretenberg/flavor/ultra_zk_flavor.hpp +++ b/barretenberg/cpp/src/barretenberg/flavor/ultra_zk_flavor.hpp @@ -115,13 +115,13 @@ class UltraZKFlavor : public UltraFlavor { static std::shared_ptr prover_init_empty() { - auto transcript = Base::prover_init_empty(); + auto transcript = Base::test_prover_init_empty(); return std::static_pointer_cast(transcript); }; static std::shared_ptr verifier_init_empty(const std::shared_ptr& transcript) { - auto verifier_transcript = Base::verifier_init_empty(transcript); + auto verifier_transcript = Base::test_verifier_init_empty(transcript); return std::static_pointer_cast(verifier_transcript); }; diff --git a/barretenberg/cpp/src/barretenberg/sumcheck/row_disabling_polynomial.test.cpp b/barretenberg/cpp/src/barretenberg/sumcheck/row_disabling_polynomial.test.cpp index c2160bb57146..fe9c311a48d6 100644 --- a/barretenberg/cpp/src/barretenberg/sumcheck/row_disabling_polynomial.test.cpp +++ b/barretenberg/cpp/src/barretenberg/sumcheck/row_disabling_polynomial.test.cpp @@ -33,7 +33,7 @@ template class RowDisablingPolynomialTest : public ::testing:: */ static SumcheckSetup create_sumcheck_setup(size_t multivariate_d) { - auto transcript = Flavor::Transcript::prover_init_empty(); + auto transcript = Flavor::Transcript::test_prover_init_empty(); FF alpha = transcript->template get_challenge("Sumcheck:alpha"); std::vector gate_challenges(multivariate_d); @@ -157,7 +157,7 @@ TEST(RowDisablingPolynomial, MasksRandomPaddingRows) RelationParameters relation_parameters{}; // Prover: Run ZK Sumcheck with RowDisablingPolynomial - auto prover_transcript = Flavor::Transcript::prover_init_empty(); + auto prover_transcript = Flavor::Transcript::test_prover_init_empty(); FF prover_alpha = prover_transcript->template get_challenge("Sumcheck:alpha"); std::vector prover_gate_challenges(virtual_log_n); @@ -179,7 +179,7 @@ TEST(RowDisablingPolynomial, MasksRandomPaddingRows) SumcheckOutput prover_output = sumcheck_prover.prove(zk_sumcheck_data); // Verifier: Verify with padding_indicator_array - auto verifier_transcript = Flavor::Transcript::verifier_init_empty(prover_transcript); + auto verifier_transcript = Flavor::Transcript::test_verifier_init_empty(prover_transcript); // Extract challenges from verifier transcript (must match prover) FF verifier_alpha = verifier_transcript->template get_challenge("Sumcheck:alpha"); @@ -403,7 +403,7 @@ TEST(RowDisablingPolynomial, FailsWithoutRowDisabling) // Set relation parameters (SumcheckTestFlavor doesn't need beta/gamma) RelationParameters relation_parameters{}; - auto prover_transcript = Flavor::Transcript::prover_init_empty(); + auto prover_transcript = Flavor::Transcript::test_prover_init_empty(); FF prover_alpha = prover_transcript->template get_challenge("Sumcheck:alpha"); std::vector prover_gate_challenges(virtual_log_n); @@ -423,7 +423,7 @@ TEST(RowDisablingPolynomial, FailsWithoutRowDisabling) // Non-ZK Sumcheck (no RowDisablingPolynomial) SumcheckOutput prover_output = sumcheck_prover.prove(); - auto verifier_transcript = Flavor::Transcript::verifier_init_empty(prover_transcript); + auto verifier_transcript = Flavor::Transcript::test_verifier_init_empty(prover_transcript); // Extract challenges from verifier transcript (must match prover) FF verifier_alpha = verifier_transcript->template get_challenge("Sumcheck:alpha"); diff --git a/barretenberg/cpp/src/barretenberg/sumcheck/sumcheck.test.cpp b/barretenberg/cpp/src/barretenberg/sumcheck/sumcheck.test.cpp index 6a733735abf2..6865cac2d23b 100644 --- a/barretenberg/cpp/src/barretenberg/sumcheck/sumcheck.test.cpp +++ b/barretenberg/cpp/src/barretenberg/sumcheck/sumcheck.test.cpp @@ -131,7 +131,7 @@ template class SumcheckTests : public ::testing::Test { } auto full_polynomials = construct_ultra_full_polynomials(random_polynomials); - auto transcript = Flavor::Transcript::prover_init_empty(); + auto transcript = Flavor::Transcript::test_prover_init_empty(); FF alpha = transcript->template get_challenge("Sumcheck:alpha"); @@ -204,7 +204,7 @@ template class SumcheckTests : public ::testing::Test { } auto full_polynomials = construct_ultra_full_polynomials(random_polynomials); - auto transcript = Flavor::Transcript::prover_init_empty(); + auto transcript = Flavor::Transcript::test_prover_init_empty(); FF alpha = transcript->template get_challenge("Sumcheck:alpha"); @@ -255,7 +255,7 @@ template class SumcheckTests : public ::testing::Test { // SumcheckTestFlavor doesn't need complex relation parameters (no permutation, lookup, etc.) RelationParameters relation_parameters{}; - auto prover_transcript = Flavor::Transcript::prover_init_empty(); + auto prover_transcript = Flavor::Transcript::test_prover_init_empty(); FF prover_alpha = prover_transcript->template get_challenge("Sumcheck:alpha"); std::vector prover_gate_challenges(virtual_log_n); @@ -278,7 +278,7 @@ template class SumcheckTests : public ::testing::Test { output = sumcheck_prover.prove(); } - auto verifier_transcript = Flavor::Transcript::verifier_init_empty(prover_transcript); + auto verifier_transcript = Flavor::Transcript::test_verifier_init_empty(prover_transcript); FF verifier_alpha = verifier_transcript->template get_challenge("Sumcheck:alpha"); @@ -321,7 +321,7 @@ template class SumcheckTests : public ::testing::Test { // SumcheckTestFlavor doesn't need complex relation parameters RelationParameters relation_parameters{}; - auto prover_transcript = Flavor::Transcript::prover_init_empty(); + auto prover_transcript = Flavor::Transcript::test_prover_init_empty(); FF prover_alpha = prover_transcript->template get_challenge("Sumcheck:alpha"); auto prover_gate_challenges = @@ -344,7 +344,7 @@ template class SumcheckTests : public ::testing::Test { output = sumcheck_prover.prove(); } - auto verifier_transcript = Flavor::Transcript::verifier_init_empty(prover_transcript); + auto verifier_transcript = Flavor::Transcript::test_verifier_init_empty(prover_transcript); FF verifier_alpha = verifier_transcript->template get_challenge("Sumcheck:alpha"); diff --git a/barretenberg/cpp/src/barretenberg/transcript/transcript.hpp b/barretenberg/cpp/src/barretenberg/transcript/transcript.hpp index 81aacd23c8cf..020322d46d36 100644 --- a/barretenberg/cpp/src/barretenberg/transcript/transcript.hpp +++ b/barretenberg/cpp/src/barretenberg/transcript/transcript.hpp @@ -429,13 +429,26 @@ template class BaseTranscript { { return Codec::template deserialize_from_fields(frs); } + + [[nodiscard]] TranscriptManifest get_manifest() const { return manifest; }; + + void print() + { + if (!use_manifest) { + info("Warning: manifest is not enabled!"); + } + manifest.print(); + } + + // Test-specific utils + /** * @brief For testing: initializes transcript with some arbitrary data so that a challenge can be generated * after initialization. Only intended to be used by Prover. * * @return BaseTranscript */ - static std::shared_ptr prover_init_empty() + static std::shared_ptr test_prover_init_empty() { auto transcript = std::make_shared(); constexpr uint32_t init{ 42 }; // arbitrary @@ -445,28 +458,17 @@ template class BaseTranscript { /** * @brief For testing: initializes transcript based on proof data then receives junk data produced by - * BaseTranscript::prover_init_empty(). Only intended to be used by Verifier. + * BaseTranscript::test_prover_init_empty(). Only intended to be used by Verifier. * * @param transcript * @return BaseTranscript */ - static std::shared_ptr verifier_init_empty(const std::shared_ptr& transcript) + static std::shared_ptr test_verifier_init_empty(const std::shared_ptr& transcript) { auto verifier_transcript = std::make_shared(transcript->proof_data); [[maybe_unused]] auto _ = verifier_transcript->template receive_from_prover("Init"); return verifier_transcript; }; - [[nodiscard]] TranscriptManifest get_manifest() const { return manifest; }; - - void print() - { - if (!use_manifest) { - info("Warning: manifest is not enabled!"); - } - manifest.print(); - } - - // Test-specific utils /** * @brief Test utility: Set proof parsing state for export after deserialization diff --git a/barretenberg/cpp/src/barretenberg/ultra_honk/mega_transcript.test.cpp b/barretenberg/cpp/src/barretenberg/ultra_honk/mega_transcript.test.cpp index 6221c1594eea..7dde0281d2a9 100644 --- a/barretenberg/cpp/src/barretenberg/ultra_honk/mega_transcript.test.cpp +++ b/barretenberg/cpp/src/barretenberg/ultra_honk/mega_transcript.test.cpp @@ -269,7 +269,7 @@ TYPED_TEST(MegaTranscriptTests, ChallengeGenerationTest) using Flavor = TypeParam; using FF = Flavor::FF; // initialized with random value sent to verifier - auto transcript = Flavor::Transcript::prover_init_empty(); + auto transcript = Flavor::Transcript::test_prover_init_empty(); // test a bunch of challenges std::vector challenge_labels{ "a", "b", "c", "d", "e", "f" }; auto challenges = transcript->template get_challenges(challenge_labels); diff --git a/barretenberg/cpp/src/barretenberg/ultra_honk/ultra_transcript.test.cpp b/barretenberg/cpp/src/barretenberg/ultra_honk/ultra_transcript.test.cpp index 0795df4cbbba..191447c585ef 100644 --- a/barretenberg/cpp/src/barretenberg/ultra_honk/ultra_transcript.test.cpp +++ b/barretenberg/cpp/src/barretenberg/ultra_honk/ultra_transcript.test.cpp @@ -292,7 +292,7 @@ TYPED_TEST(UltraTranscriptTests, ChallengeGenerationTest) using Flavor = TypeParam; using FF = Flavor::FF; // initialized with random value sent to verifier - auto transcript = TypeParam::Transcript::prover_init_empty(); + auto transcript = TypeParam::Transcript::test_prover_init_empty(); // test a bunch of challenges std::vector challenge_labels{ "a", "b", "c", "d", "e", "f" }; auto challenges = transcript->template get_challenges(challenge_labels); From ced1a8823fe349bccb093faae5c313f845311bb9 Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Tue, 6 Jan 2026 17:15:55 +0000 Subject: [PATCH 04/24] Issue 4 --- .../cpp/src/barretenberg/transcript/README.md | 44 +++++++++--------- .../barretenberg/transcript/transcript.hpp | 46 +++++++++---------- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/transcript/README.md b/barretenberg/cpp/src/barretenberg/transcript/README.md index 751ef1192114..f19870db7aea 100644 --- a/barretenberg/cpp/src/barretenberg/transcript/README.md +++ b/barretenberg/cpp/src/barretenberg/transcript/README.md @@ -76,34 +76,34 @@ The mode is set at compile-time. ### Phase and Round Tracking -The transcript maintains state for correct tag assignment: +The transcript maintains state for correct tag assignment and manifest tracking: ```cpp -size_t round_index = 0; // Current protocol round (for origin tags) -bool reception_phase = true; // Currently receiving data (true) or generating challenges (false) +size_t round_index = 0; // Current protocol round (for both tags and manifest) +bool challenge_generation_phase = false; // Currently generating challenges (true) vs proof processing (false) ``` #### Phase Transitions -**When adding/receiving data** (`send_to_verifier`, `receive_from_prover`, `add_to_hash_buffer`): +**When processing proof data** (`send_to_verifier`, `receive_from_prover`, `add_to_hash_buffer`): ```cpp -if (!reception_phase) { - reception_phase = true; // Switch back to reception - round_index++; // Move to next round +if (challenge_generation_phase) { + challenge_generation_phase = false; // Switch to proof processing phase + round_index++; // Move to next round } // Tag assigned: OriginTag(transcript_index, round_index, is_submitted=true) ``` **When generating challenges** (`get_challenge`, `get_challenges`): ```cpp -if (reception_phase) { - reception_phase = false; // Switch to challenge generation +if (!challenge_generation_phase) { + challenge_generation_phase = true; // Switch to challenge generation phase // round_index stays the same - challenges belong to the current round } // Tag assigned: OriginTag(transcript_index, round_index, is_submitted=false) ``` -**Key insight**: `round_index` increments when returning FROM challenge generation TO data reception, not when generating challenges. This ensures challenges and the round's data share the same round number. +**Key insight**: `round_index` increments when entering a new round of proof processing (transitioning FROM challenge generation TO proof processing), not when generating challenges. This ensures challenges and the round's submitted data share the same round number, keeping manifest and tag information in sync. --- @@ -217,7 +217,7 @@ auto alpha = transcript->get_challenge("alpha"); **Behavior**: - Hashes `previous_challenge || current_round_data` - Clears `current_round_data` -- Increments `round_number` +- Sets `challenge_generation_phase = true` (no round increment) - Assigns origin tag with `is_submitted=false` #### `get_challenges(std::span labels) -> std::vector` @@ -510,7 +510,7 @@ The following diagram illustrates the complete flow of the Poseidon2 in-circuit │ transcript_id = 42 (from global atomic counter) │ └─────────────────────────────────────────────────────────────────────────────┘ -ROUND 0 - PREAMBLE (reception_phase=true, round_index=0) +ROUND 0 - PREAMBLE (challenge_generation_phase=false, round_index=0) ═══════════════════════════════════════════════════════ ** VK hash MUST be the first element added to transcript ** @@ -542,14 +542,14 @@ ROUND 0 - PREAMBLE (reception_phase=true, round_index=0) │ receive_from_prover("w_o") │──┘ └──────────────────────────────────┘ │ - │ Phase: reception → challenge generation + │ Phase: proof processing → challenge generation ▼ ┌──────────────────────────────────┐ │ get_challenges("eta", "eta_two", │──► 1. Sanitize: FREE_WITNESS → CONSTANT (allow hashing) │ "eta_three") │ 2. Hash: c₀ = Poseidon2(current_round_data) └──────────────────────────────────┘ 3. Split to [127-bit, 127-bit] x3 │ 4. Clear current_round_data - │ 5. Set reception_phase = false + │ 5. Set challenge_generation_phase = true ▼ 6. Assign tag: OriginTag(42, 0, is_submitted=false) eta, eta_two, eta_three round_provenance = 0x0001...0000 (bit 0 upper) tag = OriginTag(42, 0, false) @@ -557,12 +557,12 @@ ROUND 0 - PREAMBLE (reception_phase=true, round_index=0) ** All subsequent challenges depend on: H(vk_hash || pub_inputs || wire_comms) ** -ROUND 1 - SORTED LIST ACCUMULATOR (reception_phase=false → true, round_index=0 → 1) -════════════════════════════════════════════════════════════════════════════════ +ROUND 1 - SORTED LIST ACCUMULATOR (challenge_generation_phase=true → false, round_index=0 → 1) +══════════════════════════════════════════════════════════════════════════════════════════ ┌──────────────────────────────────────────┐ │ receive_from_prover("lookup_read_counts")│──► Phase transition detected! -└──────────────────────────────────────────┘ reception_phase = true +└──────────────────────────────────────────┘ challenge_generation_phase = false ┌──────────────────────────────────────────┐ round_index++ = 1 │ receive_from_prover("lookup_read_tags") │──┐ └──────────────────────────────────────────┘ │ @@ -570,7 +570,7 @@ ROUND 1 - SORTED LIST ACCUMULATOR (reception_phase=false → true, round_index=0 │ receive_from_prover("w_4") │──┘ Origin tag: OriginTag(42, 1, is_submitted=true) └──────────────────────────────────────────┘ round_provenance = 0x0000...0002 (bit 1 lower) │ - │ Phase: reception → challenge generation + │ Phase: proof processing → challenge generation ▼ ┌──────────────────────────────────┐ │ get_challenges("beta", "gamma") │──► Generate challenges for log-derivative @@ -580,7 +580,7 @@ ROUND 1 - SORTED LIST ACCUMULATOR (reception_phase=false → true, round_index=0 beta, gamma -ROUND 2 - LOG DERIVATIVE INVERSE (reception_phase=false → true, round_index=1 → 2) +ROUND 2 - LOG DERIVATIVE INVERSE (challenge_generation_phase=true → false, round_index=1 → 2) ═══════════════════════════════════════════════════════════════════════════════ ┌──────────────────────────────────────────┐ @@ -696,9 +696,9 @@ VK HASHING WITH ORIGIN TAG ASSIGNMENT TRANSCRIPT STATE TRACKING ══════════════════════════ - transcript_index: 42 // Unique ID for this transcript (PRIVATE) - round_index: 0 → 1 // Increments when reception_phase: false → true (PRIVATE) - reception_phase: true/false // Receiving data vs generating challenges (PRIVATE) + transcript_index: 42 // Unique ID for this transcript (PRIVATE) + round_index: 0 → 1 // Increments when challenge_generation_phase: true → false (PRIVATE) + challenge_generation_phase: false/true // Generating challenges (true) vs proof processing (false) (PRIVATE) current_round_data: // Main Fiat-Shamir buffer (cleared on challenge) previous_challenge: // For duplex sponge c_next = H(c_prev || data) diff --git a/barretenberg/cpp/src/barretenberg/transcript/transcript.hpp b/barretenberg/cpp/src/barretenberg/transcript/transcript.hpp index 020322d46d36..252d08dd8cb0 100644 --- a/barretenberg/cpp/src/barretenberg/transcript/transcript.hpp +++ b/barretenberg/cpp/src/barretenberg/transcript/transcript.hpp @@ -69,9 +69,9 @@ template class BaseTranscript { template friend OriginTag bb::extract_transcript_tag(const T& transcript); // Fiat-Shamir Round Tracking - size_t transcript_index = 0; // Unique transcript ID (PRIVATE - access via extract_transcript_tag) - size_t round_index = 0; // Current FS round (PRIVATE - access via extract_transcript_tag) - bool reception_phase = true; // Whether receiving from prover or generating challenges + size_t transcript_index = 0; // Unique transcript ID (PRIVATE - access via extract_transcript_tag) + size_t round_index = 0; // Current FS round (PRIVATE - access via extract_transcript_tag) + bool challenge_generation_phase = false; // Whether currently generating challenges (vs sending/receiving data) // Challenge generatopm state== bool is_first_challenge = true; // Indicates if this is the first challenge this transcript is generating @@ -82,7 +82,6 @@ template class BaseTranscript { std::ptrdiff_t proof_start = 0; size_t num_frs_written = 0; // Number of frs written to proof_data by the prover size_t num_frs_read = 0; // Number of frs read from proof_data by the verifier - size_t round_number = 0; // Current round number for manifest // Manifest (debugging tool) bool use_manifest = false; // Indicates whether the manifest is turned on (only for manifest tests) @@ -141,7 +140,7 @@ template class BaseTranscript { { if (use_manifest) { // Add an entry to the current round of the manifest - manifest.add_entry(round_number, label, element_frs.size()); + manifest.add_entry(round_index, label, element_frs.size()); } current_round_data.insert(current_round_data.end(), element_frs.begin(), element_frs.end()); @@ -232,7 +231,7 @@ template class BaseTranscript { if (use_manifest) { // Add challenge labels for current round to the manifest for (const auto& label : labels) { - manifest.add_challenge(round_number, label); + manifest.add_challenge(round_index, label); } } @@ -258,18 +257,14 @@ template class BaseTranscript { challenges[num_challenges - 1] = Codec::template convert_challenge(challenge_buffer[0]); } - // In case the transcript is used for recursive verification, we can track proper Fiat-Shamir usage - // We are in challenge generation mode - if (reception_phase) { - reception_phase = false; + // Track Fiat-Shamir round transitions: entering challenge generation mode + if (!challenge_generation_phase) { + challenge_generation_phase = true; } // Assign origin tags to the challenges bb::assign_origin_tag(challenges, OriginTag(transcript_index, round_index, /*is_submitted=*/false)); - // Prepare for next round. - ++round_number; - return challenges; } @@ -320,11 +315,10 @@ template class BaseTranscript { template void add_to_hash_buffer(const std::string& label, const T& element) { DEBUG_LOG(label, element); - // In case the transcript is used for recursive verification, we can track proper Fiat-Shamir usage - // The verifier is receiving data from the prover. If before this we were in the challenge generation phase, - // then we need to increment the round index - if (!reception_phase) { - reception_phase = true; + // Track Fiat-Shamir round transitions: if we were generating challenges, + // now we're adding data, which means a new round has started + if (challenge_generation_phase) { + challenge_generation_phase = false; round_index++; } @@ -350,6 +344,13 @@ template class BaseTranscript { template void send_to_verifier(const std::string& label, const T& element) { DEBUG_LOG(label, element); + // Track Fiat-Shamir round transitions: if we were generating challenges, + // now we're sending data, which means a new round has started + if (challenge_generation_phase) { + challenge_generation_phase = false; + round_index++; + } + auto element_frs = Codec::template serialize_to_fields(element); proof_data.insert(proof_data.end(), element_frs.begin(), element_frs.end()); num_frs_written += element_frs.size(); @@ -370,11 +371,10 @@ template class BaseTranscript { BB_ASSERT_LTE(num_frs_read + element_size, proof_data.size()); auto element_frs = std::span{ proof_data }.subspan(num_frs_read, element_size); - // In case the transcript is used for recursive verification, we can track proper Fiat-Shamir usage - // The verifier is receiving data from the prover. If before this we were in the challenge generation phase, - // then we need to increment the round index - if (!reception_phase) { - reception_phase = true; + // Track Fiat-Shamir round transitions: if we were generating challenges, + // now we're receiving data, which means a new round has started + if (challenge_generation_phase) { + challenge_generation_phase = false; round_index++; } // Assign an origin tag to the elements going into the hash buffer From e4fa3c599e80f84fdf82f50ebeb9838726ab5ba9 Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Tue, 6 Jan 2026 17:49:13 +0000 Subject: [PATCH 05/24] Issues 5 and 6 --- .../stdlib/primitives/field/field_utils.cpp | 14 +- .../primitives/field/field_utils.test.cpp | 150 ++++++++++++++++++ 2 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.test.cpp diff --git a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp index 406a38e132ae..c56d4526e9ae 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp @@ -22,8 +22,15 @@ void validate_split_in_field_unsafe(const field_t& lo, const uint256_t r_lo = field_modulus.slice(0, lo_bits); const uint256_t r_hi = field_modulus.slice(lo_bits, field_modulus.get_msb() + 1); - // Check if we need to borrow - bool need_borrow = uint256_t(lo.get_value()) > r_lo; + // Algorithm: Validate lo + hi * 2^lo_bits < field_modulus using borrow logic + // + // We compute: hi_diff = r_hi - hi - borrow, lo_diff = r_lo - lo + borrow * 2^lo_bits + // Both must be in range [0, 2^bits) for the check to pass. + // + // - If lo < r_lo: no borrow, straightforward comparison of hi parts + // - If lo >= r_lo: set borrow=1, which reduces the allowed hi value by 1 + // This correctly rejects value == modulus because lo_diff becomes 2^lo_bits (out of range) + bool need_borrow = uint256_t(lo.get_value()) >= r_lo; field_t borrow = lo.is_constant() ? need_borrow @@ -86,8 +93,7 @@ std::pair, field_t> split_unique(const field_t + +using namespace bb; + +namespace { +template class FieldUtilsTests : public ::testing::Test { + public: + using field_t = stdlib::field_t; + using native = typename field_t::native; +}; + +using CircuitTypes = ::testing::Types; +} // namespace + +TYPED_TEST_SUITE(FieldUtilsTests, CircuitTypes); + +/** + * @brief Test that validate_split_in_field_unsafe rejects value == modulus + * @details This is a soundness bug: when lo + hi * 2^lo_bits == field_modulus, + * both hi_diff and lo_diff equal 0, which passes the range checks but should be rejected. + */ +TYPED_TEST(FieldUtilsTests, ValidateSplitRejectsModulus) +{ + using Builder = TypeParam; + using field_t = typename TestFixture::field_t; + using native = typename TestFixture::native; + + Builder builder; + constexpr size_t lo_bits = 128; + + // Construct a value equal to the bn254 scalar field modulus + uint256_t modulus = native::modulus; + uint256_t lo_val = modulus.slice(0, lo_bits); + uint256_t hi_val = modulus.slice(lo_bits, 254); + + // Create field elements from these values + auto lo = field_t::from_witness(&builder, native(lo_val)); + auto hi = field_t::from_witness(&builder, native(hi_val)); + + // Verify the reconstruction equals the modulus + uint256_t reconstructed = uint256_t(lo.get_value()) + (uint256_t(hi.get_value()) << lo_bits); + EXPECT_EQ(reconstructed, modulus); + + // Call validate_split_in_field_unsafe with the modulus itself + // This should create constraints that fail + stdlib::validate_split_in_field_unsafe(lo, hi, lo_bits, modulus); + + // The circuit should fail because value == modulus is invalid + EXPECT_FALSE(CircuitChecker::check(builder)); +} + +/** + * @brief Test that validate_split_in_field_unsafe accepts modulus - 1 + * @details The maximum valid value should be field_modulus - 1 + */ +TYPED_TEST(FieldUtilsTests, ValidateSplitAcceptsModulusMinusOne) +{ + using Builder = TypeParam; + using field_t = typename TestFixture::field_t; + using native = typename TestFixture::native; + + Builder builder; + constexpr size_t lo_bits = 128; + + // Construct a value equal to the bn254 scalar field modulus - 1 + uint256_t modulus = native::modulus; + uint256_t value = modulus - 1; + uint256_t lo_val = value.slice(0, lo_bits); + uint256_t hi_val = value.slice(lo_bits, 254); + + // Create field elements from these values + auto lo = field_t::from_witness(&builder, native(lo_val)); + auto hi = field_t::from_witness(&builder, native(hi_val)); + + // Verify the reconstruction equals modulus - 1 + uint256_t reconstructed = uint256_t(lo.get_value()) + (uint256_t(hi.get_value()) << lo_bits); + EXPECT_EQ(reconstructed, value); + + // Call validate_split_in_field_unsafe + // This should succeed because value < modulus + stdlib::validate_split_in_field_unsafe(lo, hi, lo_bits, modulus); + + // The circuit should be valid + EXPECT_FALSE(builder.failed()); + EXPECT_TRUE(CircuitChecker::check(builder)); +} + +/** + * @brief Test that split_unique rejects value == modulus + */ +TYPED_TEST(FieldUtilsTests, SplitUniqueRejectsModulus) +{ + using Builder = TypeParam; + using field_t = typename TestFixture::field_t; + using native = typename TestFixture::native; + + Builder builder; + constexpr size_t lo_bits = 128; + + // Create a field element that represents 0 (which is equivalent to modulus in field arithmetic) + // In the native field, we can't directly create a witness with value == modulus + // because it gets reduced to 0. So we test the edge case by using 0. + auto field = field_t::from_witness(&builder, native(0)); + + // Split it + auto [lo, hi] = stdlib::split_unique(field, lo_bits); + + // Both lo and hi should be 0 for value 0 + EXPECT_EQ(uint256_t(lo.get_value()), uint256_t(0)); + EXPECT_EQ(uint256_t(hi.get_value()), uint256_t(0)); + + // The circuit should be valid for 0 (the canonical representation) + EXPECT_FALSE(builder.failed()); + EXPECT_TRUE(CircuitChecker::check(builder)); +} + +/** + * @brief Test split_unique with maximum valid value + */ +TYPED_TEST(FieldUtilsTests, SplitUniqueMaxValue) +{ + using Builder = TypeParam; + using field_t = typename TestFixture::field_t; + using native = typename TestFixture::native; + + Builder builder; + constexpr size_t lo_bits = 128; + + // Create a field element with the maximum value (modulus - 1) + // This is represented as -1 in the field + auto field = field_t::from_witness(&builder, -native(1)); + + // Split it + auto [lo, hi] = stdlib::split_unique(field, lo_bits); + + // Verify reconstruction + uint256_t lo_val = uint256_t(lo.get_value()); + uint256_t hi_val = uint256_t(hi.get_value()); + uint256_t reconstructed = lo_val + (hi_val << lo_bits); + uint256_t expected = uint256_t(native::modulus) - 1; + + EXPECT_EQ(reconstructed, expected); + + // The circuit should be valid + EXPECT_FALSE(builder.failed()); + EXPECT_TRUE(CircuitChecker::check(builder)); +} From dd1f644b5b458e65ab7c9f4af6285baba1ec6357 Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Tue, 6 Jan 2026 18:03:45 +0000 Subject: [PATCH 06/24] Issue 7 --- .../stdlib/hash/poseidon2/poseidon2_permutation.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2_permutation.cpp b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2_permutation.cpp index 157ed88ee2cd..07954b18b12c 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2_permutation.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2_permutation.cpp @@ -113,10 +113,6 @@ void Poseidon2Permutation::matrix_multiplication_external(typename Pose // gate 6: Compute v3 = v4 + tmp2 state[2] = state[3] + tmp2; - - // This can only happen if the input contained constant `field_t` elements. - BB_ASSERT(state[0].is_normalized() && state[1].is_normalized() && state[2].is_normalized() && - state[3].is_normalized()); } template class Poseidon2Permutation; From d064598600b0a87904734f38f5c078b99930d798 Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Tue, 6 Jan 2026 18:35:59 +0000 Subject: [PATCH 07/24] Issue 8 --- .../poseidon2.circuit.failure.test.cpp | 73 +++++++++++++------ 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.circuit.failure.test.cpp b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.circuit.failure.test.cpp index b63df0d5549b..83e0e666d2f8 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.circuit.failure.test.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.circuit.failure.test.cpp @@ -1,6 +1,7 @@ #include "barretenberg/flavor/ultra_flavor.hpp" #include "barretenberg/relations/relation_parameters.hpp" #include "barretenberg/sumcheck/sumcheck.hpp" +#include "barretenberg/ultra_honk/witness_computation.hpp" #include @@ -77,45 +78,68 @@ class Poseidon2FailureTests : public ::testing::Test { [[maybe_unused]] auto hash = stdlib::poseidon2::hash({ random_input }); } - void prove_and_verify(const std::shared_ptr& prover_instance, bool expected_result) + void prove_and_verify(std::shared_ptr& prover_instance, bool expected_result) { const size_t virtual_log_n = Flavor::VIRTUAL_LOG_N; - // Random subrelation separators are needed here to make sure that the sumcheck is failing because of the wrong - // Poseidon2 selector/witness values. - SubrelationSeparator subrelation_separator = FF::random_element(); + // Complete the prover instance (compute selectors, relation parameters, etc.) + WitnessComputation::complete_prover_instance_for_test(prover_instance); - std::vector gate_challenges(virtual_log_n); - - // Random gate challenges ensure that relations are satisfied at every point of the hypercube - for (auto& beta : gate_challenges) { - beta = FF::random_element(); - } + auto prover_transcript = Transcript::prover_init_empty(); - RelationParameters relation_parameters; + // Generate challenges via transcript for Fiat-Shamir + SubrelationSeparator subrelation_separator = prover_transcript->template get_challenge("Sumcheck:alpha"); - for (auto& rel_param : relation_parameters.get_to_fold()) { - rel_param = FF::random_element(); + std::vector gate_challenges(virtual_log_n); + for (size_t idx = 0; idx < virtual_log_n; idx++) { + gate_challenges[idx] = + prover_transcript->template get_challenge("Sumcheck:gate_challenge_" + std::to_string(idx)); } - auto prover_transcript = std::make_shared(); + + // Set gate challenges on prover instance + prover_instance->gate_challenges = gate_challenges; SumcheckProver sumcheck_prover(prover_instance->dyadic_size(), prover_instance->polynomials, prover_transcript, subrelation_separator, gate_challenges, - relation_parameters, + prover_instance->relation_parameters, virtual_log_n); auto proof = sumcheck_prover.prove(); auto verifier_transcript = std::make_shared(prover_transcript->export_proof()); - SumcheckVerifier verifier(verifier_transcript, subrelation_separator, virtual_log_n); - auto result = verifier.verify(relation_parameters, gate_challenges, std::vector(virtual_log_n, 1)); + SubrelationSeparator verifier_subrelation_separator = + verifier_transcript->template get_challenge("Sumcheck:alpha"); + std::vector verifier_gate_challenges(virtual_log_n); + for (size_t idx = 0; idx < virtual_log_n; idx++) { + verifier_gate_challenges[idx] = + verifier_transcript->template get_challenge("Sumcheck:gate_challenge_" + std::to_string(idx)); + } + + // Run sumcheck verifier + SumcheckVerifier verifier(verifier_transcript, verifier_subrelation_separator, virtual_log_n); + auto result = verifier.verify( + prover_instance->relation_parameters, verifier_gate_challenges, std::vector(virtual_log_n, 1)); EXPECT_EQ(result.verified, expected_result); }; }; +TEST_F(Poseidon2FailureTests, ValidCircuitVerifies) +{ + Builder builder; + + // Construct a circuit that hashes a single witness field element. + hash_single_input(builder); + + // Convert circuit to polynomials. + auto prover_instance = std::make_shared>(builder); + + // Run sumcheck on the UNMODIFIED valid data - this should pass + prove_and_verify(prover_instance, true); +} + TEST_F(Poseidon2FailureTests, WrongSelectorValues) { Builder builder; @@ -126,18 +150,21 @@ TEST_F(Poseidon2FailureTests, WrongSelectorValues) // Convert circuit to polynomials. auto prover_instance = std::make_shared>(builder); { - // Disable Poseidon2 External selector in the first active row + // Disable Poseidon2 External selector in the first active row. + // This UNDERCONSTRAINS the circuit, so verification still passes because all remaining constraints are + // satisfied on the valid witness data. modify_selector(prover_instance->polynomials.q_poseidon2_external); - // Run sumcheck on the invalidated data - prove_and_verify(prover_instance, false); + // Run sumcheck - it should PASS because we only removed a constraint + prove_and_verify(prover_instance, true); } { - // Disable Poseidon2 Internal selector in the first active row + // Disable Poseidon2 Internal selector in the first active row. + // Again, this underconstrains the circuit, so verification passes. modify_selector(prover_instance->polynomials.q_poseidon2_internal); - // Run sumcheck on the invalidated data - prove_and_verify(prover_instance, false); + // Run sumcheck - it should PASS because we only removed a constraint + prove_and_verify(prover_instance, true); } } From 17f3c7418ee6f29199f3eb38386ee6c5c1b201d2 Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Wed, 7 Jan 2026 15:00:08 +0000 Subject: [PATCH 08/24] Issues 9 and 11 --- .../ecc/fields/field_conversion.hpp | 30 +++- .../ecc/fields/field_conversion.test.cpp | 45 ++++++ .../stdlib/hash/poseidon2/poseidon2.test.cpp | 85 +++++++++++ .../field/field_conversion.test.cpp | 141 ++++++++++++++++-- 4 files changed, 287 insertions(+), 14 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.hpp b/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.hpp index feda8fb2fab2..2b29493a2d04 100644 --- a/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.hpp +++ b/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.hpp @@ -39,6 +39,24 @@ class FrCodec { } } + /** + * @brief Check whether raw limbs represent the point at infinity (all limbs zero). + * @details This matches the circuit behavior in StdlibCodec::check_point_at_infinity. + * We check raw limbs BEFORE deserializing to field elements to ensure that alias + * values (e.g., x=modulus, y=modulus) are NOT treated as point at infinity. + * Only the canonical (0,0) representation with all-zero limbs is accepted. + */ + template static bool check_point_at_infinity(std::span fr_vec) + { + // Check if all limbs are zero - this is the only canonical representation of infinity + for (const auto& limb : fr_vec) { + if (!limb.is_zero()) { + return false; + } + } + return true; + } + /** * @brief Converts 2 bb::fr elements to fq * @details Splits into 136-bit lower chunk and 118-bit upper chunk to mirror stdlib bigfield limbs (68-bit each). @@ -98,12 +116,18 @@ class FrCodec { } else if constexpr (IsAnyOf) { using BaseField = typename T::Fq; constexpr size_t BASE = calc_num_fields(); + + // Check for point at infinity BEFORE deserializing to avoid alias issues. + // Only canonical (0,0) with all-zero limbs is accepted as infinity. + // This matches circuit behavior in StdlibCodec::check_point_at_infinity. + if (check_point_at_infinity(fr_vec)) { + return T::infinity(); + } + + // Deserialize coordinates (this will reject non-canonical values via BB_ASSERT) T val; val.x = deserialize_from_fields(fr_vec.subspan(0, BASE)); val.y = deserialize_from_fields(fr_vec.subspan(BASE, BASE)); - if (val.x == BaseField::zero() && val.y == BaseField::zero()) { - val.self_set_infinity(); - } BB_ASSERT(val.on_curve()); return val; } else { diff --git a/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.test.cpp b/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.test.cpp index e247ae531836..4578a9e4e50e 100644 --- a/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.test.cpp +++ b/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.test.cpp @@ -163,4 +163,49 @@ TEST_F(FieldConversionTest, ConvertChallengeGrumpkinFr) EXPECT_EQ(uint256_t(result), expected); } +// ============================================================================ +// Additional FrCodec-specific tests +// Note: Most rejection/acceptance tests are in stdlib/primitives/field/field_conversion.test.cpp +// which tests both FrCodec and StdlibCodec together for consistency. +// ============================================================================ + +/** + * @brief Test that valid canonical (0, 0) is accepted as point at infinity. + * @details This ensures we're only rejecting non-canonical aliases, not the proper encoding. + */ +TEST_F(FieldConversionTest, AcceptCanonicalPointAtInfinity) +{ + // Test for BN254 points + { + std::vector fr_vec = { bb::fr(0), bb::fr(0), bb::fr(0), bb::fr(0) }; + auto point = FrCodec::deserialize_from_fields(fr_vec); + EXPECT_TRUE(point.is_point_at_infinity()); + } + + // Test for Grumpkin points + { + std::vector fr_vec = { bb::fr(0), bb::fr(0) }; + auto point = FrCodec::deserialize_from_fields(fr_vec); + EXPECT_TRUE(point.is_point_at_infinity()); + } +} + +/** + * @brief Test that points not on the curve are rejected. + */ +TEST_F(FieldConversionTest, RejectPointNotOnCurve) +{ + // Test for BN254: (1, 4) is not on the curve + { + std::vector fr_vec = { bb::fr(1), bb::fr(0), bb::fr(4), bb::fr(0) }; + EXPECT_THROW(FrCodec::deserialize_from_fields(fr_vec), std::runtime_error); + } + + // Test for Grumpkin: (12, 100) is not on the curve + { + std::vector fr_vec = { bb::fr(12), bb::fr(100) }; + EXPECT_THROW(FrCodec::deserialize_from_fields(fr_vec), std::runtime_error); + } +} + } // namespace bb::field_conversion_tests diff --git a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.test.cpp b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.test.cpp index 6b695d375621..cfa0ab095900 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.test.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.test.cpp @@ -3,6 +3,7 @@ #include "barretenberg/common/test.hpp" #include "barretenberg/crypto/poseidon2/poseidon2.hpp" #include "barretenberg/numeric/random/engine.hpp" +#include "barretenberg/stdlib/primitives/field/field_conversion.hpp" #include "barretenberg/ultra_honk/prover_instance.hpp" using namespace bb; @@ -373,3 +374,87 @@ TYPED_TEST(StdlibPoseidon2, Consistency) TestFixture::test_against_independent_values(); } + +/** + * @brief Test that bn254 point coordinates with alias values produce different hashes. + * @details When a bn254 point is deserialized with alias x-coordinate (x + modulus), + * the resulting hash must be different from the canonical representation. + * Also verifies consistency between stdlib and native Poseidon2 implementations. + */ +TYPED_TEST(StdlibPoseidon2, PointCoordinatesVsAliasProduceDifferentHashes) +{ + using Builder = TypeParam; + using field_ct = stdlib::field_t; + using witness_ct = stdlib::witness_t; + using poseidon2 = stdlib::poseidon2; + using native_poseidon2 = crypto::Poseidon2; + using Codec = stdlib::StdlibCodec; + using bn254_point_ct = typename Codec::bn254_commitment; + + constexpr uint64_t NUM_LIMB_BITS = 68; + constexpr uint64_t LOW_BITS = 2 * NUM_LIMB_BITS; + constexpr uint256_t LOW_MASK = (uint256_t(1) << LOW_BITS) - 1; + + // Use generator point coordinates + const uint256_t x_value = uint256_t(bb::g1::affine_one.x); + const uint256_t y_value = uint256_t(bb::g1::affine_one.y); + + const uint256_t fq_modulus = bb::fq::modulus; + + // Compute canonical limbs for x and y + const uint256_t x_lo = x_value & LOW_MASK; + const uint256_t x_hi = x_value >> LOW_BITS; + const uint256_t y_lo = y_value & LOW_MASK; + const uint256_t y_hi = y_value >> LOW_BITS; + + // Compute alias limbs (x + modulus, y unchanged) + const uint256_t alias_x_value = x_value + fq_modulus; + const uint256_t alias_x_lo = alias_x_value & LOW_MASK; + const uint256_t alias_x_hi = alias_x_value >> LOW_BITS; + + BB_ASSERT(alias_x_hi != x_hi); + BB_ASSERT(alias_x_lo != x_lo); + + // Lambda to compute native hash from limbs + auto compute_native_hash = + [&](const uint256_t& xl, const uint256_t& xh, const uint256_t& yl, const uint256_t& yh) -> bb::fr { + std::vector limbs = { bb::fr(xl), bb::fr(xh), bb::fr(yl), bb::fr(yh) }; + return native_poseidon2::hash(limbs); + }; + + // Lambda to deserialize point from limbs, serialize back, and hash (stdlib) + auto compute_stdlib_hash = + [&](const uint256_t& xl, const uint256_t& xh, const uint256_t& yl, const uint256_t& yh) -> bb::fr { + Builder builder; + std::vector limbs = { field_ct(witness_ct(&builder, bb::fr(xl))), + field_ct(witness_ct(&builder, bb::fr(xh))), + field_ct(witness_ct(&builder, bb::fr(yl))), + field_ct(witness_ct(&builder, bb::fr(yh))) }; + + // Deserialize through codec + bn254_point_ct pt = Codec::template deserialize_from_fields(limbs); + + // Serialize back and hash + std::vector serialized = Codec::template serialize_to_fields(pt); + auto hash = poseidon2::hash(serialized); + + EXPECT_TRUE(CircuitChecker::check(builder)); + return hash.get_value(); + }; + + // Compute native hashes + bb::fr canonical_native_hash = compute_native_hash(x_lo, x_hi, y_lo, y_hi); + bb::fr alias_native_hash = compute_native_hash(alias_x_lo, alias_x_hi, y_lo, y_hi); + + // Compute stdlib hashes + bb::fr canonical_stdlib_hash = compute_stdlib_hash(x_lo, x_hi, y_lo, y_hi); + bb::fr alias_stdlib_hash = compute_stdlib_hash(alias_x_lo, alias_x_hi, y_lo, y_hi); + + // Verify stdlib matches native for both canonical and alias + EXPECT_EQ(canonical_stdlib_hash, canonical_native_hash); + EXPECT_EQ(alias_stdlib_hash, alias_native_hash); + + // The hashes MUST be different between canonical and alias + EXPECT_NE(canonical_native_hash, alias_native_hash); + EXPECT_NE(canonical_stdlib_hash, alias_stdlib_hash); +} diff --git a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.test.cpp b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.test.cpp index 75d6704cffc6..28aeb2457a7e 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.test.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.test.cpp @@ -445,29 +445,148 @@ TYPED_TEST(stdlib_field_conversion, GateCountUnivariateDeserialization) /** * @brief Failure test for deserializing a pair of limbs as a bigfield, where one of the limbs exceeds the strict 2^136 * upper bound. + * @details Verifies both native (FrCodec) and circuit (StdlibCodec) codecs reject out-of-range limbs. + * Native codec uses BB_ASSERT to reject, circuit codec uses in-circuit constraints. */ TYPED_TEST(stdlib_field_conversion, BigfieldDeserializationFails) { - // Need to bypass an out-of-circuit range check - BB_DISABLE_ASSERTS(); using Builder = TypeParam; using Codec = StdlibCodec>; - Builder builder; - bb::fr low_limb = bb::fr(0); // Create a limb from the value 2^136, that does not satisfy the condition < 2^136. bb::fr high_limb = bb::fr(uint256_t(1) << (2 * fq::NUM_LIMB_BITS)); - info(high_limb); - std::vector> circuit_fields = { field_t::from_witness(&builder, low_limb), - field_t::from_witness(&builder, high_limb) }; + // Test 1: Native codec should reject via BB_ASSERT (asserts enabled) + { + std::vector native_fields = { low_limb, high_limb }; + EXPECT_THROW(FrCodec::deserialize_from_fields(native_fields), std::runtime_error); + } + + // Test 2: Circuit codec should reject via circuit constraints (disable asserts to bypass bigfield constructor + // checks) + { + BB_DISABLE_ASSERTS(); + Builder builder; + std::vector> circuit_fields = { field_t::from_witness(&builder, low_limb), + field_t::from_witness(&builder, high_limb) }; + + // Deserialize as bigfield - this creates the bigfield from the two limbs + [[maybe_unused]] auto bigfield_val = Codec::template deserialize_from_fields>(circuit_fields); + + // Circuit should fail validation + EXPECT_FALSE(CircuitChecker::check(builder)); + } +} + +// ============================================================================ +// Codec Consistency Tests: Verify FrCodec and StdlibCodec behave identically +// ============================================================================ + +/** + * @brief Test that both codecs reject point with alias coordinates. + * @details Specific case from audit: (x=modulus, y=modulus) should be rejected by both. + * For Ultra, the on-curve check is in the main circuit. For Mega (goblin), the on-curve check + * is delegated to ECCVM (see ECCVMTranscriptRelationImpl), so the main circuit will pass. + */ +TYPED_TEST(stdlib_field_conversion, BothCodecsRejectPointAtInfinityAlias) +{ + using Builder = TypeParam; + using Codec = StdlibCodec>; + using fq = bigfield; + using bn254_element = element, curve::BN254::Group>; + + constexpr uint64_t NUM_LIMB_BITS = 68; + const uint256_t modulus = bb::fq::modulus; - // Deserialize as bigfield - this creates the bigfield from the two limbs - [[maybe_unused]] auto bigfield_val = Codec::template deserialize_from_fields>(circuit_fields); + // Create alias coordinates: x = modulus, y = modulus + const uint256_t x_lo = modulus & ((uint256_t(1) << (NUM_LIMB_BITS * 2)) - 1); + const uint256_t x_hi = modulus >> (NUM_LIMB_BITS * 2); + + // Test 1: Native codec rejects via on_curve check + { + std::vector native_fields = { bb::fr(x_lo), bb::fr(x_hi), bb::fr(x_lo), bb::fr(x_hi) }; + EXPECT_THROW(FrCodec::deserialize_from_fields(native_fields), std::runtime_error); + } - // Circuit should fail validation - EXPECT_FALSE(CircuitChecker::check(builder)); + // Test 2: Circuit codec rejects (Ultra only - Mega delegates on-curve check to ECCVM) + if constexpr (IsAnyOf) { + BB_DISABLE_ASSERTS(); + Builder builder; + std::vector> circuit_fields = { field_t::from_witness(&builder, bb::fr(x_lo)), + field_t::from_witness(&builder, bb::fr(x_hi)), + field_t::from_witness(&builder, bb::fr(x_lo)), + field_t::from_witness(&builder, bb::fr(x_hi)) }; + [[maybe_unused]] auto point = Codec::template deserialize_from_fields(circuit_fields); + EXPECT_FALSE(CircuitChecker::check(builder)); + } +} + +/** + * @brief Test the upper bound on fq values accepted by both codecs. + * @details Both codecs use limb bounds: low < 2^136, high < 2^118. + * This allows values in [0, 2^254 - 1]. Values >= 2^254 are rejected. + * + * - 2^254 - 1: Maximum valid value (accepted, reduces to 2^254 - 1 - modulus) + * - 2^254: Exceeds high limb bound (rejected by both codecs) + */ +TYPED_TEST(stdlib_field_conversion, BothCodecsAcceptMaxAliasRejectOverflow) +{ + using Builder = TypeParam; + using Codec = StdlibCodec>; + using fq_ct = bigfield; + + constexpr uint64_t NUM_LIMB_BITS = 68; + constexpr uint64_t LOW_BITS = NUM_LIMB_BITS * 2; // 136 + constexpr uint64_t HIGH_BITS = 254 - LOW_BITS; // 118 + + // Test 1: 2^254 - 1 is accepted (max valid alias) + { + const uint256_t max_value = (uint256_t(1) << 254) - 1; + const uint256_t low_limb = max_value & ((uint256_t(1) << LOW_BITS) - 1); // 2^136 - 1 + const uint256_t high_limb = max_value >> LOW_BITS; // 2^118 - 1 + + // Verify limbs are at their max valid values + EXPECT_EQ(low_limb, (uint256_t(1) << LOW_BITS) - 1); + EXPECT_EQ(high_limb, (uint256_t(1) << HIGH_BITS) - 1); + + // Native codec: accepts + std::vector native_fields = { bb::fr(low_limb), bb::fr(high_limb) }; + auto native_result = FrCodec::deserialize_from_fields(native_fields); + EXPECT_EQ(uint256_t(native_result), max_value - bb::fq::modulus); // Reduced + + // Circuit codec: accepts + Builder builder; + std::vector> circuit_fields = { field_t::from_witness(&builder, bb::fr(low_limb)), + field_t::from_witness(&builder, bb::fr(high_limb)) }; + [[maybe_unused]] auto circuit_result = Codec::template deserialize_from_fields(circuit_fields); + EXPECT_TRUE(CircuitChecker::check(builder)); + } + + // Test 2: 2^254 is rejected (high limb = 2^118 exceeds bound) + { + const uint256_t overflow_value = uint256_t(1) << 254; + const uint256_t low_limb = 0; + const uint256_t high_limb = uint256_t(1) << HIGH_BITS; // 2^118 (exactly at boundary) + + // Verify this represents 2^254 + EXPECT_EQ(low_limb + (high_limb << LOW_BITS), overflow_value); + + // Native codec: rejects (high limb >= 2^118) + std::vector native_fields = { bb::fr(low_limb), bb::fr(high_limb) }; + EXPECT_THROW(FrCodec::deserialize_from_fields(native_fields), std::runtime_error); + + // Circuit codec: rejects via range constraints + { + BB_DISABLE_ASSERTS(); + Builder builder; + std::vector> circuit_fields = { field_t::from_witness(&builder, bb::fr(low_limb)), + field_t::from_witness(&builder, + bb::fr(high_limb)) }; + [[maybe_unused]] auto circuit_result = Codec::template deserialize_from_fields(circuit_fields); + EXPECT_FALSE(CircuitChecker::check(builder)); + } + } } } // namespace bb::stdlib::field_conversion_tests From 98178c58586ef929fc1fc75f42351dce1e67ae2e Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Wed, 7 Jan 2026 15:14:21 +0000 Subject: [PATCH 09/24] Issue 10 --- .../stdlib/primitives/field/field_utils.cpp | 21 +++--- .../primitives/field/field_utils.test.cpp | 69 +++++++++++++++++++ 2 files changed, 82 insertions(+), 8 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp index c56d4526e9ae..430b7bab66ae 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp @@ -31,16 +31,21 @@ void validate_split_in_field_unsafe(const field_t& lo, // - If lo >= r_lo: set borrow=1, which reduces the allowed hi value by 1 // This correctly rejects value == modulus because lo_diff becomes 2^lo_bits (out of range) bool need_borrow = uint256_t(lo.get_value()) >= r_lo; - field_t borrow = - lo.is_constant() - ? need_borrow - : field_t::from_witness(lo.get_context(), typename field_t::native(need_borrow)); - // directly call `create_small_range_constraint` to avoid creating an arithmetic gate - if (!lo.is_constant()) { + // If both lo and hi are constant, the validation is entirely deterministic + const bool both_constant = lo.is_constant() && hi.is_constant(); + Builder* ctx = validate_context(lo.get_context(), hi.get_context()); + + field_t borrow = both_constant + ? need_borrow + : field_t::from_witness(ctx, typename field_t::native(need_borrow)); + + // Constrain borrow to be boolean (0 or 1) unless both inputs are constant. + // This prevents a malicious prover from using non-boolean borrow values. + if (!both_constant) { // We need to manually propagate the origin tag - borrow.set_origin_tag(lo.get_origin_tag()); - lo.get_context()->create_small_range_constraint(borrow.get_witness_index(), 1, "borrow"); + borrow.set_origin_tag(lo.is_constant() ? hi.get_origin_tag() : lo.get_origin_tag()); + ctx->create_small_range_constraint(borrow.get_witness_index(), 1, "borrow"); } // Hi range check = r_hi - hi - borrow diff --git a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.test.cpp b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.test.cpp index 7e7bfabfc022..97f56aec0cc1 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.test.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.test.cpp @@ -148,3 +148,72 @@ TYPED_TEST(FieldUtilsTests, SplitUniqueMaxValue) EXPECT_FALSE(builder.failed()); EXPECT_TRUE(CircuitChecker::check(builder)); } + +/** + * @brief Test validate_split_in_field_unsafe rejects modulus with constant lo and witness hi + * @details Regression test for audit finding: when lo is constant but hi is a witness, + * the borrow value must still be constrained to be boolean. Previously, the range constraint + * was skipped if lo was constant, allowing malicious provers to use non-boolean borrow values + * to bypass the field validation check. + */ +TYPED_TEST(FieldUtilsTests, ValidateSplitConstantLoWitnessHiRejectsModulus) +{ + using Builder = TypeParam; + using field_t = typename TestFixture::field_t; + using native = typename TestFixture::native; + + Builder builder; + constexpr size_t lo_bits = 128; + + // Use value == modulus (should be rejected) + uint256_t modulus = native::modulus; + uint256_t lo_val = modulus.slice(0, lo_bits); + uint256_t hi_val = modulus.slice(lo_bits, 254); + + // Create constant lo and witness hi + auto lo = field_t(native(lo_val)); // constant + auto hi = field_t::from_witness(&builder, native(hi_val)); // witness + + // Verify the setup + EXPECT_TRUE(lo.is_constant()); + EXPECT_FALSE(hi.is_constant()); + + // Call validate_split_in_field_unsafe with value == modulus + stdlib::validate_split_in_field_unsafe(lo, hi, lo_bits, modulus); + + // The circuit should FAIL because value == modulus is invalid + EXPECT_FALSE(CircuitChecker::check(builder)); +} + +/** + * @brief Test validate_split_in_field_unsafe rejects modulus with witness lo and constant hi + * @details Symmetric case to the above test. + */ +TYPED_TEST(FieldUtilsTests, ValidateSplitWitnessLoConstantHiRejectsModulus) +{ + using Builder = TypeParam; + using field_t = typename TestFixture::field_t; + using native = typename TestFixture::native; + + Builder builder; + constexpr size_t lo_bits = 128; + + // Use value == modulus (should be rejected) + uint256_t modulus = native::modulus; + uint256_t lo_val = modulus.slice(0, lo_bits); + uint256_t hi_val = modulus.slice(lo_bits, 254); + + // Create witness lo and constant hi + auto lo = field_t::from_witness(&builder, native(lo_val)); // witness + auto hi = field_t(native(hi_val)); // constant + + // Verify the setup + EXPECT_FALSE(lo.is_constant()); + EXPECT_TRUE(hi.is_constant()); + + // Call validate_split_in_field_unsafe with value == modulus + stdlib::validate_split_in_field_unsafe(lo, hi, lo_bits, modulus); + + // The circuit should FAIL because value == modulus is invalid + EXPECT_FALSE(CircuitChecker::check(builder)); +} From 7f1cb65c90ca7ca58f34d51406205ca15d53c068 Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Thu, 8 Jan 2026 12:02:52 +0000 Subject: [PATCH 10/24] Redo Issues 9 and 11 --- .../barretenberg/ecc/fields/CODEC_README.md | 246 ++++++++++++++++++ .../ecc/fields/field_conversion.hpp | 5 + .../primitives/field/field_conversion.hpp | 30 ++- .../field/field_conversion.test.cpp | 76 ++++-- 4 files changed, 325 insertions(+), 32 deletions(-) create mode 100644 barretenberg/cpp/src/barretenberg/ecc/fields/CODEC_README.md diff --git a/barretenberg/cpp/src/barretenberg/ecc/fields/CODEC_README.md b/barretenberg/cpp/src/barretenberg/ecc/fields/CODEC_README.md new file mode 100644 index 000000000000..a660d29aaddd --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/ecc/fields/CODEC_README.md @@ -0,0 +1,246 @@ +# Codecs + +This document describes the codec classes used for serializing/deserializing field elements and curve points in proof transcripts. + +## Overview + +| Codec | Location | Context | Data Type | +|-------|----------|---------|-----------| +| `FrCodec` | `ecc/fields/field_conversion.hpp` | Native (prover/verifier) | `bb::fr` | +| `StdlibCodec` | `stdlib/primitives/field/field_conversion.hpp` | In-circuit (recursive verification) | `field_t` | +| `U256Codec` | `ecc/fields/field_conversion.hpp` | Native (simple) | `uint256_t` | + +## Field Element Encoding + +Non-native field elements (`fq` = Grumpkin scalar = BN254 base field) are encoded as 2 limbs: + +``` +value = low_limb + (high_limb << 136) + +low_limb: 136 bits (2 × 68-bit bigfield limbs combined) +high_limb: 118 bits (remaining 2 × 68-bit limbs, but only 118 bits used) +``` + +This matches the internal representation of `stdlib::bigfield` which uses four 68-bit limbs. + +## Canonical Representation + +**Both codecs reject non-canonical values** (values ≥ `fq::modulus`): + +| Codec | Enforcement | Method | +|-------|-------------|--------| +| `FrCodec` | Native assertion | `BB_ASSERT_LT(value, fq::modulus)` | +| `StdlibCodec` | In-circuit constraint | `bigfield::assert_is_in_field()` | + +This ensures consistent behavior whether verification happens natively or in-circuit. + +### Limb Bounds + +Before the modulus check, limb bounds are verified: +- `low_limb < 2^136` +- `high_limb < 2^118` + +Values passing limb bounds but ≥ modulus (aliases) are rejected by the modulus check. + +## Point at Infinity + +The point at infinity is represented as `(0, 0)` with **all limbs zero**. + +**Critical**: The infinity check examines raw limbs BEFORE field reduction to prevent alias attacks: + +```cpp + +for (const auto& limb : fr_vec) { + if (!limb.is_zero()) return false; +} +return true; // Only canonical (0,0) accepted +``` + +### BN254 vs Grumpkin Infinity Check (Circuit) + +| Curve | Method | Rationale | +|-------|--------|-----------| +| BN254 | `sum(limbs) == 0` | 4 limbs, sum ≤ 2^138, no overflow | +| Grumpkin | `x² + 5y² == 0` | Uses that 5 is non-square mod p | + +## Ultra vs Mega Arithmetization + +| Aspect | Ultra | Mega (Goblin) | +|--------|-------|---------------| +| Base field type | `bigfield` | `goblin_field` | +| Range constraints | In-circuit | Deferred to Translator | +| On-curve check | In-circuit | Deferred to ECCVM | +| Point type | `element_default` | `goblin_element` | + + +## Supported Types + +Both codecs handle: +- `fr` / `field_t` — single field element +- `fq` / `bigfield` — 2-limb encoding +- `goblin_field` — 2-limb encoding (Mega only) +- `bn254_commitment` — 4 limbs (x_lo, x_hi, y_lo, y_hi) +- `grumpkin_commitment` — 2 limbs (x, y in fr) +- `std::array` — concatenated encoding +- `Univariate` — concatenated encoding + +## Challenge Splitting + +Challenges are split into two 127-bit limbs: + +```cpp +static std::array split_challenge(const fr& challenge) { + // lo = challenge[0:127], hi = challenge[127:254] + // Both halves provide >100-bit security +} +``` + + +## Threat Model: Verification Consistency + +### Why Aliased Values Aren't Inherently Dangerous + +Values we deserialize come from **proof transcripts**. If someone uses non-canonical (aliased) +representations in a proof: +- The Fiat-Shamir hash changes (different bytes = different challenges) +- The proof is simply invalid for the original statement +- This doesn't allow "cheating" the proof system + +See test `StdlibPoseidon2::PointCoordinatesVsAliasProduceDifferentHashes` in +`stdlib/hash/poseidon2/poseidon2.test.cpp` which demonstrates that aliased coordinates +produce different hashes than canonical ones. + +### The Real Concern: Native vs Recursive Consistency + +The critical security property is that **native and recursive verification must accept/reject +the same set of proofs**. In practice, native verification runs first (it's ~1000x cheaper), +so the main threat is: + +| Scenario | Risk | Impact | +|----------|------|--------| +| **Native OK, Recursive FAILS** | **HIGH (DoS)** | Attacker crafts proof passing cheap native check, then expensive recursive verification fails. Wastes resources, can block IVC chains. | +| **Recursive OK, Native FAILS** | Low | Unlikely in practice since native verification precedes recursive. Would be caught at native step. | + +### Ensuring Consistency + +Both `FrCodec` (native) and `StdlibCodec` (circuit) must: +1. Accept the same valid inputs +2. Reject the same invalid inputs (including aliases) +3. Handle edge cases identically (point at infinity, modulus-1, etc.) + +## Security Properties + +1. **No alias acceptance**: Values ≥ modulus are rejected everywhere +2. **Unique infinity**: Only `(0,0)` with zero limbs represents infinity +3. **Consistent native/circuit**: Both paths reject the same malformed inputs + +## Mega/Goblin Specifics + +### Mega/Goblin Validation Flow + +For `goblin_field` and `goblin_element`: +1. Limbs stored as-is during deserialization (no in-circuit range check) +2. Values flow to op queue as ECC operations +3. **Translator** enforces limb range constraints +4. **ECCVM** enforces on-curve property via `ECCVMTranscriptRelationImpl` + +### Deferred Validation and Consistency + +In Mega circuits, `StdlibCodec` deserializes points into `goblin_field`/`goblin_element` without +immediate in-circuit range or modulus checks. This is intentional for efficiency - expensive +checks are deferred to specialized Translator/ECCVM circuits. + +The `assert_equal` constraints in `biggroup_goblin.hpp` ensure that the limbs in the circuit +match what the op_queue uses: + +```cpp +// biggroup_goblin.hpp:181-190 +op_tuple = builder->queue_ecc_add_accum(other.get_value()); +x_lo.assert_equal(other._x.limbs[0]); +x_hi.assert_equal(other._x.limbs[1]); +``` + +For an honest prover, `get_value()` auto-reduces field values, so aliased inputs would cause +these constraints to fail (circuit uses aliased limbs, op_queue uses reduced limbs). +This ensures recursive verification rejects what native verification would also reject. + +However, a malicious prover who modifies native code could make both sides of `assert_equal` +contain aliased limbs, bypassing these constraints. The ultimate security guarantee comes +from the translation check. + +### ECCVM ↔ Translator Translation Check + +The ECCVM and Translator circuits must agree on the accumulated result of all ECC operations. +This provides security even against a malicious prover because: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Op Queue (shared data) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ eccvm_ops_table │ ultra_ops_table │ +│ (native Point coords) │ (limb-decomposed data) │ +│ ───────────────────── │ ───────────────────── │ +│ base_point.x (bb::fq) │ x_lo, x_hi (Fr limbs) │ +│ base_point.y (bb::fq) │ y_lo, y_hi (Fr limbs) │ +│ ALWAYS CANONICAL │ Could be aliased if prover modifies code │ +└─────────────────────────────────────────────────────────────────────────┘ + │ │ + ▼ ▼ + ┌───────────────┐ ┌───────────────┐ + │ ECCVM │ │ Translator │ + │ │ │ │ + │ transcript_Px │ │ op_queue wire │ + │ transcript_Py │ │ columns │ + │ (from native │ │ (from limbs) │ + │ bb::fq) │ │ │ + └───────┬───────┘ └───────┬───────┘ + │ │ + ▼ ▼ + accumulated_result Translator's + = (op + v·Px + v²·Py computed + + v³·z1 + v⁴·z2 accumulator + - masking) / x + │ │ + └──────────────┬───────────────┘ + ▼ + TranslatorAccumulatorTransferRelation + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + accumulators_binary_limbs_i == accumulated_result[i] + + If aliased limbs in Translator but canonical in ECCVM: + → Accumulators DON'T MATCH → Relation FAILS +``` + +### Why Native Code Modifications Can't Bypass Security + +1. **ECCVM uses native `bb::fq` coordinates**: + - `ECCVMTranscriptBuilder::compute_rows(op_queue->get_eccvm_ops(), ...)` + - `get_eccvm_ops()` returns `ECCVMOperation` structs with `base_point` (native `Point`) + - `bb::fq` is **always canonical** - the field class auto-reduces on construction + - Even a modified binary cannot store non-canonical values in `bb::fq` + +2. **Translator uses limb data from op_queue**: + - `ultra_ops_table` contains `(x_lo, x_hi, y_lo, y_hi)` limbs + - These limbs come from `construct_and_populate_ultra_ops()` which decomposes from native `Point` + - A modified prover could potentially alter this decomposition + +3. **Translation check catches inconsistency**: + - ECCVM's `accumulated_result` is computed from canonical `transcript_Px`, `transcript_Py` evaluations + - Translator computes its accumulator from the limbs in `ultra_ops_table` + - `TranslatorAccumulatorTransferRelation` enforces equality at the result row + - If limbs differ (canonical vs aliased), the accumulators differ → **verification fails** + +### Key Invariants + +1. **`bb::fq` is inherently canonical**: Montgomery representation requires `value < modulus` +2. **ECCVM transcript comes from native Points** +3. **Translation check** +4. **Merge protocol final table commitment is the input to the Translator Verifier** + +### Relevant Code Locations + +- Translation data flow: `goblin_verifier.cpp:48-59` +- ECCVM accumulated_result: `eccvm_verifier.cpp:244-256` +- Translator receives accumulated_result: `translator_verifier.cpp:122-126, 159-160` +- TranslatorAccumulatorTransferRelation: `translator_extra_relations_impl.hpp` +- Op queue dual representation: `ecc_op_queue.hpp:198-207` diff --git a/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.hpp b/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.hpp index 2b29493a2d04..048ea09d69cd 100644 --- a/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.hpp +++ b/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.hpp @@ -60,6 +60,7 @@ class FrCodec { /** * @brief Converts 2 bb::fr elements to fq * @details Splits into 136-bit lower chunk and 118-bit upper chunk to mirror stdlib bigfield limbs (68-bit each). + * Rejects aliased values (>= fq::modulus) to ensure canonical representation. */ static fq convert_grumpkin_fr_from_bn254_frs(std::span fr_vec) { @@ -76,6 +77,10 @@ class FrCodec { const uint256_t value = uint256_t(fr_vec[0]) + (uint256_t(fr_vec[1]) << (NUM_LIMB_BITS * 2)); + // Reject aliased values to ensure canonical representation. + // This matches the circuit behavior in StdlibCodec where assert_is_in_field is called. + BB_ASSERT_LT(value, fq::modulus, "Non-canonical field element: value >= fq::modulus"); + return fq(value); } diff --git a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.hpp b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.hpp index e9ebc30f8c28..4638352442da 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.hpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.hpp @@ -4,6 +4,13 @@ // external_2: { status: not started, auditors: [], date: YYYY-MM-DD } // ===================== +/** + * @file field_conversion.hpp + * @brief StdlibCodec for in-circuit (recursive) verification transcript handling. + * + * @details See ecc/fields/CODEC_README.md + */ + #pragma once #include "barretenberg/common/assert.hpp" @@ -113,13 +120,14 @@ template class StdlibCodec { * @details Deserializes a vector of in-circuit `fr`s, i.e. `field_t` elements, into * - `field_t` — no conversion needed * - * - \ref bb::stdlib::bigfield< Builder, T > "bigfield" — 2 input `field_t`s are fed into `bigfield` constructor - * that ensures that they are properly constrained. Specific to \ref UltraCircuitBuilder_ "UltraCircuitBuilder". + * - \ref bb::stdlib::bigfield< Builder, T > "bigfield" — 2 input `field_t`s are fed into `bigfield` constructor, + * then `assert_is_in_field()` is called to reject aliased values (>= Fq::modulus). This ensures consistency with + * native FrCodec which also rejects aliases. Specific to \ref UltraCircuitBuilder_ "UltraCircuitBuilder". * - * - \ref bb::stdlib::goblin_field< Builder > "goblin field element" — in contrast to `bigfield`, range constraints - * are performed in `Translator` (see \ref TranslatorDeltaRangeConstraintRelationImpl "Translator Range Constraint" - * relation). Feed the limbs to the `bigfield` constructor and set the `point_at_infinity` flag derived by the - * `check_point_at_infinity` method. Specific to \ref MegaCircuitBuilder_ "MegaCircuitBuilder". + * - \ref bb::stdlib::goblin_field< Builder > "goblin field element" — stores limbs as-is without in-circuit + * modulus check. Range constraints are deferred to Translator circuit (see \ref + * TranslatorDeltaRangeConstraintRelationImpl "Translator Range Constraint" relation). Alias rejection is ensured + * by the ECCVM ↔ Translator translation check. Specific to \ref MegaCircuitBuilder_ "MegaCircuitBuilder". * * - \ref bb::stdlib::element_goblin::goblin_element< Builder_, Fq, Fr, NativeGroup > "bn254 goblin point" — input * vector of size 4 is transformed into a pair of `goblin_field` elements, which are fed into the relevant @@ -155,8 +163,14 @@ template class StdlibCodec { if constexpr (IsAnyOf) { // Case 1: input type matches the output type return fr_vec[0]; - } else if constexpr (IsAnyOf>) { - // Cases 2 and 3: a bigfield/goblin_field element is reconstructed from low and high limbs. + } else if constexpr (IsAnyOf) { + // Case 2: bigfield is reconstructed from low and high limbs with in-field validation. + // This ensures aliased values (>= Fq::modulus) are rejected. + T result(fr_vec[0], fr_vec[1]); + result.assert_is_in_field(); + return result; + } else if constexpr (IsAnyOf>) { + // Case 3: goblin_field stores limbs as-is; range validation is deferred to Translator circuit. return T(fr_vec[0], fr_vec[1]); } else if constexpr (IsAnyOf) { // Case 4 and 5: Convert a vector of frs to a group element diff --git a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.test.cpp b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.test.cpp index 28aeb2457a7e..36dfe2eaf75c 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.test.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.test.cpp @@ -354,44 +354,50 @@ TYPED_TEST(stdlib_field_conversion, GateCountScalarDeserialization) /** * @brief Measure gate counts for bigfield deserialization + * @details Includes assert_is_in_field to reject aliased values (>= Fq::modulus) */ TYPED_TEST(stdlib_field_conversion, GateCountBigfieldDeserialization) { - // Deserializing a single bigfield element is expensive due to creating new ranges for range constraints - this->template check_deserialization_gate_count>([] { return bb::fq::random_element(); }, 3483); + // Deserializing a single bigfield element is expensive due to: + // 1. Creating new ranges for range constraints + // 2. assert_is_in_field check (~32 gates) to reject aliases + this->template check_deserialization_gate_count>([] { return bb::fq::random_element(); }, 3515); } /** * @brief Measure gate counts for multiple bigfield deserializations - * @details Range constraints are batched, making subsequent bigfields much cheaper + * @details Range constraints are batched, making subsequent bigfields much cheaper. + * Each bigfield adds ~32 gates for assert_is_in_field. */ TYPED_TEST(stdlib_field_conversion, GateCountMultipleBigfieldDeserialization) { - this->template check_deserialization_gate_count>([] { return bb::fq::random_element(); }, 3608, 10); + this->template check_deserialization_gate_count>([] { return bb::fq::random_element(); }, 3914, 10); } /** * @brief Measure gate counts for BN254 point deserialization - * @details Includes bigfield reconstruction + point-at-infinity check + on-curve validation + * @details Includes bigfield reconstruction + point-at-infinity check + on-curve validation. + * For Ultra, each coordinate (x, y) gets assert_is_in_field (~32 gates each). */ TYPED_TEST(stdlib_field_conversion, GateCountBN254PointDeserialization) { using Builder = TypeParam; - // Ultra: full bigfield construction + on-curve validation + // Ultra: full bigfield construction + on-curve validation + assert_is_in_field for x and y // Mega: only is_infinity check, range constraint and on_curve validation deferred to ECCVM and Translator - constexpr uint32_t expected = std::is_same_v ? 3789 : 5; + constexpr uint32_t expected = std::is_same_v ? 3850 : 5; this->template check_deserialization_gate_count>( [] { return curve::BN254::AffineElement::random_element(); }, expected); } /** * @brief Measure gate counts for multiple BN254 point deserializations + * @details Each point adds ~62 gates for assert_is_in_field (x and y coordinates). */ TYPED_TEST(stdlib_field_conversion, GateCountMultipleBN254PointDeserialization) { using Builder = TypeParam; - constexpr uint32_t expected = std::is_same_v ? 4986 : 50; + constexpr uint32_t expected = std::is_same_v ? 5601 : 50; this->template check_deserialization_gate_count>( [] { return curve::BN254::AffineElement::random_element(); }, expected, 10); } @@ -524,13 +530,14 @@ TYPED_TEST(stdlib_field_conversion, BothCodecsRejectPointAtInfinityAlias) /** * @brief Test the upper bound on fq values accepted by both codecs. - * @details Both codecs use limb bounds: low < 2^136, high < 2^118. - * This allows values in [0, 2^254 - 1]. Values >= 2^254 are rejected. + * @details With assert_is_in_field, only canonical values < Fq::modulus are accepted. + * Values >= Fq::modulus are rejected (aliases), and values >= 2^254 are rejected (overflow). * - * - 2^254 - 1: Maximum valid value (accepted, reduces to 2^254 - 1 - modulus) - * - 2^254: Exceeds high limb bound (rejected by both codecs) + * - Fq::modulus - 1: Maximum canonical value (accepted) + * - 2^254 - 1: Alias value >= Fq::modulus (rejected by assert_is_in_field) + * - 2^254: Exceeds high limb bound (rejected by range constraints) */ -TYPED_TEST(stdlib_field_conversion, BothCodecsAcceptMaxAliasRejectOverflow) +TYPED_TEST(stdlib_field_conversion, BothCodecsAcceptCanonicalRejectAlias) { using Builder = TypeParam; using Codec = StdlibCodec>; @@ -538,22 +545,17 @@ TYPED_TEST(stdlib_field_conversion, BothCodecsAcceptMaxAliasRejectOverflow) constexpr uint64_t NUM_LIMB_BITS = 68; constexpr uint64_t LOW_BITS = NUM_LIMB_BITS * 2; // 136 - constexpr uint64_t HIGH_BITS = 254 - LOW_BITS; // 118 - // Test 1: 2^254 - 1 is accepted (max valid alias) + // Test 1: Fq::modulus - 1 is accepted (max canonical value) { - const uint256_t max_value = (uint256_t(1) << 254) - 1; - const uint256_t low_limb = max_value & ((uint256_t(1) << LOW_BITS) - 1); // 2^136 - 1 - const uint256_t high_limb = max_value >> LOW_BITS; // 2^118 - 1 - - // Verify limbs are at their max valid values - EXPECT_EQ(low_limb, (uint256_t(1) << LOW_BITS) - 1); - EXPECT_EQ(high_limb, (uint256_t(1) << HIGH_BITS) - 1); + const uint256_t max_canonical = bb::fq::modulus - 1; + const uint256_t low_limb = max_canonical & ((uint256_t(1) << LOW_BITS) - 1); + const uint256_t high_limb = max_canonical >> LOW_BITS; // Native codec: accepts std::vector native_fields = { bb::fr(low_limb), bb::fr(high_limb) }; auto native_result = FrCodec::deserialize_from_fields(native_fields); - EXPECT_EQ(uint256_t(native_result), max_value - bb::fq::modulus); // Reduced + EXPECT_EQ(uint256_t(native_result), max_canonical); // Circuit codec: accepts Builder builder; @@ -563,8 +565,34 @@ TYPED_TEST(stdlib_field_conversion, BothCodecsAcceptMaxAliasRejectOverflow) EXPECT_TRUE(CircuitChecker::check(builder)); } - // Test 2: 2^254 is rejected (high limb = 2^118 exceeds bound) + // Test 2: 2^254 - 1 is rejected (alias >= Fq::modulus) + { + const uint256_t alias_value = (uint256_t(1) << 254) - 1; + const uint256_t low_limb = alias_value & ((uint256_t(1) << LOW_BITS) - 1); + const uint256_t high_limb = alias_value >> LOW_BITS; + + // Verify this is an alias (>= modulus) + EXPECT_GE(alias_value, bb::fq::modulus); + + // Native codec: rejects (value >= fq::modulus) + std::vector native_fields = { bb::fr(low_limb), bb::fr(high_limb) }; + EXPECT_THROW(FrCodec::deserialize_from_fields(native_fields), std::runtime_error); + + // Circuit codec: rejects via assert_is_in_field + { + BB_DISABLE_ASSERTS(); + Builder builder; + std::vector> circuit_fields = { field_t::from_witness(&builder, bb::fr(low_limb)), + field_t::from_witness(&builder, + bb::fr(high_limb)) }; + [[maybe_unused]] auto circuit_result = Codec::template deserialize_from_fields(circuit_fields); + EXPECT_FALSE(CircuitChecker::check(builder)); + } + } + + // Test 3: 2^254 is rejected (high limb = 2^118 exceeds bound) { + constexpr uint64_t HIGH_BITS = 254 - LOW_BITS; // 118 const uint256_t overflow_value = uint256_t(1) << 254; const uint256_t low_limb = 0; const uint256_t high_limb = uint256_t(1) << HIGH_BITS; // 2^118 (exactly at boundary) From 59f601f528737d79d4bb9e03d55c5dfc86c03bbe Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Thu, 8 Jan 2026 12:20:22 +0000 Subject: [PATCH 11/24] clean up tests + fix comment --- .../field/field_conversion.test.cpp | 76 +++++++++---------- .../stdlib/primitives/field/field_utils.cpp | 1 - 2 files changed, 34 insertions(+), 43 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.test.cpp b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.test.cpp index 36dfe2eaf75c..e2e03250f5d4 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.test.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.test.cpp @@ -354,20 +354,16 @@ TYPED_TEST(stdlib_field_conversion, GateCountScalarDeserialization) /** * @brief Measure gate counts for bigfield deserialization - * @details Includes assert_is_in_field to reject aliased values (>= Fq::modulus) */ TYPED_TEST(stdlib_field_conversion, GateCountBigfieldDeserialization) { - // Deserializing a single bigfield element is expensive due to: - // 1. Creating new ranges for range constraints - // 2. assert_is_in_field check (~32 gates) to reject aliases + // Deserializing a single bigfield element is expensive due to creating new ranges for range constraints this->template check_deserialization_gate_count>([] { return bb::fq::random_element(); }, 3515); } /** * @brief Measure gate counts for multiple bigfield deserializations - * @details Range constraints are batched, making subsequent bigfields much cheaper. - * Each bigfield adds ~32 gates for assert_is_in_field. + * @details Range constraints are batched, making subsequent bigfields much cheaper */ TYPED_TEST(stdlib_field_conversion, GateCountMultipleBigfieldDeserialization) { @@ -376,8 +372,7 @@ TYPED_TEST(stdlib_field_conversion, GateCountMultipleBigfieldDeserialization) /** * @brief Measure gate counts for BN254 point deserialization - * @details Includes bigfield reconstruction + point-at-infinity check + on-curve validation. - * For Ultra, each coordinate (x, y) gets assert_is_in_field (~32 gates each). + * @details Includes bigfield reconstruction + point-at-infinity check + on-curve validation */ TYPED_TEST(stdlib_field_conversion, GateCountBN254PointDeserialization) { @@ -391,7 +386,6 @@ TYPED_TEST(stdlib_field_conversion, GateCountBN254PointDeserialization) /** * @brief Measure gate counts for multiple BN254 point deserializations - * @details Each point adds ~62 gates for assert_is_in_field (x and y coordinates). */ TYPED_TEST(stdlib_field_conversion, GateCountMultipleBN254PointDeserialization) { @@ -449,18 +443,18 @@ TYPED_TEST(stdlib_field_conversion, GateCountUnivariateDeserialization) } /** - * @brief Failure test for deserializing a pair of limbs as a bigfield, where one of the limbs exceeds the strict 2^136 - * upper bound. - * @details Verifies both native (FrCodec) and circuit (StdlibCodec) codecs reject out-of-range limbs. - * Native codec uses BB_ASSERT to reject, circuit codec uses in-circuit constraints. + * @brief Failure test for deserializing limbs that exceed their range bounds. + * @details The encoding uses low_limb (136 bits) and high_limb (118 bits). + * This test verifies that a high_limb value exceeding 2^118 is rejected by both codecs. + * Native codec uses BB_ASSERT, circuit codec uses in-circuit range constraints. */ -TYPED_TEST(stdlib_field_conversion, BigfieldDeserializationFails) +TYPED_TEST(stdlib_field_conversion, BigfieldDeserializationFailsOnLimbOverflow) { using Builder = TypeParam; using Codec = StdlibCodec>; bb::fr low_limb = bb::fr(0); - // Create a limb from the value 2^136, that does not satisfy the condition < 2^136. + // 2^136 placed in high_limb position far exceeds the 2^118 bound for high limbs bb::fr high_limb = bb::fr(uint256_t(1) << (2 * fq::NUM_LIMB_BITS)); // Test 1: Native codec should reject via BB_ASSERT (asserts enabled) @@ -529,13 +523,12 @@ TYPED_TEST(stdlib_field_conversion, BothCodecsRejectPointAtInfinityAlias) } /** - * @brief Test the upper bound on fq values accepted by both codecs. + * @brief Test that both codecs accept canonical values and reject aliases. * @details With assert_is_in_field, only canonical values < Fq::modulus are accepted. - * Values >= Fq::modulus are rejected (aliases), and values >= 2^254 are rejected (overflow). * - * - Fq::modulus - 1: Maximum canonical value (accepted) - * - 2^254 - 1: Alias value >= Fq::modulus (rejected by assert_is_in_field) - * - 2^254: Exceeds high limb bound (rejected by range constraints) + * - q - 1: Maximum canonical value (accepted) + * - q: Smallest alias (rejected) + * - Large value between q and 2^254: Also rejected (not just boundary) */ TYPED_TEST(stdlib_field_conversion, BothCodecsAcceptCanonicalRejectAlias) { @@ -545,17 +538,21 @@ TYPED_TEST(stdlib_field_conversion, BothCodecsAcceptCanonicalRejectAlias) constexpr uint64_t NUM_LIMB_BITS = 68; constexpr uint64_t LOW_BITS = NUM_LIMB_BITS * 2; // 136 + const uint256_t LOW_MASK = (uint256_t(1) << LOW_BITS) - 1; - // Test 1: Fq::modulus - 1 is accepted (max canonical value) + auto split_to_limbs = [&](const uint256_t& value) -> std::pair { + return { value & LOW_MASK, value >> LOW_BITS }; + }; + + // Test 1: q - 1 is accepted (max canonical value) { - const uint256_t max_canonical = bb::fq::modulus - 1; - const uint256_t low_limb = max_canonical & ((uint256_t(1) << LOW_BITS) - 1); - const uint256_t high_limb = max_canonical >> LOW_BITS; + const uint256_t value = bb::fq::modulus - 1; + const auto [low_limb, high_limb] = split_to_limbs(value); // Native codec: accepts std::vector native_fields = { bb::fr(low_limb), bb::fr(high_limb) }; auto native_result = FrCodec::deserialize_from_fields(native_fields); - EXPECT_EQ(uint256_t(native_result), max_canonical); + EXPECT_EQ(uint256_t(native_result), value); // Circuit codec: accepts Builder builder; @@ -565,16 +562,12 @@ TYPED_TEST(stdlib_field_conversion, BothCodecsAcceptCanonicalRejectAlias) EXPECT_TRUE(CircuitChecker::check(builder)); } - // Test 2: 2^254 - 1 is rejected (alias >= Fq::modulus) + // Test 2: q is rejected (smallest alias) { - const uint256_t alias_value = (uint256_t(1) << 254) - 1; - const uint256_t low_limb = alias_value & ((uint256_t(1) << LOW_BITS) - 1); - const uint256_t high_limb = alias_value >> LOW_BITS; + const uint256_t value = bb::fq::modulus; + const auto [low_limb, high_limb] = split_to_limbs(value); - // Verify this is an alias (>= modulus) - EXPECT_GE(alias_value, bb::fq::modulus); - - // Native codec: rejects (value >= fq::modulus) + // Native codec: rejects std::vector native_fields = { bb::fr(low_limb), bb::fr(high_limb) }; EXPECT_THROW(FrCodec::deserialize_from_fields(native_fields), std::runtime_error); @@ -590,21 +583,20 @@ TYPED_TEST(stdlib_field_conversion, BothCodecsAcceptCanonicalRejectAlias) } } - // Test 3: 2^254 is rejected (high limb = 2^118 exceeds bound) + // Test 3: Large value between q and 2^254 is rejected { - constexpr uint64_t HIGH_BITS = 254 - LOW_BITS; // 118 - const uint256_t overflow_value = uint256_t(1) << 254; - const uint256_t low_limb = 0; - const uint256_t high_limb = uint256_t(1) << HIGH_BITS; // 2^118 (exactly at boundary) + const uint256_t value = (uint256_t(1) << 254) - 1; // 2^254 - 1, well above modulus + const auto [low_limb, high_limb] = split_to_limbs(value); - // Verify this represents 2^254 - EXPECT_EQ(low_limb + (high_limb << LOW_BITS), overflow_value); + // Verify this is indeed between modulus and 2^254 + EXPECT_GT(value, bb::fq::modulus); + EXPECT_LT(value, uint256_t(1) << 254); - // Native codec: rejects (high limb >= 2^118) + // Native codec: rejects std::vector native_fields = { bb::fr(low_limb), bb::fr(high_limb) }; EXPECT_THROW(FrCodec::deserialize_from_fields(native_fields), std::runtime_error); - // Circuit codec: rejects via range constraints + // Circuit codec: rejects via assert_is_in_field { BB_DISABLE_ASSERTS(); Builder builder; diff --git a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp index 430b7bab66ae..1009168b54b9 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp @@ -41,7 +41,6 @@ void validate_split_in_field_unsafe(const field_t& lo, : field_t::from_witness(ctx, typename field_t::native(need_borrow)); // Constrain borrow to be boolean (0 or 1) unless both inputs are constant. - // This prevents a malicious prover from using non-boolean borrow values. if (!both_constant) { // We need to manually propagate the origin tag borrow.set_origin_tag(lo.is_constant() ? hi.get_origin_tag() : lo.get_origin_tag()); From aeab64c14771d7d0bc41e5413b32dfb803aa1126 Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Thu, 8 Jan 2026 16:12:52 +0000 Subject: [PATCH 12/24] fix tests --- .../dsl/acir_format/gate_count_constants.hpp | 20 ++-- .../eccvm/eccvm_transcript.test.cpp | 2 - .../src/barretenberg/goblin/merge.test.cpp | 14 +-- .../hypernova_decider_verifier.test.cpp | 47 ++++---- .../hypernova/hypernova_verifier.test.cpp | 103 +++++++++--------- .../poseidon2.circuit.failure.test.cpp | 44 +------- .../stdlib/hash/poseidon2/poseidon2.test.cpp | 21 +++- .../translator_vm/translator.test.cpp | 100 ++++++++--------- .../ultra_honk/mega_transcript.test.cpp | 2 - .../ultra_honk/ultra_transcript.test.cpp | 2 - 10 files changed, 159 insertions(+), 196 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/dsl/acir_format/gate_count_constants.hpp b/barretenberg/cpp/src/barretenberg/dsl/acir_format/gate_count_constants.hpp index d6f71781772c..665a9225778e 100644 --- a/barretenberg/cpp/src/barretenberg/dsl/acir_format/gate_count_constants.hpp +++ b/barretenberg/cpp/src/barretenberg/dsl/acir_format/gate_count_constants.hpp @@ -55,7 +55,7 @@ template inline constexpr size_t ASSERT_EQUALITY = ZERO_GATE // Honk Recursion Constants // ======================================== -inline constexpr size_t ROOT_ROLLUP_GATE_COUNT = 12986977; +inline constexpr size_t ROOT_ROLLUP_GATE_COUNT = 12995065; template constexpr std::tuple HONK_RECURSION_CONSTANTS( @@ -67,26 +67,26 @@ constexpr std::tuple HONK_RECURSION_CONSTANTS( if constexpr (std::is_same_v>) { switch (mode) { case PredicateTestCase::ConstantTrue: - return std::make_tuple(722844, 0); + return std::make_tuple(726842, 0); case PredicateTestCase::WitnessTrue: case PredicateTestCase::WitnessFalse: - return std::make_tuple(723995, 0); + return std::make_tuple(727993, 0); } } else if constexpr (std::is_same_v>) { switch (mode) { case PredicateTestCase::ConstantTrue: - return std::make_tuple(766262, 0); + return std::make_tuple(770506, 0); case PredicateTestCase::WitnessTrue: case PredicateTestCase::WitnessFalse: - return std::make_tuple(767515, 0); + return std::make_tuple(771759, 0); } } else if constexpr (std::is_same_v>) { switch (mode) { case PredicateTestCase::ConstantTrue: - return std::make_tuple(723163, 0); + return std::make_tuple(727160, 0); case PredicateTestCase::WitnessTrue: case PredicateTestCase::WitnessFalse: - return std::make_tuple(724462, 0); + return std::make_tuple(728459, 0); } } else if constexpr (std::is_same_v>) { switch (mode) { @@ -108,7 +108,7 @@ constexpr std::tuple HONK_RECURSION_CONSTANTS( if (mode != PredicateTestCase::ConstantTrue) { bb::assert_failure("Unhandled mode in MegaZKRecursiveFlavor."); } - return std::make_tuple(814519, 0); + return std::make_tuple(817287, 0); } else { bb::assert_failure("Unhandled recursive flavor."); } @@ -119,7 +119,7 @@ constexpr std::tuple HONK_RECURSION_CONSTANTS( // ======================================== // Gate count for Chonk recursive verification (UltraRollup builder) -inline constexpr size_t CHONK_RECURSION_GATES = 2368439; +inline constexpr size_t CHONK_RECURSION_GATES = 2385506; // ======================================== // Hypernova Recursion Constants @@ -153,7 +153,7 @@ inline constexpr size_t HIDING_KERNEL_ULTRA_OPS = 124; // ======================================== // Gate count for ECCVM recursive verifier (Ultra-arithmetized) -inline constexpr size_t ECCVM_RECURSIVE_VERIFIER_GATE_COUNT = 214950; +inline constexpr size_t ECCVM_RECURSIVE_VERIFIER_GATE_COUNT = 220455; // ======================================== // Goblin AVM Recursive Verifier Constants diff --git a/barretenberg/cpp/src/barretenberg/eccvm/eccvm_transcript.test.cpp b/barretenberg/cpp/src/barretenberg/eccvm/eccvm_transcript.test.cpp index 5836c59dd019..614b499122cf 100644 --- a/barretenberg/cpp/src/barretenberg/eccvm/eccvm_transcript.test.cpp +++ b/barretenberg/cpp/src/barretenberg/eccvm/eccvm_transcript.test.cpp @@ -135,9 +135,7 @@ class ECCVMTranscriptTests : public ::testing::Test { manifest_expected.add_entry(round, "LOOKUP_INVERSES", frs_per_G); manifest_expected.add_entry(round, "Z_PERM", frs_per_G); manifest_expected.add_challenge(round, "Sumcheck:alpha"); - for (size_t i = 0; i < CONST_ECCVM_LOG_N; i++) { - round++; std::string label = "Sumcheck:gate_challenge_" + std::to_string(i); manifest_expected.add_challenge(round, label); } diff --git a/barretenberg/cpp/src/barretenberg/goblin/merge.test.cpp b/barretenberg/cpp/src/barretenberg/goblin/merge.test.cpp index b1f24e6d9fc2..1b1a473ece20 100644 --- a/barretenberg/cpp/src/barretenberg/goblin/merge.test.cpp +++ b/barretenberg/cpp/src/barretenberg/goblin/merge.test.cpp @@ -542,29 +542,25 @@ class MergeTranscriptTests : public ::testing::Test { size_t round = 0; - // Round 0: Prover sends shift_size and merged table commitments + // Round 0: Prover sends shift_size and merged table commitments, gets degree check challenges manifest_expected.add_entry(round, "shift_size", frs_per_uint32); for (size_t idx = 0; idx < NUM_WIRES; ++idx) { manifest_expected.add_entry(round, "MERGED_TABLE_" + std::to_string(idx), frs_per_G); } - // Verifier generates degree check challenges manifest_expected.add_challenge(round, "LEFT_TABLE_DEGREE_CHECK_0"); manifest_expected.add_challenge(round, "LEFT_TABLE_DEGREE_CHECK_1"); manifest_expected.add_challenge(round, "LEFT_TABLE_DEGREE_CHECK_2"); manifest_expected.add_challenge(round, "LEFT_TABLE_DEGREE_CHECK_3"); - // Round 1: Verifier generates Shplonk batching challenges, Prover sends degree check polynomial commitment + // Round 1: Batching challenges + kappa, then send batched polynomial commitment round++; for (size_t idx = 0; idx < 13; ++idx) { manifest_expected.add_challenge(round, "SHPLONK_MERGE_BATCHING_CHALLENGE_" + std::to_string(idx)); } - manifest_expected.add_entry(round, "REVERSED_BATCHED_LEFT_TABLES", frs_per_G); - - // Round 2: Verifier generates evaluation challenge kappa - round++; manifest_expected.add_challenge(round, "kappa"); + manifest_expected.add_entry(round, "REVERSED_BATCHED_LEFT_TABLES", frs_per_G); - // Round 3: Verifier generates Shplonk opening challenge, Prover sends all evaluations and quotient + // Round 2: Shplonk opening challenge, then send all evaluations and quotient round++; manifest_expected.add_challenge(round, "shplonk_opening_challenge"); for (size_t idx = 0; idx < NUM_WIRES; ++idx) { @@ -579,7 +575,7 @@ class MergeTranscriptTests : public ::testing::Test { manifest_expected.add_entry(round, "REVERSED_BATCHED_LEFT_TABLES_EVAL", frs_per_Fr); manifest_expected.add_entry(round, "SHPLONK_BATCHED_QUOTIENT", frs_per_G); - // Round 4: KZG opening proof with masking challenge + // Round 3: KZG masking challenge, then send W commitment round++; manifest_expected.add_challenge(round, "KZG:masking_challenge"); manifest_expected.add_entry(round, "KZG:W", frs_per_G); diff --git a/barretenberg/cpp/src/barretenberg/hypernova/hypernova_decider_verifier.test.cpp b/barretenberg/cpp/src/barretenberg/hypernova/hypernova_decider_verifier.test.cpp index c02515419fee..0fb3d95d7564 100644 --- a/barretenberg/cpp/src/barretenberg/hypernova/hypernova_decider_verifier.test.cpp +++ b/barretenberg/cpp/src/barretenberg/hypernova/hypernova_decider_verifier.test.cpp @@ -50,13 +50,14 @@ class HypernovaDeciderVerifierTests : public ::testing::Test { /** * @brief Build the expected transcript manifest for HyperNova decider - * @details Manifest tracking is enabled after folding (which uses 50 rounds), so only - * decider rounds are tracked. Round numbers continue from folding: - * - Round 50: rho challenge (batching for Gemini) - * - Round 51: Gemini FOLD commitments -> Gemini:r challenge - * - Round 52: Gemini evaluations -> Shplonk:nu challenge - * - Round 53: Shplonk:Q commitment -> Shplonk:z challenge - * - Round 54: KZG:W commitment -> KZG:masking_challenge + * @details Manifest tracking is enabled after folding (which uses 48 rounds, 0-47), so only + * decider rounds are tracked. Since folding ends with a challenge (claim_batching_challenge) + * at round 47, and the decider starts with a challenge (rho), they are in the same round: + * - Round 47: rho challenge (same round as folding's claim_batching_challenge) + * - Round 48: Gemini FOLD commitments -> Gemini:r challenge + * - Round 49: Gemini evaluations -> Shplonk:nu challenge + * - Round 50: Shplonk:Q commitment -> Shplonk:z challenge + * - Round 51: KZG:W commitment -> KZG:masking_challenge */ static TranscriptManifest build_expected_decider_manifest() { @@ -64,30 +65,32 @@ class HypernovaDeciderVerifierTests : public ::testing::Test { constexpr size_t frs_per_G = FrCodec::calc_num_fields(); constexpr size_t NUM_GEMINI_FOLDS = NativeFlavor::VIRTUAL_LOG_N - 1; // 20 constexpr size_t NUM_GEMINI_EVALS = NativeFlavor::VIRTUAL_LOG_N; // 21 - constexpr size_t FOLDING_ROUNDS = 50; // Rounds used by folding verifier + // Folding uses 48 rounds (0-47). The last round (47) ends with claim_batching_challenge. + // Since rho is also a challenge with no data between, it stays in round 47. + constexpr size_t LAST_FOLDING_ROUND = 47; - // Round 50: rho challenge - manifest.add_challenge(FOLDING_ROUNDS, "rho"); + // Round 47: rho challenge (same round as folding's claim_batching_challenge) + manifest.add_challenge(LAST_FOLDING_ROUND, "rho"); - // Round 51: Gemini FOLD commitments -> Gemini:r + // Round 48: Gemini FOLD commitments -> Gemini:r for (size_t i = 1; i <= NUM_GEMINI_FOLDS; ++i) { - manifest.add_entry(FOLDING_ROUNDS + 1, "Gemini:FOLD_" + std::to_string(i), frs_per_G); + manifest.add_entry(LAST_FOLDING_ROUND + 1, "Gemini:FOLD_" + std::to_string(i), frs_per_G); } - manifest.add_challenge(FOLDING_ROUNDS + 1, "Gemini:r"); + manifest.add_challenge(LAST_FOLDING_ROUND + 1, "Gemini:r"); - // Round 52: Gemini evaluations -> Shplonk:nu + // Round 49: Gemini evaluations -> Shplonk:nu for (size_t i = 1; i <= NUM_GEMINI_EVALS; ++i) { - manifest.add_entry(FOLDING_ROUNDS + 2, "Gemini:a_" + std::to_string(i), 1); + manifest.add_entry(LAST_FOLDING_ROUND + 2, "Gemini:a_" + std::to_string(i), 1); } - manifest.add_challenge(FOLDING_ROUNDS + 2, "Shplonk:nu"); + manifest.add_challenge(LAST_FOLDING_ROUND + 2, "Shplonk:nu"); - // Round 53: Shplonk:Q -> Shplonk:z - manifest.add_entry(FOLDING_ROUNDS + 3, "Shplonk:Q", frs_per_G); - manifest.add_challenge(FOLDING_ROUNDS + 3, "Shplonk:z"); + // Round 50: Shplonk:Q -> Shplonk:z + manifest.add_entry(LAST_FOLDING_ROUND + 3, "Shplonk:Q", frs_per_G); + manifest.add_challenge(LAST_FOLDING_ROUND + 3, "Shplonk:z"); - // Round 54: KZG:W -> KZG:masking_challenge - manifest.add_entry(FOLDING_ROUNDS + 4, "KZG:W", frs_per_G); - manifest.add_challenge(FOLDING_ROUNDS + 4, "KZG:masking_challenge"); + // Round 51: KZG:W -> KZG:masking_challenge + manifest.add_entry(LAST_FOLDING_ROUND + 4, "KZG:W", frs_per_G); + manifest.add_challenge(LAST_FOLDING_ROUND + 4, "KZG:masking_challenge"); return manifest; } diff --git a/barretenberg/cpp/src/barretenberg/hypernova/hypernova_verifier.test.cpp b/barretenberg/cpp/src/barretenberg/hypernova/hypernova_verifier.test.cpp index e8ab97c30e08..9c7a94b92330 100644 --- a/barretenberg/cpp/src/barretenberg/hypernova/hypernova_verifier.test.cpp +++ b/barretenberg/cpp/src/barretenberg/hypernova/hypernova_verifier.test.cpp @@ -168,77 +168,82 @@ class HypernovaFoldingVerifierTests : public ::testing::Test { constexpr size_t frs_per_G = FrCodec::calc_num_fields(); constexpr size_t NUM_SUMCHECK_UNIVARIATES = NativeFlavor::VIRTUAL_LOG_N; // 21 - // Round 0: Oink preamble + wires + ECC ops + databus - manifest.add_entry(0, "vk_hash", 1); + size_t round = 0; + + // Round 0: Oink preamble + wires + ECC ops + databus -> eta challenges + manifest.add_challenge(round, std::array{ "eta", "eta_two", "eta_three" }); + manifest.add_entry(round, "vk_hash", 1); for (size_t i = 0; i < 4; ++i) { - manifest.add_entry(0, "public_input_" + std::to_string(i), 1); + manifest.add_entry(round, "public_input_" + std::to_string(i), 1); } for (const auto& wire : { "W_L", "W_R", "W_O" }) { - manifest.add_entry(0, wire, frs_per_G); + manifest.add_entry(round, wire, frs_per_G); } for (const auto& wire : { "ECC_OP_WIRE_1", "ECC_OP_WIRE_2", "ECC_OP_WIRE_3", "ECC_OP_WIRE_4" }) { - manifest.add_entry(0, wire, frs_per_G); + manifest.add_entry(round, wire, frs_per_G); } for (const auto& bus : { "CALLDATA", "SECONDARY_CALLDATA", "RETURN_DATA" }) { - manifest.add_entry(0, bus, frs_per_G); - manifest.add_entry(0, std::string(bus) + "_READ_COUNTS", frs_per_G); - manifest.add_entry(0, std::string(bus) + "_READ_TAGS", frs_per_G); + manifest.add_entry(round, bus, frs_per_G); + manifest.add_entry(round, std::string(bus) + "_READ_COUNTS", frs_per_G); + manifest.add_entry(round, std::string(bus) + "_READ_TAGS", frs_per_G); } - manifest.add_challenge(0, std::array{ "eta", "eta_two", "eta_three" }); - - // Round 1: lookup + w_4 - manifest.add_entry(1, "LOOKUP_READ_COUNTS", frs_per_G); - manifest.add_entry(1, "LOOKUP_READ_TAGS", frs_per_G); - manifest.add_entry(1, "W_4", frs_per_G); - manifest.add_challenge(1, std::array{ "beta", "gamma" }); - - // Round 2: inverses + z_perm - manifest.add_entry(2, "LOOKUP_INVERSES", frs_per_G); - manifest.add_entry(2, "CALLDATA_INVERSES", frs_per_G); - manifest.add_entry(2, "SECONDARY_CALLDATA_INVERSES", frs_per_G); - manifest.add_entry(2, "RETURN_DATA_INVERSES", frs_per_G); - manifest.add_entry(2, "Z_PERM", frs_per_G); - manifest.add_challenge(2, "alpha"); - - // Round 3: gate challenge - manifest.add_challenge(3, "HypernovaFoldingProver:gate_challenge"); - - // Rounds 4-24: main sumcheck univariates + round++; + + // Round 1: lookup + w_4 -> beta, gamma challenges + manifest.add_challenge(round, std::array{ "beta", "gamma" }); + manifest.add_entry(round, "LOOKUP_READ_COUNTS", frs_per_G); + manifest.add_entry(round, "LOOKUP_READ_TAGS", frs_per_G); + manifest.add_entry(round, "W_4", frs_per_G); + round++; + + // Round 2: inverses + z_perm -> alpha + gate_challenge (consecutive challenges in same round) + manifest.add_challenge(round, "alpha"); + manifest.add_challenge(round, "HypernovaFoldingProver:gate_challenge"); + manifest.add_entry(round, "LOOKUP_INVERSES", frs_per_G); + manifest.add_entry(round, "CALLDATA_INVERSES", frs_per_G); + manifest.add_entry(round, "SECONDARY_CALLDATA_INVERSES", frs_per_G); + manifest.add_entry(round, "RETURN_DATA_INVERSES", frs_per_G); + manifest.add_entry(round, "Z_PERM", frs_per_G); + round++; + + // Rounds 3-23: main sumcheck univariates (21 rounds) for (size_t i = 0; i < NUM_SUMCHECK_UNIVARIATES; ++i) { - manifest.add_entry(4 + i, "Sumcheck:univariate_" + std::to_string(i), 8); - manifest.add_challenge(4 + i, "Sumcheck:u_" + std::to_string(i)); + manifest.add_challenge(round, "Sumcheck:u_" + std::to_string(i)); + manifest.add_entry(round, "Sumcheck:univariate_" + std::to_string(i), 8); + round++; } - // Round 25: evaluations + unshifted batching challenges - manifest.add_entry(25, "Sumcheck:evaluations", 60); + // Round 24: evaluations + all batching challenges (unshifted + shifted in same round) for (size_t i = 0; i < 55; ++i) { - manifest.add_challenge(25, "unshifted_challenge_" + std::to_string(i)); + manifest.add_challenge(round, "unshifted_challenge_" + std::to_string(i)); } - - // Round 26: shifted batching challenges for (size_t i = 0; i < 5; ++i) { - manifest.add_challenge(26, "shifted_challenge_" + std::to_string(i)); + manifest.add_challenge(round, "shifted_challenge_" + std::to_string(i)); } + manifest.add_entry(round, "Sumcheck:evaluations", 60); + round++; - // Round 27: MLB accumulator data - manifest.add_entry(27, "non_shifted_accumulator_commitment", frs_per_G); - manifest.add_entry(27, "shifted_accumulator_commitment", frs_per_G); + // Round 25: Sumcheck:alpha + MLB accumulator data (Sumcheck:alpha is consecutive challenge) + manifest.add_challenge(round, "Sumcheck:alpha"); + manifest.add_entry(round, "non_shifted_accumulator_commitment", frs_per_G); + manifest.add_entry(round, "shifted_accumulator_commitment", frs_per_G); for (size_t i = 0; i < NUM_SUMCHECK_UNIVARIATES; ++i) { - manifest.add_entry(27, "accumulator_challenge_" + std::to_string(i), 1); + manifest.add_entry(round, "accumulator_challenge_" + std::to_string(i), 1); } - manifest.add_entry(27, "accumulator_evaluation_0", 1); - manifest.add_entry(27, "accumulator_evaluation_1", 1); - manifest.add_challenge(27, "Sumcheck:alpha"); + manifest.add_entry(round, "accumulator_evaluation_0", 1); + manifest.add_entry(round, "accumulator_evaluation_1", 1); + round++; - // Rounds 28-48: MLB sumcheck univariates + // Rounds 26-46: MLB sumcheck univariates (21 rounds) for (size_t i = 0; i < NUM_SUMCHECK_UNIVARIATES; ++i) { - manifest.add_entry(28 + i, "Sumcheck:univariate_" + std::to_string(i), 4); - manifest.add_challenge(28 + i, "Sumcheck:u_" + std::to_string(i)); + manifest.add_challenge(round, "Sumcheck:u_" + std::to_string(i)); + manifest.add_entry(round, "Sumcheck:univariate_" + std::to_string(i), 4); + round++; } - // Round 49: final evaluations + claim_batching_challenge - manifest.add_entry(49, "Sumcheck:evaluations", 6); - manifest.add_challenge(49, "claim_batching_challenge"); + // Round 47: final evaluations + claim_batching_challenge + manifest.add_challenge(round, "claim_batching_challenge"); + manifest.add_entry(round, "Sumcheck:evaluations", 6); return manifest; } diff --git a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.circuit.failure.test.cpp b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.circuit.failure.test.cpp index 83e0e666d2f8..a8cd2e62f698 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.circuit.failure.test.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.circuit.failure.test.cpp @@ -19,20 +19,6 @@ class Poseidon2FailureTests : public ::testing::Test { using SubrelationSeparator = Flavor::SubrelationSeparator; using RelationParameters = RelationParameters; - void modify_selector(auto& selector) - { - size_t start_idx = selector.start_index(); - size_t end_idx = selector.end_index(); - - // Flip the first non-zero selector value. - for (size_t idx = start_idx; idx < end_idx; idx++) { - if (selector.at(idx) == 1) { - selector.at(idx) = 0; - break; - } - } - } - void modify_witness(const auto& selector, auto& witness) { size_t start_idx = selector.start_index(); @@ -108,7 +94,7 @@ class Poseidon2FailureTests : public ::testing::Test { virtual_log_n); auto proof = sumcheck_prover.prove(); - auto verifier_transcript = std::make_shared(prover_transcript->export_proof()); + auto verifier_transcript = Transcript::verifier_init_empty(prover_transcript); SubrelationSeparator verifier_subrelation_separator = verifier_transcript->template get_challenge("Sumcheck:alpha"); @@ -140,34 +126,6 @@ TEST_F(Poseidon2FailureTests, ValidCircuitVerifies) prove_and_verify(prover_instance, true); } -TEST_F(Poseidon2FailureTests, WrongSelectorValues) -{ - Builder builder; - - // Construct a circuit that hashes a single witness field element. - hash_single_input(builder); - - // Convert circuit to polynomials. - auto prover_instance = std::make_shared>(builder); - { - // Disable Poseidon2 External selector in the first active row. - // This UNDERCONSTRAINS the circuit, so verification still passes because all remaining constraints are - // satisfied on the valid witness data. - modify_selector(prover_instance->polynomials.q_poseidon2_external); - - // Run sumcheck - it should PASS because we only removed a constraint - prove_and_verify(prover_instance, true); - } - { - // Disable Poseidon2 Internal selector in the first active row. - // Again, this underconstrains the circuit, so verification passes. - modify_selector(prover_instance->polynomials.q_poseidon2_internal); - - // Run sumcheck - it should PASS because we only removed a constraint - prove_and_verify(prover_instance, true); - } -} - TEST_F(Poseidon2FailureTests, WrongWitnessValues) { Builder builder; diff --git a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.test.cpp b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.test.cpp index cfa0ab095900..f5c1af900891 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.test.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.test.cpp @@ -423,8 +423,12 @@ TYPED_TEST(StdlibPoseidon2, PointCoordinatesVsAliasProduceDifferentHashes) }; // Lambda to deserialize point from limbs, serialize back, and hash (stdlib) - auto compute_stdlib_hash = - [&](const uint256_t& xl, const uint256_t& xh, const uint256_t& yl, const uint256_t& yh) -> bb::fr { + // For Ultra, alias values will fail assert_is_in_field; for Mega, validation is deferred to Translator/ECCVM + auto compute_stdlib_hash = [&](const uint256_t& xl, + const uint256_t& xh, + const uint256_t& yl, + const uint256_t& yh, + bool is_alias) -> bb::fr { Builder builder; std::vector limbs = { field_ct(witness_ct(&builder, bb::fr(xl))), field_ct(witness_ct(&builder, bb::fr(xh))), @@ -438,7 +442,14 @@ TYPED_TEST(StdlibPoseidon2, PointCoordinatesVsAliasProduceDifferentHashes) std::vector serialized = Codec::template serialize_to_fields(pt); auto hash = poseidon2::hash(serialized); - EXPECT_TRUE(CircuitChecker::check(builder)); + // Ultra arithmetization uses bigfield with strict assert_is_in_field, so alias values fail. + // Mega arithmetization uses goblin_field which defers validation to Translator/ECCVM. + if constexpr (IsMegaBuilder) { + EXPECT_TRUE(CircuitChecker::check(builder)); + } else { + // For Ultra: canonical values should pass, alias values should fail + EXPECT_EQ(CircuitChecker::check(builder), !is_alias); + } return hash.get_value(); }; @@ -447,8 +458,8 @@ TYPED_TEST(StdlibPoseidon2, PointCoordinatesVsAliasProduceDifferentHashes) bb::fr alias_native_hash = compute_native_hash(alias_x_lo, alias_x_hi, y_lo, y_hi); // Compute stdlib hashes - bb::fr canonical_stdlib_hash = compute_stdlib_hash(x_lo, x_hi, y_lo, y_hi); - bb::fr alias_stdlib_hash = compute_stdlib_hash(alias_x_lo, alias_x_hi, y_lo, y_hi); + bb::fr canonical_stdlib_hash = compute_stdlib_hash(x_lo, x_hi, y_lo, y_hi, /*is_alias=*/false); + bb::fr alias_stdlib_hash = compute_stdlib_hash(alias_x_lo, alias_x_hi, y_lo, y_hi, /*is_alias=*/true); // Verify stdlib matches native for both canonical and alias EXPECT_EQ(canonical_stdlib_hash, canonical_native_hash); diff --git a/barretenberg/cpp/src/barretenberg/translator_vm/translator.test.cpp b/barretenberg/cpp/src/barretenberg/translator_vm/translator.test.cpp index 8f4454c32ba3..e92d9bc87ad1 100644 --- a/barretenberg/cpp/src/barretenberg/translator_vm/translator.test.cpp +++ b/barretenberg/cpp/src/barretenberg/translator_vm/translator.test.cpp @@ -29,18 +29,17 @@ class TranslatorTests : public ::testing::Test { /** * @brief Build the expected transcript manifest for Translator verification - * @details The manifest has 43 rounds total: + * @details The manifest has 26 rounds total: * - Round 0: vk_hash, Gemini masking, 82 wire commitments -> beta challenge * - Round 1: (empty) -> gamma challenge - * - Round 2: Z_PERM -> Sumcheck:alpha challenge - * - Rounds 3-19: Gate challenges (17 rounds) - * - Round 20: Libra:concatenation_commitment + Sum -> Libra:Challenge - * - Rounds 21-37: Sumcheck univariates (17 rounds) - * - Round 38: Sumcheck evaluations + Libra commitments -> rho - * - Round 39: Gemini fold commitments -> Gemini:r - * - Round 40: Gemini evaluations + Libra evals -> Shplonk:nu - * - Round 41: Shplonk:Q -> Shplonk:z - * - Round 42: KZG:W -> KZG:masking_challenge + * - Round 2: Z_PERM -> Sumcheck:alpha + all gate challenges + * - Round 3: Libra:concatenation_commitment + Sum -> Libra:Challenge + * - Rounds 4-20: Sumcheck univariates (17 rounds) + * - Round 21: Sumcheck evaluations + Libra commitments -> rho + * - Round 22: Gemini fold commitments -> Gemini:r + * - Round 23: Gemini evaluations + Libra evals -> Shplonk:nu + * - Round 24: Shplonk:Q -> Shplonk:z + * - Round 25: KZG:W -> KZG:masking_challenge */ static TranscriptManifest build_expected_translator_manifest() { @@ -101,63 +100,60 @@ class TranslatorTests : public ::testing::Test { for (const auto& label : wire_labels) { manifest.add_entry(0, label, frs_per_G); } + // beta and gamma are consecutive challenges (no data between), so both in round 0 manifest.add_challenge(0, "beta"); + manifest.add_challenge(0, "gamma"); - // Round 1: gamma challenge (no entries) - manifest.add_challenge(1, "gamma"); - - // Round 2: Z_PERM -> Sumcheck:alpha - manifest.add_entry(2, "Z_PERM", frs_per_G); - manifest.add_challenge(2, "Sumcheck:alpha"); - - // Rounds 3-19: Gate challenges (17 rounds) + // Round 1: Z_PERM -> Sumcheck:alpha + all gate challenges (same round, no data between them) + manifest.add_entry(1, "Z_PERM", frs_per_G); + manifest.add_challenge(1, "Sumcheck:alpha"); for (size_t i = 0; i < NUM_SUMCHECK_ROUNDS; ++i) { - manifest.add_challenge(3 + i, "Sumcheck:gate_challenge_" + std::to_string(i)); + manifest.add_challenge(1, "Sumcheck:gate_challenge_" + std::to_string(i)); } - // Round 20: Libra concatenation commitment + Sum -> Libra:Challenge - manifest.add_entry(20, "Libra:concatenation_commitment", frs_per_G); - manifest.add_entry(20, "Libra:Sum", 1); - manifest.add_challenge(20, "Libra:Challenge"); + // Round 2: Libra concatenation commitment + Sum -> Libra:Challenge + manifest.add_entry(2, "Libra:concatenation_commitment", frs_per_G); + manifest.add_entry(2, "Libra:Sum", 1); + manifest.add_challenge(2, "Libra:Challenge"); - // Rounds 21-37: Sumcheck univariates (17 rounds) + // Rounds 3-19: Sumcheck univariates (17 rounds) for (size_t i = 0; i < NUM_SUMCHECK_ROUNDS; ++i) { - manifest.add_entry(21 + i, "Sumcheck:univariate_" + std::to_string(i), 9); - manifest.add_challenge(21 + i, "Sumcheck:u_" + std::to_string(i)); + manifest.add_entry(3 + i, "Sumcheck:univariate_" + std::to_string(i), 9); + manifest.add_challenge(3 + i, "Sumcheck:u_" + std::to_string(i)); } - // Round 38: Sumcheck evaluations + Libra commitments -> rho - manifest.add_entry(38, "Sumcheck:evaluations", 188); - manifest.add_entry(38, "Libra:claimed_evaluation", 1); - manifest.add_entry(38, "Libra:grand_sum_commitment", frs_per_G); - manifest.add_entry(38, "Libra:quotient_commitment", frs_per_G); - manifest.add_challenge(38, "rho"); + // Round 20: Sumcheck evaluations + Libra commitments -> rho + manifest.add_entry(20, "Sumcheck:evaluations", 188); + manifest.add_entry(20, "Libra:claimed_evaluation", 1); + manifest.add_entry(20, "Libra:grand_sum_commitment", frs_per_G); + manifest.add_entry(20, "Libra:quotient_commitment", frs_per_G); + manifest.add_challenge(20, "rho"); - // Round 39: Gemini fold commitments -> Gemini:r + // Round 21: Gemini fold commitments -> Gemini:r for (size_t i = 1; i <= 16; ++i) { - manifest.add_entry(39, "Gemini:FOLD_" + std::to_string(i), frs_per_G); + manifest.add_entry(21, "Gemini:FOLD_" + std::to_string(i), frs_per_G); } - manifest.add_challenge(39, "Gemini:r"); + manifest.add_challenge(21, "Gemini:r"); - // Round 40: Gemini evaluations + Libra evals -> Shplonk:nu + // Round 22: Gemini evaluations + Libra evals -> Shplonk:nu for (size_t i = 1; i <= 17; ++i) { - manifest.add_entry(40, "Gemini:a_" + std::to_string(i), 1); + manifest.add_entry(22, "Gemini:a_" + std::to_string(i), 1); } - manifest.add_entry(40, "Gemini:P_pos", 1); - manifest.add_entry(40, "Gemini:P_neg", 1); - manifest.add_entry(40, "Libra:concatenation_eval", 1); - manifest.add_entry(40, "Libra:shifted_grand_sum_eval", 1); - manifest.add_entry(40, "Libra:grand_sum_eval", 1); - manifest.add_entry(40, "Libra:quotient_eval", 1); - manifest.add_challenge(40, "Shplonk:nu"); - - // Round 41: Shplonk:Q -> Shplonk:z - manifest.add_entry(41, "Shplonk:Q", frs_per_G); - manifest.add_challenge(41, "Shplonk:z"); - - // Round 42: KZG:W -> KZG:masking_challenge - manifest.add_entry(42, "KZG:W", frs_per_G); - manifest.add_challenge(42, "KZG:masking_challenge"); + manifest.add_entry(22, "Gemini:P_pos", 1); + manifest.add_entry(22, "Gemini:P_neg", 1); + manifest.add_entry(22, "Libra:concatenation_eval", 1); + manifest.add_entry(22, "Libra:shifted_grand_sum_eval", 1); + manifest.add_entry(22, "Libra:grand_sum_eval", 1); + manifest.add_entry(22, "Libra:quotient_eval", 1); + manifest.add_challenge(22, "Shplonk:nu"); + + // Round 23: Shplonk:Q -> Shplonk:z + manifest.add_entry(23, "Shplonk:Q", frs_per_G); + manifest.add_challenge(23, "Shplonk:z"); + + // Round 24: KZG:W -> KZG:masking_challenge + manifest.add_entry(24, "KZG:W", frs_per_G); + manifest.add_challenge(24, "KZG:masking_challenge"); return manifest; } diff --git a/barretenberg/cpp/src/barretenberg/ultra_honk/mega_transcript.test.cpp b/barretenberg/cpp/src/barretenberg/ultra_honk/mega_transcript.test.cpp index 7dde0281d2a9..0d5b24c1ad3d 100644 --- a/barretenberg/cpp/src/barretenberg/ultra_honk/mega_transcript.test.cpp +++ b/barretenberg/cpp/src/barretenberg/ultra_honk/mega_transcript.test.cpp @@ -88,8 +88,6 @@ template class MegaTranscriptTests : public ::testing::Test { manifest_expected.add_entry(round, "Z_PERM", frs_per_G); manifest_expected.add_challenge(round, "alpha"); - round++; - manifest_expected.add_challenge(round, "Sumcheck:gate_challenge"); round++; diff --git a/barretenberg/cpp/src/barretenberg/ultra_honk/ultra_transcript.test.cpp b/barretenberg/cpp/src/barretenberg/ultra_honk/ultra_transcript.test.cpp index 191447c585ef..ba825311a224 100644 --- a/barretenberg/cpp/src/barretenberg/ultra_honk/ultra_transcript.test.cpp +++ b/barretenberg/cpp/src/barretenberg/ultra_honk/ultra_transcript.test.cpp @@ -109,8 +109,6 @@ template class UltraTranscriptTests : public ::testing::Test { manifest_expected.add_entry(round, "Z_PERM", data_types_per_G); manifest_expected.add_challenge(round, "alpha"); - round++; - manifest_expected.add_challenge(round, "Sumcheck:gate_challenge"); round++; From 11f4391798ce40fe420fc395d386351f3b6dd2d9 Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Thu, 8 Jan 2026 16:31:00 +0000 Subject: [PATCH 13/24] move readme --- .../{ecc/fields => stdlib/primitives/field}/CODEC_README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename barretenberg/cpp/src/barretenberg/{ecc/fields => stdlib/primitives/field}/CODEC_README.md (100%) diff --git a/barretenberg/cpp/src/barretenberg/ecc/fields/CODEC_README.md b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/CODEC_README.md similarity index 100% rename from barretenberg/cpp/src/barretenberg/ecc/fields/CODEC_README.md rename to barretenberg/cpp/src/barretenberg/stdlib/primitives/field/CODEC_README.md From 7be41f1f835cf7c1284465ed23fbbe40353c97d0 Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Thu, 15 Jan 2026 11:50:33 +0000 Subject: [PATCH 14/24] update codec readme, make the comment about grumpkin inf check more precise --- ...slator_delta_range_constraint_relation.hpp | 3 + .../stdlib/primitives/field/CODEC_README.md | 135 ++++++------------ .../primitives/field/field_conversion.hpp | 14 +- 3 files changed, 54 insertions(+), 98 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/relations/translator_vm/translator_delta_range_constraint_relation.hpp b/barretenberg/cpp/src/barretenberg/relations/translator_vm/translator_delta_range_constraint_relation.hpp index a9e9641e11ed..84fb4b555a94 100644 --- a/barretenberg/cpp/src/barretenberg/relations/translator_vm/translator_delta_range_constraint_relation.hpp +++ b/barretenberg/cpp/src/barretenberg/relations/translator_vm/translator_delta_range_constraint_relation.hpp @@ -37,6 +37,9 @@ template class TranslatorDeltaRangeConstraintRelationImpl { * @details The relation enforces 2 constraints on each of the ordered_range_constraints wires: * 1) 2 sequential values are non-descending and have a difference of at most 3, except for the value at last index * 2) The value at last index is 2¹⁴ - 1. + * TODO(https://github.com/AztecProtocol/barretenberg/issues/1607): This only enforces <254-bit range constraints, + * NOT strict get_eccvm_ops(), ...)` - - `get_eccvm_ops()` returns `ECCVMOperation` structs with `base_point` (native `Point`) - - `bb::fq` is **always canonical** - the field class auto-reduces on construction - - Even a modified binary cannot store non-canonical values in `bb::fq` - -2. **Translator uses limb data from op_queue**: - - `ultra_ops_table` contains `(x_lo, x_hi, y_lo, y_hi)` limbs - - These limbs come from `construct_and_populate_ultra_ops()` which decomposes from native `Point` - - A modified prover could potentially alter this decomposition - -3. **Translation check catches inconsistency**: - - ECCVM's `accumulated_result` is computed from canonical `transcript_Px`, `transcript_Py` evaluations - - Translator computes its accumulator from the limbs in `ultra_ops_table` - - `TranslatorAccumulatorTransferRelation` enforces equality at the result row - - If limbs differ (canonical vs aliased), the accumulators differ → **verification fails** - -### Key Invariants +these constraints to fail. However, **these constraints can be avoided**. -1. **`bb::fq` is inherently canonical**: Montgomery representation requires `value < modulus` -2. **ECCVM transcript comes from native Points** -3. **Translation check** -4. **Merge protocol final table commitment is the input to the Translator Verifier** ### Relevant Code Locations diff --git a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.hpp b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.hpp index 4638352442da..3c1f2377efb5 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.hpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.hpp @@ -40,8 +40,9 @@ template class StdlibCodec { * sum is a non-negative integer not exceeding 2^138, i.e. it does not overflow the fq modulus, hence all limbs must * be 0. * - * Grumpkin: We are using the observation that (x^2 + 5 * y^2 = 0) has no non-trivial solutions in fr, since fr - * modulus p == 2 mod 5, i.e. 5 is not a square mod p. + * Grumpkin: We are using the observation that (x^2 + 5 * y^2 = 0) has no non-trivial solutions in fr. + * Rearranging: x^2 = -5y^2, which requires -5 to be a quadratic residue for non-zero solutions. + * Since Fr modulus p ≡ 2 mod 5, we have 5 is not a square mod p, and therefore -5 is also not a square mod p. */ template static bool_t check_point_at_infinity(std::span fr_vec) { @@ -49,6 +50,9 @@ template class StdlibCodec { // Sum the limbs and check whether the sum is 0 return (fr::accumulate(std::vector(fr_vec.begin(), fr_vec.end())).is_zero()); } else { + // For Grumpkin infinity check: verify that Fr modulus p ≡ 2 mod 5 + static_assert(bb::fr::modulus % 5 == 2, "Grumpkin infinity check requires Fr modulus p ≡ 2 mod 5"); + // Efficiently compute ((x^2 + 5 y^2) == 0) const fr x_sqr = fr_vec[0].sqr(); const fr y = fr_vec[1]; @@ -126,8 +130,10 @@ template class StdlibCodec { * * - \ref bb::stdlib::goblin_field< Builder > "goblin field element" — stores limbs as-is without in-circuit * modulus check. Range constraints are deferred to Translator circuit (see \ref - * TranslatorDeltaRangeConstraintRelationImpl "Translator Range Constraint" relation). Alias rejection is ensured - * by the ECCVM ↔ Translator translation check. Specific to \ref MegaCircuitBuilder_ "MegaCircuitBuilder". + * TranslatorDeltaRangeConstraintRelationImpl "Translator Range Constraint" relation). + * TODO(https://github.com/AztecProtocol/barretenberg/issues/1607): Translator only enforces <254-bit constraints, + * NOT strict "bn254 goblin point" — input * vector of size 4 is transformed into a pair of `goblin_field` elements, which are fed into the relevant From 8926462813b1b6ba46cc3c1f7aa14d512d168c1a Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Thu, 15 Jan 2026 13:56:53 +0000 Subject: [PATCH 15/24] upd infinity check and docs --- .../stdlib/primitives/field/CODEC_README.md | 2 +- .../stdlib/primitives/field/field_conversion.hpp | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/CODEC_README.md b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/CODEC_README.md index 1408453fa686..302318f6b2d4 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/CODEC_README.md +++ b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/CODEC_README.md @@ -52,7 +52,7 @@ The point at infinity is represented as `(0, 0)` with **all limbs zero**. |---------|-------|--------|----------------| | **Native** | All | `for each limb: if (limb != 0) return false` | Direct limb-by-limb zero check. | | **Circuit** | BN254 | `sum(limbs) == 0` | Sum of 4 valid limbs (2×136-bit + 2×118-bit) ≤ 2^138, cannot wrap to 0 mod Fr (254 bits). Only all-zero limbs satisfy this. | -| **Circuit** | Grumpkin | `x² + 5y² == 0` | Equation `x² = -5y²` requires -5 to be a quadratic residue. Since -5 is not a square mod p, only `(0,0)` satisfies this. | +| **Circuit** | Grumpkin | `x² - 5y² == 0` | Equation `x² = 5y²` requires 5 to be a quadratic residue in `bb::fr`, which is not the case since `fr`'s modulus is == 2 mod 5. **Note for BN254 Goblin/Mega**: The infinity check operates on raw limbs before range constraints. However, the full protocol ensures soundness: diff --git a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.hpp b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.hpp index 3c1f2377efb5..9f0e4ee7f288 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.hpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_conversion.hpp @@ -40,9 +40,9 @@ template class StdlibCodec { * sum is a non-negative integer not exceeding 2^138, i.e. it does not overflow the fq modulus, hence all limbs must * be 0. * - * Grumpkin: We are using the observation that (x^2 + 5 * y^2 = 0) has no non-trivial solutions in fr. - * Rearranging: x^2 = -5y^2, which requires -5 to be a quadratic residue for non-zero solutions. - * Since Fr modulus p ≡ 2 mod 5, we have 5 is not a square mod p, and therefore -5 is also not a square mod p. + * Grumpkin: We are using the observation that (x^2 - 5 * y^2 = 0) has no non-trivial solutions in fr. + * Rearranging: x^2 = 5y^2, which requires 5 to be a quadratic residue for non-zero solutions. + * Since Fr modulus p ≡ 2 mod 5, we have 5 is not a square mod p. */ template static bool_t check_point_at_infinity(std::span fr_vec) { @@ -50,14 +50,15 @@ template class StdlibCodec { // Sum the limbs and check whether the sum is 0 return (fr::accumulate(std::vector(fr_vec.begin(), fr_vec.end())).is_zero()); } else { - // For Grumpkin infinity check: verify that Fr modulus p ≡ 2 mod 5 - static_assert(bb::fr::modulus % 5 == 2, "Grumpkin infinity check requires Fr modulus p ≡ 2 mod 5"); + // For Grumpkin infinity check: verify that Fr modulus p ≡ 2 or 3 mod 5 + static_assert(bb::fr::modulus % 5 == 2 || bb::fr::modulus % 5 == 3, + "Grumpkin infinity check requires Fr modulus p ≡ 2 mod 5"); - // Efficiently compute ((x^2 + 5 y^2) == 0) + // Efficiently compute ((x^2 - 5 y^2) == 0) const fr x_sqr = fr_vec[0].sqr(); const fr y = fr_vec[1]; const fr five_y = y * bb::fr(5); - return (y.madd(five_y, x_sqr).is_zero()); + return (y.madd(-five_y, x_sqr).is_zero()); } } From 14dc90b2e4632139fb0ee818ac405ca1fb5b434e Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Fri, 16 Jan 2026 11:09:40 +0000 Subject: [PATCH 16/24] fix tests --- .../dsl/acir_format/gate_count_constants.hpp | 18 +- .../eccvm/eccvm_transcript.test.cpp | 1 + .../hypernova/hypernova_verifier.test.cpp | 20 +- .../relations/poseidon2_internal_relation.hpp | 102 +++++----- .../poseidon2.circuit.failure.test.cpp | 4 +- .../stdlib/hash/poseidon2/poseidon2.test.cpp | 182 +++++++++--------- .../sumcheck/partial_evaluation.test.cpp | 10 +- .../barretenberg/ultra_honk/sumcheck.test.cpp | 4 +- 8 files changed, 171 insertions(+), 170 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/dsl/acir_format/gate_count_constants.hpp b/barretenberg/cpp/src/barretenberg/dsl/acir_format/gate_count_constants.hpp index b145897b9261..04fcae5eacd1 100644 --- a/barretenberg/cpp/src/barretenberg/dsl/acir_format/gate_count_constants.hpp +++ b/barretenberg/cpp/src/barretenberg/dsl/acir_format/gate_count_constants.hpp @@ -67,26 +67,26 @@ constexpr std::tuple HONK_RECURSION_CONSTANTS( if constexpr (std::is_same_v>) { switch (mode) { case PredicateTestCase::ConstantTrue: - return std::make_tuple(726840, 0); + return std::make_tuple(726841, 0); case PredicateTestCase::WitnessTrue: case PredicateTestCase::WitnessFalse: - return std::make_tuple(727991, 0); + return std::make_tuple(727992, 0); } } else if constexpr (std::is_same_v>) { switch (mode) { case PredicateTestCase::ConstantTrue: - return std::make_tuple(770506, 0); + return std::make_tuple(770505, 0); case PredicateTestCase::WitnessTrue: case PredicateTestCase::WitnessFalse: - return std::make_tuple(771759, 0); + return std::make_tuple(771758, 0); } } else if constexpr (std::is_same_v>) { switch (mode) { case PredicateTestCase::ConstantTrue: - return std::make_tuple(727160, 0); + return std::make_tuple(727159, 0); case PredicateTestCase::WitnessTrue: case PredicateTestCase::WitnessFalse: - return std::make_tuple(728459, 0); + return std::make_tuple(728458, 0); } } else if constexpr (std::is_same_v>) { switch (mode) { @@ -108,7 +108,7 @@ constexpr std::tuple HONK_RECURSION_CONSTANTS( if (mode != PredicateTestCase::ConstantTrue) { bb::assert_failure("Unhandled mode in MegaZKRecursiveFlavor."); } - return std::make_tuple(817287, 0); + return std::make_tuple(817286, 0); } else { bb::assert_failure("Unhandled recursive flavor."); } @@ -119,7 +119,7 @@ constexpr std::tuple HONK_RECURSION_CONSTANTS( // ======================================== // Gate count for Chonk recursive verification (UltraRollup builder) -inline constexpr size_t CHONK_RECURSION_GATES = 2385506; +inline constexpr size_t CHONK_RECURSION_GATES = 2385459; // ======================================== // Hypernova Recursion Constants @@ -153,7 +153,7 @@ inline constexpr size_t HIDING_KERNEL_ULTRA_OPS = 124; // ======================================== // Gate count for ECCVM recursive verifier (Ultra-arithmetized) -inline constexpr size_t ECCVM_RECURSIVE_VERIFIER_GATE_COUNT = 220455; +inline constexpr size_t ECCVM_RECURSIVE_VERIFIER_GATE_COUNT = 220411; // ======================================== // Goblin AVM Recursive Verifier Constants diff --git a/barretenberg/cpp/src/barretenberg/eccvm/eccvm_transcript.test.cpp b/barretenberg/cpp/src/barretenberg/eccvm/eccvm_transcript.test.cpp index 614b499122cf..9db51a2bf07e 100644 --- a/barretenberg/cpp/src/barretenberg/eccvm/eccvm_transcript.test.cpp +++ b/barretenberg/cpp/src/barretenberg/eccvm/eccvm_transcript.test.cpp @@ -135,6 +135,7 @@ class ECCVMTranscriptTests : public ::testing::Test { manifest_expected.add_entry(round, "LOOKUP_INVERSES", frs_per_G); manifest_expected.add_entry(round, "Z_PERM", frs_per_G); manifest_expected.add_challenge(round, "Sumcheck:alpha"); + for (size_t i = 0; i < CONST_ECCVM_LOG_N; i++) { std::string label = "Sumcheck:gate_challenge_" + std::to_string(i); manifest_expected.add_challenge(round, label); diff --git a/barretenberg/cpp/src/barretenberg/hypernova/hypernova_verifier.test.cpp b/barretenberg/cpp/src/barretenberg/hypernova/hypernova_verifier.test.cpp index e2dfa3cb4e91..fc37a8fd9f56 100644 --- a/barretenberg/cpp/src/barretenberg/hypernova/hypernova_verifier.test.cpp +++ b/barretenberg/cpp/src/barretenberg/hypernova/hypernova_verifier.test.cpp @@ -155,12 +155,13 @@ class HypernovaFoldingVerifierTests : public ::testing::Test { /** * @brief Build the expected transcript manifest for HyperNova folding - * @details The manifest has 50 rounds total: + * @details The manifest has 48 rounds total: * - Oink (rounds 0-2): vk_hash, public inputs, wires, ECC ops, databus, lookup, inverses - * - Main sumcheck (rounds 3-25): gate_challenge, univariates, evaluations + batching challenges - * - Batching challenges (round 26): shifted challenges - * - MLB data (round 27): accumulator commitments/challenges/evaluations - * - MLB sumcheck (rounds 28-49): univariates, final evaluations + claim_batching_challenge + * - Main sumcheck (rounds 3-23): gate_challenge, univariates + * - Main sumcheck batching (round 24): unshifted/shifted batching challenges + evaluations + * - MLB data (round 25): accumulator commitments/challenges/evaluations + * - MLB sumcheck (rounds 26-46): univariates + * - MLB final (round 47): final evaluations + claim_batching_challenge */ static TranscriptManifest build_expected_folding_manifest() { @@ -213,15 +214,12 @@ class HypernovaFoldingVerifierTests : public ::testing::Test { round++; } - // Round 25: evaluations + unshifted batching challenges - manifest.add_entry(25, "Sumcheck:evaluations", 60); + // Round 24: unshifted batching challenges + shifted batching challenges + evaluations for (size_t i = 0; i < MegaFlavor::NUM_UNSHIFTED_ENTITIES - 1; ++i) { - manifest.add_challenge(25, "unshifted_challenge_" + std::to_string(i)); + manifest.add_challenge(round, "unshifted_challenge_" + std::to_string(i)); } - - // Round 26: shifted batching challenges for (size_t i = 0; i < MegaFlavor::NUM_SHIFTED_ENTITIES - 1; ++i) { - manifest.add_challenge(26, "shifted_challenge_" + std::to_string(i)); + manifest.add_challenge(round, "shifted_challenge_" + std::to_string(i)); } manifest.add_entry(round, "Sumcheck:evaluations", 60); round++; diff --git a/barretenberg/cpp/src/barretenberg/relations/poseidon2_internal_relation.hpp b/barretenberg/cpp/src/barretenberg/relations/poseidon2_internal_relation.hpp index 032d963c1e94..9a9daef439ef 100644 --- a/barretenberg/cpp/src/barretenberg/relations/poseidon2_internal_relation.hpp +++ b/barretenberg/cpp/src/barretenberg/relations/poseidon2_internal_relation.hpp @@ -10,6 +10,58 @@ namespace bb { +/** + * @brief Expression for the Poseidon2 internal round relation, based on I_i in Section 6 of + * https://eprint.iacr.org/2023/323.pdf. + * + * @details Let the internal round matrix M_I be the 4×4 matrix + * \f[ + * M_I = + * \begin{bmatrix} + * D_1 & 1 & 1 & 1 \\ + * 1 & D_2 & 1 & 1 \\ + * 1 & 1 & D_3 & 1 \\ + * 1 & 1 & 1 & D_4 + * \end{bmatrix}, + * \quad + * \text{where } D_i \text{ are the diagonal entries of } M_I. + * \f] + * + * Define the state + * \f[ + * u_1 = \big(w_1 + \hat{c}_0^{(i)}\big)^{5},\qquad + * u_2 = w_2,\quad + * u_3 = w_3,\quad + * u_4 = w_4,\qquad + * \mathbf{u} = (u_1,u_2,u_3,u_4). + * \f] + * The internal round computes \f$ \mathbf{v} = M_I \cdot \mathbf{u}^{\top} \f$ and the relation enforces + * \f$ v_k = w_{k,\mathrm{shift}} \f$ for \f$ k \in \{1,2,3,4\} \f$: + * \f{align*} + * v_1 &= D_1\,u_1 + u_2 + u_3 + u_4,\\ + * v_2 &= u_1 + D_2\,u_2 + u_3 + u_4,\\ + * v_3 &= u_1 + u_2 + D_3\,u_3 + u_4,\\ + * v_4 &= u_1 + u_2 + u_3 + D_4\,u_4, + * \f} + * where \f$ \hat{c}_0^{(i)} \f$ is the internal round constant (provided via the \f$ q_l \f$ selector). + * + * Concretely, the relation is encoded as four independent constraints multiplied by the + * \f$\text{q_poseidon2_external}\f$ selector and the scaling factor \f$\hat{g}\f$ arising from the + * `GateSeparatorPolynomial`. These contributions are added to the corresponding univariate accumulators + * \f$ A_k \f$ (one per subrelation): + * \f{align*} + * A_1 &\;\mathrel{+}= q_{\mathrm{poseidon2\_internal}}\cdot\big(v_1 - w_{1,\mathrm{shift}}\big)\cdot \hat{g},\\ + * A_2 &\;\mathrel{+}= q_{\mathrm{poseidon2\_internal}}\cdot\big(v_2 - w_{2,\mathrm{shift}}\big)\cdot \hat{g},\\ + * A_3 &\;\mathrel{+}= q_{\mathrm{poseidon2\_internal}}\cdot\big(v_3 - w_{3,\mathrm{shift}}\big)\cdot \hat{g},\\ + * A_4 &\;\mathrel{+}= q_{\mathrm{poseidon2\_internal}}\cdot\big(v_4 - w_{4,\mathrm{shift}}\big)\cdot \hat{g}. + * \f} + * At the end of each Sumcheck round, the subrelation accumulators are aggregated with independent challenges + * \f$ \alpha_i = \alpha_{i,\mathrm{Poseidon2Int}} \f$ (from the `SubrelationSeparators`) + * \f[ + * \alpha_{0}A_1 + \alpha_{1}A_2 + \alpha_{2}A_3 + \alpha_{3}A_4 + * \f] + * and multiplied by the linear factor of the `GateSeparatorPolynomial`. + */ template class Poseidon2InternalRelationImpl { public: using FF = FF_; @@ -38,56 +90,6 @@ template class Poseidon2InternalRelationImpl { } /** - * @brief Expression for the Poseidon2 internal round relation, based on I_i in Section 6 of - * https://eprint.iacr.org/2023/323.pdf. - * - * @details Let the internal round matrix M_I be the 4×4 matrix - * \f[ - * M_I = - * \begin{bmatrix} - * D_1 & 1 & 1 & 1 \\ - * 1 & D_2 & 1 & 1 \\ - * 1 & 1 & D_3 & 1 \\ - * 1 & 1 & 1 & D_4 - * \end{bmatrix}, - * \quad - * \text{where } D_i \text{ are the diagonal entries of } M_I. - * \f] - * - * Define the state - * \f[ - * u_1 = \big(w_1 + \hat{c}_0^{(i)}\big)^{5},\qquad - * u_2 = w_2,\quad - * u_3 = w_3,\quad - * u_4 = w_4,\qquad - * \mathbf{u} = (u_1,u_2,u_3,u_4). - * \f] - * The internal round computes \f$ \mathbf{v} = M_I \cdot \mathbf{u}^{\top} \f$ and the relation enforces - * \f$ v_k = w_{k,\mathrm{shift}} \f$ for \f$ k \in \{1,2,3,4\} \f$: - * \f{align*} - * v_1 &= D_1\,u_1 + u_2 + u_3 + u_4,\\ - * v_2 &= u_1 + D_2\,u_2 + u_3 + u_4,\\ - * v_3 &= u_1 + u_2 + D_3\,u_3 + u_4,\\ - * v_4 &= u_1 + u_2 + u_3 + D_4\,u_4, - * \f} - * where \f$ \hat{c}_0^{(i)} \f$ is the internal round constant (provided via the \f$ q_l \f$ selector). - * - * Concretely, the relation is encoded as four independent constraints multiplied by the - * \f$\text{q_poseidon2_external}\f$ selector and the scaling factor \f$\hat{g}\f$ arising from the - * `GateSeparatorPolynomial`. These contributions are added to the corresponding univariate accumulators - * \f$ A_k \f$ (one per subrelation): - * \f{align*} - * A_1 &\;\mathrel{+}= q_{\mathrm{poseidon2\_internal}}\cdot\big(v_1 - w_{1,\mathrm{shift}}\big)\cdot \hat{g},\\ - * A_2 &\;\mathrel{+}= q_{\mathrm{poseidon2\_internal}}\cdot\big(v_2 - w_{2,\mathrm{shift}}\big)\cdot \hat{g},\\ - * A_3 &\;\mathrel{+}= q_{\mathrm{poseidon2\_internal}}\cdot\big(v_3 - w_{3,\mathrm{shift}}\big)\cdot \hat{g},\\ - * A_4 &\;\mathrel{+}= q_{\mathrm{poseidon2\_internal}}\cdot\big(v_4 - w_{4,\mathrm{shift}}\big)\cdot \hat{g}. - * \f} - * At the end of each Sumcheck round, the subrelation accumulators are aggregated with independent challenges - * \f$ \alpha_i = \alpha_{i,\mathrm{Poseidon2Int}} \f$ (from the `SubrelationSeparators`) - * \f[ - * \alpha_{0}A_1 + \alpha_{1}A_2 + \alpha_{2}A_3 + \alpha_{3}A_4 - * \f] - * and multiplied by the linear factor of the `GateSeparatorPolynomial`. * @param evals A tuple of tuples of univariate accumulators; the subtuple for this relation is * \f$[A_1,A_2,A_3,A_4]\f$, with \f$ \deg(A_k) = \text{SUBRELATION_PARTIAL_LENGTHS}[k] - 1 \f$. * @param in In round \f$ k \f$ of Sumcheck at the point \f$ i_{>k} = (i_{k+1},\ldots,i_{d-1}) \f$ on the diff --git a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.circuit.failure.test.cpp b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.circuit.failure.test.cpp index a8cd2e62f698..f656a152b7e5 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.circuit.failure.test.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.circuit.failure.test.cpp @@ -71,7 +71,7 @@ class Poseidon2FailureTests : public ::testing::Test { // Complete the prover instance (compute selectors, relation parameters, etc.) WitnessComputation::complete_prover_instance_for_test(prover_instance); - auto prover_transcript = Transcript::prover_init_empty(); + auto prover_transcript = Transcript::test_prover_init_empty(); // Generate challenges via transcript for Fiat-Shamir SubrelationSeparator subrelation_separator = prover_transcript->template get_challenge("Sumcheck:alpha"); @@ -94,7 +94,7 @@ class Poseidon2FailureTests : public ::testing::Test { virtual_log_n); auto proof = sumcheck_prover.prove(); - auto verifier_transcript = Transcript::verifier_init_empty(prover_transcript); + auto verifier_transcript = Transcript::test_verifier_init_empty(prover_transcript); SubrelationSeparator verifier_subrelation_separator = verifier_transcript->template get_challenge("Sumcheck:alpha"); diff --git a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.test.cpp b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.test.cpp index f5c1af900891..a8eda3f3141c 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.test.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.test.cpp @@ -140,6 +140,96 @@ template class StdlibPoseidon2 : public testing::Test { EXPECT_THROW_WITH_MESSAGE(poseidon2::hash(witness_inputs), "Sponge inputs should not be stdlib constants"); } + /** + * @brief Test that bn254 point coordinates with alias values produce different hashes. + * @details When a bn254 point is deserialized with alias x-coordinate (x + modulus), + * the resulting hash must be different from the canonical representation. + * Also verifies consistency between stdlib and native Poseidon2 implementations. + */ + static void test_hash_aliased_points() + { + + using Codec = stdlib::StdlibCodec; + using bn254_point_ct = typename Codec::bn254_commitment; + + constexpr uint64_t NUM_LIMB_BITS = 68; + constexpr uint64_t LOW_BITS = 2 * NUM_LIMB_BITS; + constexpr uint256_t LOW_MASK = (uint256_t(1) << LOW_BITS) - 1; + + // Use generator point coordinates + const uint256_t x_value = uint256_t(bb::g1::affine_one.x); + const uint256_t y_value = uint256_t(bb::g1::affine_one.y); + + const uint256_t fq_modulus = bb::fq::modulus; + + // Compute canonical limbs for x and y + const uint256_t x_lo = x_value & LOW_MASK; + const uint256_t x_hi = x_value >> LOW_BITS; + const uint256_t y_lo = y_value & LOW_MASK; + const uint256_t y_hi = y_value >> LOW_BITS; + + // Compute alias limbs (x + modulus, y unchanged) + const uint256_t alias_x_value = x_value + fq_modulus; + const uint256_t alias_x_lo = alias_x_value & LOW_MASK; + const uint256_t alias_x_hi = alias_x_value >> LOW_BITS; + + BB_ASSERT(alias_x_hi != x_hi); + BB_ASSERT(alias_x_lo != x_lo); + + // Lambda to compute native hash from limbs + auto compute_native_hash = + [&](const uint256_t& xl, const uint256_t& xh, const uint256_t& yl, const uint256_t& yh) -> bb::fr { + std::vector limbs = { bb::fr(xl), bb::fr(xh), bb::fr(yl), bb::fr(yh) }; + return native_poseidon2::hash(limbs); + }; + + // Lambda to deserialize point from limbs, serialize back, and hash (stdlib) + // For Ultra, alias values will fail assert_is_in_field; for Mega, validation is deferred to Translator/ECCVM + auto compute_stdlib_hash = [&](const uint256_t& xl, + const uint256_t& xh, + const uint256_t& yl, + const uint256_t& yh, + bool is_alias) -> bb::fr { + Builder builder; + std::vector limbs = { field_ct(witness_ct(&builder, bb::fr(xl))), + field_ct(witness_ct(&builder, bb::fr(xh))), + field_ct(witness_ct(&builder, bb::fr(yl))), + field_ct(witness_ct(&builder, bb::fr(yh))) }; + + // Deserialize through codec + bn254_point_ct pt = Codec::template deserialize_from_fields(limbs); + + // Serialize back and hash + std::vector serialized = Codec::template serialize_to_fields(pt); + auto hash = poseidon2::hash(serialized); + + // Ultra arithmetization uses bigfield with strict assert_is_in_field, so alias values fail. + // Mega arithmetization uses goblin_field which defers validation to Translator/ECCVM. + if constexpr (IsMegaBuilder) { + EXPECT_TRUE(CircuitChecker::check(builder)); + } else { + // For Ultra: canonical values should pass, alias values should fail + EXPECT_EQ(CircuitChecker::check(builder), !is_alias); + } + return hash.get_value(); + }; + + // Compute native hashes + bb::fr canonical_native_hash = compute_native_hash(x_lo, x_hi, y_lo, y_hi); + bb::fr alias_native_hash = compute_native_hash(alias_x_lo, alias_x_hi, y_lo, y_hi); + + // Compute stdlib hashes + bb::fr canonical_stdlib_hash = compute_stdlib_hash(x_lo, x_hi, y_lo, y_hi, /*is_alias=*/false); + bb::fr alias_stdlib_hash = compute_stdlib_hash(alias_x_lo, alias_x_hi, y_lo, y_hi, /*is_alias=*/true); + + // Verify stdlib matches native for both canonical and alias + EXPECT_EQ(canonical_stdlib_hash, canonical_native_hash); + EXPECT_EQ(alias_stdlib_hash, alias_native_hash); + + // The hashes MUST be different between canonical and alias + EXPECT_NE(canonical_native_hash, alias_native_hash); + EXPECT_NE(canonical_stdlib_hash, alias_stdlib_hash); + } static void test_padding_collisions() { Builder builder; @@ -375,97 +465,7 @@ TYPED_TEST(StdlibPoseidon2, Consistency) TestFixture::test_against_independent_values(); } -/** - * @brief Test that bn254 point coordinates with alias values produce different hashes. - * @details When a bn254 point is deserialized with alias x-coordinate (x + modulus), - * the resulting hash must be different from the canonical representation. - * Also verifies consistency between stdlib and native Poseidon2 implementations. - */ TYPED_TEST(StdlibPoseidon2, PointCoordinatesVsAliasProduceDifferentHashes) { - using Builder = TypeParam; - using field_ct = stdlib::field_t; - using witness_ct = stdlib::witness_t; - using poseidon2 = stdlib::poseidon2; - using native_poseidon2 = crypto::Poseidon2; - using Codec = stdlib::StdlibCodec; - using bn254_point_ct = typename Codec::bn254_commitment; - - constexpr uint64_t NUM_LIMB_BITS = 68; - constexpr uint64_t LOW_BITS = 2 * NUM_LIMB_BITS; - constexpr uint256_t LOW_MASK = (uint256_t(1) << LOW_BITS) - 1; - - // Use generator point coordinates - const uint256_t x_value = uint256_t(bb::g1::affine_one.x); - const uint256_t y_value = uint256_t(bb::g1::affine_one.y); - - const uint256_t fq_modulus = bb::fq::modulus; - - // Compute canonical limbs for x and y - const uint256_t x_lo = x_value & LOW_MASK; - const uint256_t x_hi = x_value >> LOW_BITS; - const uint256_t y_lo = y_value & LOW_MASK; - const uint256_t y_hi = y_value >> LOW_BITS; - - // Compute alias limbs (x + modulus, y unchanged) - const uint256_t alias_x_value = x_value + fq_modulus; - const uint256_t alias_x_lo = alias_x_value & LOW_MASK; - const uint256_t alias_x_hi = alias_x_value >> LOW_BITS; - - BB_ASSERT(alias_x_hi != x_hi); - BB_ASSERT(alias_x_lo != x_lo); - - // Lambda to compute native hash from limbs - auto compute_native_hash = - [&](const uint256_t& xl, const uint256_t& xh, const uint256_t& yl, const uint256_t& yh) -> bb::fr { - std::vector limbs = { bb::fr(xl), bb::fr(xh), bb::fr(yl), bb::fr(yh) }; - return native_poseidon2::hash(limbs); - }; - - // Lambda to deserialize point from limbs, serialize back, and hash (stdlib) - // For Ultra, alias values will fail assert_is_in_field; for Mega, validation is deferred to Translator/ECCVM - auto compute_stdlib_hash = [&](const uint256_t& xl, - const uint256_t& xh, - const uint256_t& yl, - const uint256_t& yh, - bool is_alias) -> bb::fr { - Builder builder; - std::vector limbs = { field_ct(witness_ct(&builder, bb::fr(xl))), - field_ct(witness_ct(&builder, bb::fr(xh))), - field_ct(witness_ct(&builder, bb::fr(yl))), - field_ct(witness_ct(&builder, bb::fr(yh))) }; - - // Deserialize through codec - bn254_point_ct pt = Codec::template deserialize_from_fields(limbs); - - // Serialize back and hash - std::vector serialized = Codec::template serialize_to_fields(pt); - auto hash = poseidon2::hash(serialized); - - // Ultra arithmetization uses bigfield with strict assert_is_in_field, so alias values fail. - // Mega arithmetization uses goblin_field which defers validation to Translator/ECCVM. - if constexpr (IsMegaBuilder) { - EXPECT_TRUE(CircuitChecker::check(builder)); - } else { - // For Ultra: canonical values should pass, alias values should fail - EXPECT_EQ(CircuitChecker::check(builder), !is_alias); - } - return hash.get_value(); - }; - - // Compute native hashes - bb::fr canonical_native_hash = compute_native_hash(x_lo, x_hi, y_lo, y_hi); - bb::fr alias_native_hash = compute_native_hash(alias_x_lo, alias_x_hi, y_lo, y_hi); - - // Compute stdlib hashes - bb::fr canonical_stdlib_hash = compute_stdlib_hash(x_lo, x_hi, y_lo, y_hi, /*is_alias=*/false); - bb::fr alias_stdlib_hash = compute_stdlib_hash(alias_x_lo, alias_x_hi, y_lo, y_hi, /*is_alias=*/true); - - // Verify stdlib matches native for both canonical and alias - EXPECT_EQ(canonical_stdlib_hash, canonical_native_hash); - EXPECT_EQ(alias_stdlib_hash, alias_native_hash); - - // The hashes MUST be different between canonical and alias - EXPECT_NE(canonical_native_hash, alias_native_hash); - EXPECT_NE(canonical_stdlib_hash, alias_stdlib_hash); + TestFixture::test_hash_aliased_points(); } diff --git a/barretenberg/cpp/src/barretenberg/sumcheck/partial_evaluation.test.cpp b/barretenberg/cpp/src/barretenberg/sumcheck/partial_evaluation.test.cpp index 94b9ead878a3..250ad9939033 100644 --- a/barretenberg/cpp/src/barretenberg/sumcheck/partial_evaluation.test.cpp +++ b/barretenberg/cpp/src/barretenberg/sumcheck/partial_evaluation.test.cpp @@ -61,7 +61,7 @@ TYPED_TEST(PartialEvaluationTests, TwoRoundsSpecial) typename Flavor::ProverPolynomials full_polynomials; full_polynomials.q_m = f0; - auto transcript = Transcript::prover_init_empty(); + auto transcript = Transcript::test_prover_init_empty(); FF alpha = FF(1); std::vector gate_challenges{ 1, 1 }; @@ -104,7 +104,7 @@ TYPED_TEST(PartialEvaluationTests, TwoRoundsGeneric) Polynomial f0(4); f0.template copy_vector({ v00, v10, v01, v11 }); - auto transcript = Transcript::prover_init_empty(); + auto transcript = Transcript::test_prover_init_empty(); FF alpha = FF(1); typename Flavor::ProverPolynomials full_polynomials; full_polynomials.q_m = f0; @@ -175,7 +175,7 @@ TYPED_TEST(PartialEvaluationTests, ThreeRoundsSpecial) typename Flavor::ProverPolynomials full_polynomials; full_polynomials.q_m = f0; - auto transcript = Transcript::prover_init_empty(); + auto transcript = Transcript::test_prover_init_empty(); FF alpha = FF(1); std::vector gate_challenges{ 1, 1, 1 }; @@ -236,7 +236,7 @@ TYPED_TEST(PartialEvaluationTests, ThreeRoundsGeneric) typename Flavor::ProverPolynomials full_polynomials; full_polynomials.q_m = f0; - auto transcript = Transcript::prover_init_empty(); + auto transcript = Transcript::test_prover_init_empty(); FF alpha = FF(1); std::vector gate_challenges{ 1, 1, 1 }; @@ -309,7 +309,7 @@ TYPED_TEST(PartialEvaluationTests, ThreeRoundsGenericMultiplePolys) full_polynomials.q_m = f0; full_polynomials.q_c = f1; full_polynomials.q_l = f2; - auto transcript = Transcript::prover_init_empty(); + auto transcript = Transcript::test_prover_init_empty(); FF alpha = FF(1); std::vector gate_challenges{ 1, 1, 1 }; diff --git a/barretenberg/cpp/src/barretenberg/ultra_honk/sumcheck.test.cpp b/barretenberg/cpp/src/barretenberg/ultra_honk/sumcheck.test.cpp index dbe2f554f7ad..64d0d45e2647 100644 --- a/barretenberg/cpp/src/barretenberg/ultra_honk/sumcheck.test.cpp +++ b/barretenberg/cpp/src/barretenberg/ultra_honk/sumcheck.test.cpp @@ -152,7 +152,7 @@ TEST_F(SumcheckTestsRealCircuit, Ultra) WitnessComputation::complete_prover_instance_for_test(prover_inst); - auto prover_transcript = Transcript::prover_init_empty(); + auto prover_transcript = Transcript::test_prover_init_empty(); auto circuit_size = prover_inst->dyadic_size(); auto log_circuit_size = numeric::get_msb(circuit_size); const size_t virtual_log_n = log_circuit_size + 2; // arbitrary but larger than genuine log n @@ -176,7 +176,7 @@ TEST_F(SumcheckTestsRealCircuit, Ultra) auto prover_output = sumcheck_prover.prove(); - auto verifier_transcript = Transcript::verifier_init_empty(prover_transcript); + auto verifier_transcript = Transcript::test_verifier_init_empty(prover_transcript); FF verifier_alpha = verifier_transcript->template get_challenge("Sumcheck:alpha"); SumcheckVerifier sumcheck_verifier(verifier_transcript, verifier_alpha, virtual_log_n); From b12f81f14af0835b9f6ac5689b34c66204b600cb Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Thu, 22 Jan 2026 16:32:15 +0000 Subject: [PATCH 17/24] fix constraint + add test --- .../stdlib/primitives/field/field_utils.cpp | 18 +++---- .../primitives/field/field_utils.test.cpp | 50 +++++++++++++++++++ 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp index af51084aea45..7e7eb1cebf68 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp @@ -24,15 +24,15 @@ void validate_split_in_field_unsafe(const field_t& lo, // Algorithm: Validate lo + hi * 2^lo_bits < field_modulus using borrow logic // - // We compute: hi_diff = r_hi - hi - borrow, lo_diff = r_lo - lo + borrow * 2^lo_bits - // Both must be in range [0, 2^bits) for the check to pass. + // We want: value < modulus, i.e., value <= modulus - 1 + // We compute: hi_diff = r_hi - hi - borrow, lo_diff = (r_lo - 1) - lo + borrow * 2^lo_bits + // Both must be in range [0, 2^{*_bits}) for the check to pass. // - // - If lo < r_lo: no borrow, straightforward comparison of hi parts - // - If lo >= r_lo: set borrow=1, which reduces the allowed hi value by 1 - // This correctly rejects value == modulus because lo_diff becomes 2^lo_bits (out of range) - bool need_borrow = uint256_t(lo.get_value()) >= r_lo; + // - If lo <= r_lo - 1: no borrow needed + // - If lo > r_lo - 1 (i.e., lo >= r_lo): set borrow=1, which reduces the allowed hi value by 1 + bool need_borrow = uint256_t(lo.get_value()) > r_lo - 1; - // If both lo and hi are constant, the validation is entirely deterministic + // If both lo and hi are constant, the validation is straightforward const bool both_constant = lo.is_constant() && hi.is_constant(); Builder* ctx = validate_context(lo.get_context(), hi.get_context()); @@ -48,9 +48,9 @@ void validate_split_in_field_unsafe(const field_t& lo, } // Hi range check = r_hi - hi - borrow - // Lo range check = r_lo - lo + borrow * 2^lo_bits + // Lo range check = (r_lo - 1) - lo + borrow * 2^lo_bits field_t hi_diff = (-hi + r_hi) - borrow; - field_t lo_diff = (-lo + r_lo) + (borrow * (uint256_t(1) << lo_bits)); + field_t lo_diff = (-lo + (r_lo - 1)) + (borrow * (uint256_t(1) << lo_bits)); hi_diff.create_range_constraint(hi_bits); lo_diff.create_range_constraint(lo_bits); diff --git a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.test.cpp b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.test.cpp index 97f56aec0cc1..1f548f2db3c1 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.test.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.test.cpp @@ -217,3 +217,53 @@ TYPED_TEST(FieldUtilsTests, ValidateSplitWitnessLoConstantHiRejectsModulus) // The circuit should FAIL because value == modulus is invalid EXPECT_FALSE(CircuitChecker::check(builder)); } + +/** + * @brief Test that the constraint rejects value == modulus even with corrupted borrow witness + * @details This test manually builds the constraint logic with a corrupted borrow value (set to 0 + * when it should be 1) to verify that the constraint equation itself is sound. A malicious prover + * cannot bypass the check by choosing an arbitrary borrow value. + * + * Before the fix, with constraint: lo_diff = r_lo - lo + borrow * 2^lo_bits + * - Setting borrow=0 with lo=r_lo gave lo_diff=0, which passed the range check (BUG) + * + * After the fix, with constraint: lo_diff = (r_lo - 1) - lo + borrow * 2^lo_bits + * - Setting borrow=0 with lo=r_lo gives lo_diff=-1, which fails the range check (FIXED) + * - Setting borrow=1 with lo=r_lo gives lo_diff=2^lo_bits - 1, but then hi_diff=-1 fails + */ +TYPED_TEST(FieldUtilsTests, ValidateSplitRejectsModulusWithCorruptedBorrowZero) +{ + using Builder = TypeParam; + using field_t = typename TestFixture::field_t; + using native = typename TestFixture::native; + + Builder builder; + constexpr size_t lo_bits = 128; + const size_t hi_bits = native::modulus.get_msb() + 1 - lo_bits; + + // Use value == modulus + uint256_t modulus = native::modulus; + uint256_t r_lo = modulus.slice(0, lo_bits); + uint256_t r_hi = modulus.slice(lo_bits, native::modulus.get_msb() + 1); + + // Create witnesses for lo = r_lo, hi = r_hi (value == modulus) + auto lo = field_t::from_witness(&builder, native(r_lo)); + auto hi = field_t::from_witness(&builder, native(r_hi)); + + // Malicious prover sets borrow = 0 (trying to bypass the check) + auto borrow = field_t::from_witness(&builder, native(0)); + builder.create_small_range_constraint(borrow.get_witness_index(), 1, "borrow"); + + // Build the constraints manually (matching the fixed implementation) + // hi_diff = r_hi - hi - borrow + // lo_diff = (r_lo - 1) - lo + borrow * 2^lo_bits + field_t hi_diff = (-hi + r_hi) - borrow; + field_t lo_diff = (-lo + (r_lo - 1)) + (borrow * (uint256_t(1) << lo_bits)); + + hi_diff.create_range_constraint(hi_bits); + lo_diff.create_range_constraint(lo_bits); + + // The circuit should FAIL because with borrow=0 and lo=r_lo: + // lo_diff = (r_lo - 1) - r_lo = -1, which underflows and fails the range check + EXPECT_FALSE(CircuitChecker::check(builder)); +} From 8d4f7894ed656375a7e877d0a1b2a021ba41353b Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Thu, 22 Jan 2026 16:37:08 +0000 Subject: [PATCH 18/24] use fr(1) --- .../src/barretenberg/stdlib/primitives/field/field_utils.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp index 7e7eb1cebf68..6a2fd44efb88 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/primitives/field/field_utils.cpp @@ -50,7 +50,7 @@ void validate_split_in_field_unsafe(const field_t& lo, // Hi range check = r_hi - hi - borrow // Lo range check = (r_lo - 1) - lo + borrow * 2^lo_bits field_t hi_diff = (-hi + r_hi) - borrow; - field_t lo_diff = (-lo + (r_lo - 1)) + (borrow * (uint256_t(1) << lo_bits)); + field_t lo_diff = (-lo + (r_lo - fr(1))) + (borrow * (uint256_t(1) << lo_bits)); hi_diff.create_range_constraint(hi_bits); lo_diff.create_range_constraint(lo_bits); From fce6dc55725d324eaba843179cc301422276977c Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Fri, 23 Jan 2026 12:35:26 +0000 Subject: [PATCH 19/24] upd gate count after merge --- .../src/barretenberg/dsl/acir_format/gate_count_constants.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/barretenberg/cpp/src/barretenberg/dsl/acir_format/gate_count_constants.hpp b/barretenberg/cpp/src/barretenberg/dsl/acir_format/gate_count_constants.hpp index fba6b50e5f96..2ccfa0789bd2 100644 --- a/barretenberg/cpp/src/barretenberg/dsl/acir_format/gate_count_constants.hpp +++ b/barretenberg/cpp/src/barretenberg/dsl/acir_format/gate_count_constants.hpp @@ -121,7 +121,7 @@ constexpr std::tuple HONK_RECURSION_CONSTANTS( // ======================================== // Gate count for Chonk recursive verification (UltraRollup builder) -inline constexpr size_t CHONK_RECURSION_GATES = 2385459; +inline constexpr size_t CHONK_RECURSION_GATES = 2385456; // ======================================== // Hypernova Recursion Constants From 0f73dca1487a73c500061ef6d73354b6b980f6d8 Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Fri, 23 Jan 2026 13:23:59 +0000 Subject: [PATCH 20/24] vks hash upd --- .../cpp/scripts/test_chonk_standalone_vks_havent_changed.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 65ef51bfc13a..08e0ae67ebb1 100755 --- a/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_changed.sh +++ b/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_changed.sh @@ -13,7 +13,7 @@ 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="275ed862" +pinned_short_hash="098617ba" pinned_chonk_inputs_url="https://aztec-ci-artifacts.s3.us-east-2.amazonaws.com/protocol/bb-chonk-inputs-${pinned_short_hash}.tar.gz" script_path="$(cd "$(dirname "${BASH_SOURCE[0]}")/scripts" && pwd)/$(basename "${BASH_SOURCE[0]}")" From a7d9f598e1a72881aa74e7375aa26b416134a8e6 Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Fri, 23 Jan 2026 14:30:03 +0000 Subject: [PATCH 21/24] upd vk hash + throw or abort in failure test for wasm --- .../cpp/scripts/test_chonk_standalone_vks_havent_changed.sh | 2 +- .../src/barretenberg/ecc/fields/field_conversion.test.cpp | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) 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 08e0ae67ebb1..554805e35eea 100755 --- a/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_changed.sh +++ b/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_changed.sh @@ -13,7 +13,7 @@ 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="098617ba" +pinned_short_hash="f185b2ed" pinned_chonk_inputs_url="https://aztec-ci-artifacts.s3.us-east-2.amazonaws.com/protocol/bb-chonk-inputs-${pinned_short_hash}.tar.gz" script_path="$(cd "$(dirname "${BASH_SOURCE[0]}")/scripts" && pwd)/$(basename "${BASH_SOURCE[0]}")" diff --git a/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.test.cpp b/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.test.cpp index 4578a9e4e50e..26cc4ddbd402 100644 --- a/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.test.cpp +++ b/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.test.cpp @@ -1,4 +1,5 @@ #include "barretenberg/ecc/fields/field_conversion.hpp" +#include "barretenberg/common/assert.hpp" #include "barretenberg/ecc/curves/grumpkin/grumpkin.hpp" #include @@ -198,13 +199,13 @@ TEST_F(FieldConversionTest, RejectPointNotOnCurve) // Test for BN254: (1, 4) is not on the curve { std::vector fr_vec = { bb::fr(1), bb::fr(0), bb::fr(4), bb::fr(0) }; - EXPECT_THROW(FrCodec::deserialize_from_fields(fr_vec), std::runtime_error); + EXPECT_THROW_OR_ABORT(FrCodec::deserialize_from_fields(fr_vec), "on_curve"); } // Test for Grumpkin: (12, 100) is not on the curve { std::vector fr_vec = { bb::fr(12), bb::fr(100) }; - EXPECT_THROW(FrCodec::deserialize_from_fields(fr_vec), std::runtime_error); + EXPECT_THROW_OR_ABORT(FrCodec::deserialize_from_fields(fr_vec), "on_curve"); } } From ee9d62d4e06529bd781143a1b6ab6e84baa6563a Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Fri, 23 Jan 2026 15:15:07 +0000 Subject: [PATCH 22/24] make wasm threads happy --- .../cpp/src/barretenberg/ecc/fields/field_conversion.test.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.test.cpp b/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.test.cpp index 26cc4ddbd402..4d78507137f3 100644 --- a/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.test.cpp +++ b/barretenberg/cpp/src/barretenberg/ecc/fields/field_conversion.test.cpp @@ -193,7 +193,9 @@ TEST_F(FieldConversionTest, AcceptCanonicalPointAtInfinity) /** * @brief Test that points not on the curve are rejected. + * @note Skipped in WASM builds because death tests (EXPECT_DEATH) aren't supported. */ +#ifndef __wasm__ TEST_F(FieldConversionTest, RejectPointNotOnCurve) { // Test for BN254: (1, 4) is not on the curve @@ -208,5 +210,6 @@ TEST_F(FieldConversionTest, RejectPointNotOnCurve) EXPECT_THROW_OR_ABORT(FrCodec::deserialize_from_fields(fr_vec), "on_curve"); } } +#endif } // namespace bb::field_conversion_tests From d6241c90f39fd01b55dacfe7c4a668d6cb282e82 Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Fri, 23 Jan 2026 17:22:38 +0000 Subject: [PATCH 23/24] fix avm transcript --- .../recursion/recursive_flavor.hpp | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/vm2/constraining/recursion/recursive_flavor.hpp b/barretenberg/cpp/src/barretenberg/vm2/constraining/recursion/recursive_flavor.hpp index e17277a8df7f..075d7fcece4e 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/constraining/recursion/recursive_flavor.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2/constraining/recursion/recursive_flavor.hpp @@ -73,6 +73,8 @@ class AvmRecursiveFlavor { template class TemplatedTranscript : public StdlibTranscript { using Base = StdlibTranscript; using FF = stdlib::field_t; + using StdlibCurve = stdlib::bn254; + using StdlibCommitment = typename StdlibCurve::AffineElement; private: /** @@ -116,21 +118,16 @@ class AvmRecursiveFlavor { } } - size_t proof_idx = 0; - std::span> proof_span = stdlib_proof; - - constexpr size_t num_frs_comm = NativeFlavor::NUM_FRS_COM; for (const auto& wire_label : challenges.get_wires_labels()) { - transcript->add_element_frs_to_hash_buffer(wire_label, proof_span.subspan(proof_idx, num_frs_comm)); - proof_idx += num_frs_comm; + [[maybe_unused]] auto _ = transcript->template receive_from_prover(wire_label); } [[maybe_unused]] auto [_beta, _gamma] = transcript->template get_challenges(std::array{ "beta", "gamma" }); for (const auto& derived_label : challenges.get_derived_labels()) { - transcript->add_element_frs_to_hash_buffer(derived_label, proof_span.subspan(proof_idx, num_frs_comm)); - proof_idx += num_frs_comm; + [[maybe_unused]] auto _ = + transcript->template receive_from_prover(derived_label); } [[maybe_unused]] const FF _alpha = transcript->template get_challenge("Sumcheck:alpha"); @@ -138,18 +135,17 @@ class AvmRecursiveFlavor { [[maybe_unused]] const FF _initial_gate_challenge = transcript->template get_challenge("Sumcheck:gate_challenge"); + using SumcheckUnivariate = std::array; for (size_t i = 0; i < MAX_AVM_TRACE_LOG_SIZE; i++) { std::string round_univariate_label = "Sumcheck:univariate_" + std::to_string(i); - transcript->add_element_frs_to_hash_buffer( - round_univariate_label, proof_span.subspan(proof_idx, AvmFlavor::BATCHED_RELATION_PARTIAL_LENGTH)); - proof_idx += AvmFlavor::BATCHED_RELATION_PARTIAL_LENGTH; + [[maybe_unused]] auto _ = + transcript->template receive_from_prover(round_univariate_label); [[maybe_unused]] FF _round_challenge = transcript->template get_challenge("Sumcheck:u_" + std::to_string(i)); } - transcript->add_element_frs_to_hash_buffer("Sumcheck:evaluations", - proof_span.subspan(proof_idx, NUM_ALL_ENTITIES)); - proof_idx += NUM_ALL_ENTITIES; + [[maybe_unused]] auto _evals = + transcript->template receive_from_prover>("Sumcheck:evaluations"); [[maybe_unused]] auto _unshifted_challenges = transcript->template get_challenges(challenges.get_unshifted_labels()); @@ -157,28 +153,25 @@ class AvmRecursiveFlavor { [[maybe_unused]] const FF _gemini_batching_challenge = transcript->template get_challenge("rho"); for (size_t i = 1; i < MAX_AVM_TRACE_LOG_SIZE; ++i) { - transcript->add_element_frs_to_hash_buffer("Gemini:FOLD_" + std::to_string(i), - proof_span.subspan(proof_idx, num_frs_comm)); - proof_idx += num_frs_comm; + [[maybe_unused]] auto _ = + transcript->template receive_from_prover("Gemini:FOLD_" + std::to_string(i)); } [[maybe_unused]] const FF _gemini_evaluation_challenge = transcript->template get_challenge("Gemini:r"); for (size_t i = 1; i <= MAX_AVM_TRACE_LOG_SIZE; ++i) { - transcript->add_to_hash_buffer("Gemini:a_" + std::to_string(i), proof_span[proof_idx++]); + [[maybe_unused]] auto _ = transcript->template receive_from_prover("Gemini:a_" + std::to_string(i)); } [[maybe_unused]] const FF _shplonk_batching_challenge = transcript->template get_challenge("Shplonk:nu"); - transcript->add_element_frs_to_hash_buffer("Shplonk:Q", proof_span.subspan(proof_idx, num_frs_comm)); - proof_idx += num_frs_comm; + [[maybe_unused]] auto _shplonk_q = transcript->template receive_from_prover("Shplonk:Q"); [[maybe_unused]] const FF _shplonk_evaluation_challenge = transcript->template get_challenge("Shplonk:z"); - transcript->add_element_frs_to_hash_buffer("KZG:W", proof_span.subspan(proof_idx, num_frs_comm)); - proof_idx += num_frs_comm; + [[maybe_unused]] auto _kzg_w = transcript->template receive_from_prover("KZG:W"); [[maybe_unused]] const FF _masking_challenge = transcript->template get_challenge("KZG:masking_challenge"); From b63426933823b120087b368445777ac2d67736c5 Mon Sep 17 00:00:00 2001 From: iakovenkos Date: Fri, 23 Jan 2026 17:29:47 +0000 Subject: [PATCH 24/24] oops --- .../vm2/constraining/recursion/recursive_flavor.hpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/vm2/constraining/recursion/recursive_flavor.hpp b/barretenberg/cpp/src/barretenberg/vm2/constraining/recursion/recursive_flavor.hpp index 075d7fcece4e..59f90f8238ab 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/constraining/recursion/recursive_flavor.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2/constraining/recursion/recursive_flavor.hpp @@ -126,8 +126,7 @@ class AvmRecursiveFlavor { transcript->template get_challenges(std::array{ "beta", "gamma" }); for (const auto& derived_label : challenges.get_derived_labels()) { - [[maybe_unused]] auto _ = - transcript->template receive_from_prover(derived_label); + [[maybe_unused]] auto _ = transcript->template receive_from_prover(derived_label); } [[maybe_unused]] const FF _alpha = transcript->template get_challenge("Sumcheck:alpha");