diff --git a/barretenberg/cpp/scripts/test_civc_standalone_vks_havent_changed.sh b/barretenberg/cpp/scripts/test_civc_standalone_vks_havent_changed.sh index 1866f47626c9..98ada8cbd720 100755 --- a/barretenberg/cpp/scripts/test_civc_standalone_vks_havent_changed.sh +++ b/barretenberg/cpp/scripts/test_civc_standalone_vks_havent_changed.sh @@ -14,7 +14,7 @@ cd .. # - Upload the compressed results: aws s3 cp bb-civc-inputs.tar.gz s3://aztec-ci-artifacts/protocol/bb-civc-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="f2981f9f" +pinned_short_hash="ec9b5be3" pinned_civc_inputs_url="https://aztec-ci-artifacts.s3.us-east-2.amazonaws.com/protocol/bb-civc-inputs-${pinned_short_hash}.tar.gz" function compress_and_upload { diff --git a/barretenberg/cpp/src/barretenberg/boomerang_value_detection/graph_description_poseidon2s_permutation.test.cpp b/barretenberg/cpp/src/barretenberg/boomerang_value_detection/graph_description_poseidon2s_permutation.test.cpp index 19380bcceda1..2ce179f82d7b 100644 --- a/barretenberg/cpp/src/barretenberg/boomerang_value_detection/graph_description_poseidon2s_permutation.test.cpp +++ b/barretenberg/cpp/src/barretenberg/boomerang_value_detection/graph_description_poseidon2s_permutation.test.cpp @@ -20,7 +20,7 @@ auto& engine = numeric::get_debug_randomness(); using Params = crypto::Poseidon2Bn254ScalarFieldParams; using Builder = UltraCircuitBuilder; -using Permutation = stdlib::Poseidon2Permutation; +using Permutation = stdlib::Poseidon2Permutation; using field_t = stdlib::field_t; using witness_t = stdlib::witness_t; using _curve = stdlib::bn254; @@ -70,7 +70,7 @@ void test_poseidon2s_circuit(size_t num_inputs = 5) for (auto& elem : inputs) { elem.fix_witness(); } - [[maybe_unused]] auto result = stdlib::poseidon2::hash(builder, inputs); + [[maybe_unused]] auto result = stdlib::poseidon2::hash(inputs); auto graph = StaticAnalyzer(builder); auto connected_components = graph.find_connected_components(); EXPECT_EQ(connected_components.size(), 1); @@ -104,7 +104,7 @@ void test_poseidon2s_hash_repeated_pairs(size_t num_inputs = 5) std::unordered_set outputs{ left.witness_index }; // num_inputs - 1 iterations since the first hash hashes two elements for (size_t i = 0; i < num_inputs - 1; ++i) { - left = stdlib::poseidon2::hash(builder, { left, right }); + left = stdlib::poseidon2::hash({ left, right }); outputs.insert(left.witness_index + 1); outputs.insert(left.witness_index + 2); outputs.insert(left.witness_index + 3); diff --git a/barretenberg/cpp/src/barretenberg/dsl/acir_format/poseidon2_constraint.cpp b/barretenberg/cpp/src/barretenberg/dsl/acir_format/poseidon2_constraint.cpp index 27b48961184e..2072c262fe3e 100644 --- a/barretenberg/cpp/src/barretenberg/dsl/acir_format/poseidon2_constraint.cpp +++ b/barretenberg/cpp/src/barretenberg/dsl/acir_format/poseidon2_constraint.cpp @@ -17,8 +17,7 @@ using namespace bb; template void create_poseidon2_permutations(Builder& builder, const Poseidon2Constraint& constraint) { using field_ct = stdlib::field_t; - using Poseidon2Params = crypto::Poseidon2Bn254ScalarFieldParams; - using State = std::array; + using State = stdlib::Poseidon2Permutation::State; BB_ASSERT_EQ(constraint.state.size(), 4U); BB_ASSERT_EQ(constraint.result.size(), 4U); @@ -29,19 +28,9 @@ template void create_poseidon2_permutations(Builder& builder, state[i] = to_field_ct(constraint.state[i], builder); } State output_state; - output_state = stdlib::Poseidon2Permutation::permutation(&builder, state); + output_state = stdlib::Poseidon2Permutation::permutation(&builder, state); for (size_t i = 0; i < output_state.size(); ++i) { - poly_triple assert_equal{ - .a = output_state[i].normalize().witness_index, - .b = constraint.result[i], - .c = 0, - .q_m = 0, - .q_l = 1, - .q_r = -1, - .q_o = 0, - .q_c = 0, - }; - builder.create_poly_gate(assert_equal); + output_state[i].assert_equal(field_ct::from_witness_index(&builder, constraint.result[i])); } } diff --git a/barretenberg/cpp/src/barretenberg/flavor/flavor.hpp b/barretenberg/cpp/src/barretenberg/flavor/flavor.hpp index 2431d4199ecf..8b932d4d1bf4 100644 --- a/barretenberg/cpp/src/barretenberg/flavor/flavor.hpp +++ b/barretenberg/cpp/src/barretenberg/flavor/flavor.hpp @@ -356,9 +356,9 @@ class StdlibVerificationKey_ : public PrecomputedCommitments { * @param builder * @return FF */ - FF hash(Builder& builder) + FF hash() { - FF vk_hash = stdlib::poseidon2::hash(builder, to_field_elements()); + FF vk_hash = stdlib::poseidon2::hash(to_field_elements()); return vk_hash; } diff --git a/barretenberg/cpp/src/barretenberg/flavor/stdlib_verification_key.test.cpp b/barretenberg/cpp/src/barretenberg/flavor/stdlib_verification_key.test.cpp index 173ca06cf389..ecb0c75335c2 100644 --- a/barretenberg/cpp/src/barretenberg/flavor/stdlib_verification_key.test.cpp +++ b/barretenberg/cpp/src/barretenberg/flavor/stdlib_verification_key.test.cpp @@ -79,7 +79,7 @@ TYPED_TEST(StdlibVerificationKeyTests, VKHashingConsistency) } FF vk_hash_1 = transcript.hash_independent_buffer(); // Second method of hashing: using hash(). - FF vk_hash_2 = vk.hash(outer_builder); + FF vk_hash_2 = vk.hash(); EXPECT_EQ(vk_hash_1.get_value(), vk_hash_2.get_value()); // Third method of hashing: using hash_through_transcript. if constexpr (!IsAnyOf) { diff --git a/barretenberg/cpp/src/barretenberg/polynomials/univariate.hpp b/barretenberg/cpp/src/barretenberg/polynomials/univariate.hpp index 92a99892e73e..228de6fe30e6 100644 --- a/barretenberg/cpp/src/barretenberg/polynomials/univariate.hpp +++ b/barretenberg/cpp/src/barretenberg/polynomials/univariate.hpp @@ -69,6 +69,7 @@ template Univariate(UnivariateCoefficientBasis monomial) { static_assert(domain_start == 0); @@ -86,6 +87,7 @@ template Univariate(UnivariateCoefficientBasis monomial) { static_assert(domain_start == 0); diff --git a/barretenberg/cpp/src/barretenberg/relations/poseidon2_external_relation.hpp b/barretenberg/cpp/src/barretenberg/relations/poseidon2_external_relation.hpp index 14eef2802d50..7512579fd841 100644 --- a/barretenberg/cpp/src/barretenberg/relations/poseidon2_external_relation.hpp +++ b/barretenberg/cpp/src/barretenberg/relations/poseidon2_external_relation.hpp @@ -29,28 +29,60 @@ template class Poseidon2ExternalRelationImpl { } /** - * @brief Expression for the poseidon2 external round relation, based on E_i in Section 6 of + * @brief Expression for the poseidon2 external round relation, based on \f$ E_i \f$ in Section 6 of * https://eprint.iacr.org/2023/323.pdf. - * @details This relation is defined as C(in(X)...) := - * q_poseidon2_external * ( (v1 - w_1_shift) + \alpha * (v2 - w_2_shift) + - * \alpha^2 * (v3 - w_3_shift) + \alpha^3 * (v4 - w_4_shift) ) = 0 where: - * u1 := (w_1 + q_1)^5 - * u2 := (w_2 + q_2)^5 - * u3 := (w_3 + q_3)^5 - * u4 := (w_4 + q_4)^5 - * t0 := u1 + u2 (1, 1, 0, 0) - * t1 := u3 + u4 (0, 0, 1, 1) - * t2 := 2 * u2 + t1 = 2 * u2 + u3 + u4 (0, 2, 1, 1) - * t3 := 2 * u4 + t0 = u1 + u2 + 2 * u4 (1, 1, 0, 2) - * v4 := 4 * t1 + t3 = u1 + u2 + 4 * u3 + 6 * u4 (1, 1, 4, 6) - * v2 := 4 * t0 + t2 = 4 * u1 + 6 * u2 + u3 + u4 (4, 6, 1, 1) - * v1 := t3 + v2 = 5 * u1 + 7 * u2 + 1 * u3 + 3 * u4 (5, 7, 1, 3) - * v3 := t2 + v4 (1, 3, 5, 7) + * @details For state \f$ \mathbf{u} = (u_1, u_2, u_3, u_4)\f$ with \f$ u_i = \big(w_i + c_i^{(i)}\big)^5 \f$, the + * external round computes \f$ \mathbf{v} = M_E \cdot \mathbf{u}^{\top}\f$, where \f$M_E\f$ is the external round + * matrix defined as follows: + * + * \f[ + * M_E = + * \begin{bmatrix} + * 5 & 7 & 1 & 3 \\ + * 4 & 6 & 1 & 1 \\ + * 1 & 3 & 5 & 7 \\ + * 1 & 1 & 4 & 6 + * \end{bmatrix} + * \f] + * + * i.e. + * \f{align}{ + * v_1 &= 5u_1 + 7u_2 + u_3 + 3u_4 \\ + * v_2 &= 4u_1 + 6u_2 + u_3 + u_4 \\ + * v_3 &= u_1 + 3u_2 + 5u_3 + 7u_4 \\ + * v_4 &= u_1 + u_2 + 4u_3 + 6u_4 + * \f} + * + * The relation enforces \f$ v_k = w_{k,shift}\f$ for \f$ k \in \{1,2,3,4\}\f$. + * 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 accumulator \f$ A_i + * \f$: + * \f{align}{ + * A_1 &\;\mathrel{+}= \text{q_poseidon2_internal}\cdot \big(v_1 - w_{1,\text{shift}}\big) \cdot \hat{g} \\ + * A_2 &\;\mathrel{+}= \text{q_poseidon2_internal}\cdot \big(v_1 - w_{1,\text{shift}}\big) \cdot \hat{g} \\ + * A_3 &\;\mathrel{+}= \text{q_poseidon2_internal}\cdot \big(v_3 - w_{3,\text{shift}}\big) \cdot \hat{g} \\ + * A_4 &\;\mathrel{+}= \text{q_poseidon2_internal}\cdot \big(v_4 - w_{4,\text{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, \text{Poseidon2Ext}}\f$ taken from the array of `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 corresponding to this relation consists + * of \f$ [A_0, A_1, A_2, A_3]\f$ , such that + * \f$ \deg(A_i) = \text{SUBRELATION_PARTIAL_LENGTHS}[i] - 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 + * \f$d-k-1\f$-dimensional hypercube, given by an array containing the restrictions of the prover polynomials + * \f$ P_i(u_{k}) \f$. + * @param parameters Not used in this relation + * @param scaling_factor scaling term coming from `GateSeparatorPolynomial`. * - * @param evals transformed to `evals + C(in(X)...)*scaling_factor` - * @param in an std::array containing the fully extended Univariate edges. - * @param parameters contains beta, gamma, and public_input_delta, .... - * @param scaling_factor optional term to scale the evaluation before adding to evals. */ template void static accumulate(ContainerOverSubrelations& evals, @@ -58,70 +90,73 @@ template class Poseidon2ExternalRelationImpl { const Parameters&, const FF& scaling_factor) { + // Univariates of degree 6 represented in Lagrange basis using Accumulator = std::tuple_element_t<0, ContainerOverSubrelations>; + // Low-degree univariates represented in monomial basis using CoefficientAccumulator = typename Accumulator::CoefficientAccumulator; - auto w_l = CoefficientAccumulator(in.w_l); - auto w_r = CoefficientAccumulator(in.w_r); - auto w_o = CoefficientAccumulator(in.w_o); - auto w_4 = CoefficientAccumulator(in.w_4); - auto w_l_shift = CoefficientAccumulator(in.w_l_shift); - auto w_r_shift = CoefficientAccumulator(in.w_r_shift); - auto w_o_shift = CoefficientAccumulator(in.w_o_shift); - auto w_4_shift = CoefficientAccumulator(in.w_4_shift); - auto q_l = CoefficientAccumulator(in.q_l); - auto q_r = CoefficientAccumulator(in.q_r); - auto q_o = CoefficientAccumulator(in.q_o); - auto q_4 = CoefficientAccumulator(in.q_4); - auto q_poseidon2_external = CoefficientAccumulator(in.q_poseidon2_external); + + // Current state + const auto w_1 = CoefficientAccumulator(in.w_l); + const auto w_2 = CoefficientAccumulator(in.w_r); + const auto w_3 = CoefficientAccumulator(in.w_o); + const auto w_4 = CoefficientAccumulator(in.w_4); + // Expected state, contained in the next row + const auto w_1_shift = CoefficientAccumulator(in.w_l_shift); + const auto w_2_shift = CoefficientAccumulator(in.w_r_shift); + const auto w_3_shift = CoefficientAccumulator(in.w_o_shift); + const auto w_4_shift = CoefficientAccumulator(in.w_4_shift); + // i-th external round constants + const auto c_1 = CoefficientAccumulator(in.q_l); + const auto c_2 = CoefficientAccumulator(in.q_r); + const auto c_3 = CoefficientAccumulator(in.q_o); + const auto c_4 = CoefficientAccumulator(in.q_4); + // Poseidon2 external relation selector + const auto q_poseidon2_external = CoefficientAccumulator(in.q_poseidon2_external); // add round constants which are loaded in selectors - auto s1 = Accumulator(w_l + q_l); - auto s2 = Accumulator(w_r + q_r); - auto s3 = Accumulator(w_o + q_o); - auto s4 = Accumulator(w_4 + q_4); + auto sbox = [](const Accumulator& x) { + auto t2 = x.sqr(); // x^2 + auto t4 = t2.sqr(); // x^4 + return t4 * x; // x^5 + }; // apply s-box round - auto u1 = s1.sqr(); - u1 = u1.sqr(); - u1 *= s1; - auto u2 = s2.sqr(); - u2 = u2.sqr(); - u2 *= s2; - auto u3 = s3.sqr(); - u3 = u3.sqr(); - u3 *= s3; - auto u4 = s4.sqr(); - u4 = u4.sqr(); - u4 *= s4; - - // matrix mul v = M_E * u with 14 additions + auto u1 = sbox(Accumulator(w_1 + c_1)); + auto u2 = sbox(Accumulator(w_2 + c_2)); + auto u3 = sbox(Accumulator(w_3 + c_3)); + auto u4 = sbox(Accumulator(w_4 + c_4)); + // Matrix mul v = M_E * u with 14 additions. + // Precompute common summands. auto t0 = u1 + u2; // u_1 + u_2 auto t1 = u3 + u4; // u_3 + u_4 auto t2 = u2 + u2; // 2u_2 t2 += t1; // 2u_2 + u_3 + u_4 auto t3 = u4 + u4; // 2u_4 t3 += t0; // u_1 + u_2 + 2u_4 + + // Row 4: u_1 + u_2 + 4u_3 + 6u_4 auto v4 = t1 + t1; v4 += v4; - v4 += t3; // u_1 + u_2 + 4u_3 + 6u_4 + v4 += t3; + + // Row 2: 4u_1 + 6u_2 + u_3 + u_4 auto v2 = t0 + t0; v2 += v2; - v2 += t2; // 4u_1 + 6u_2 + u_3 + u_4 - auto v1 = t3 + v2; // 5u_1 + 7u_2 + u_3 + 3u_4 - auto v3 = t2 + v4; // u_1 + 3u_2 + 5u_3 + 7u_4 + v2 += t2; + // Row 1: 5u_1 + 7u_2 + u_3 + 3u_4 + auto v1 = t3 + v2; + + // Row 3: u_1 + 3u_2 + 5u_3 + 7u_4 + auto v3 = t2 + v4; auto q_pos_by_scaling = Accumulator(q_poseidon2_external * scaling_factor); - auto tmp = q_pos_by_scaling * (v1 - Accumulator(w_l_shift)); - std::get<0>(evals) += tmp; + std::get<0>(evals) += q_pos_by_scaling * (v1 - Accumulator(w_1_shift)); - tmp = q_pos_by_scaling * (v2 - Accumulator(w_r_shift)); - std::get<1>(evals) += tmp; + std::get<1>(evals) += q_pos_by_scaling * (v2 - Accumulator(w_2_shift)); - tmp = q_pos_by_scaling * (v3 - Accumulator(w_o_shift)); - std::get<2>(evals) += tmp; + std::get<2>(evals) += q_pos_by_scaling * (v3 - Accumulator(w_3_shift)); - tmp = q_pos_by_scaling * (v4 - Accumulator(w_4_shift)); - std::get<3>(evals) += tmp; + std::get<3>(evals) += q_pos_by_scaling * (v4 - Accumulator(w_4_shift)); }; }; diff --git a/barretenberg/cpp/src/barretenberg/relations/poseidon2_internal_relation.hpp b/barretenberg/cpp/src/barretenberg/relations/poseidon2_internal_relation.hpp index 5dffb71cb02e..06ec09b41afb 100644 --- a/barretenberg/cpp/src/barretenberg/relations/poseidon2_internal_relation.hpp +++ b/barretenberg/cpp/src/barretenberg/relations/poseidon2_internal_relation.hpp @@ -21,33 +21,78 @@ 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; /** * @brief Returns true if the contribution from all subrelations for the provided inputs is identically zero * */ template inline static bool skip(const AllEntities& in) { - return in.q_poseidon2_internal.is_zero(); + return (in.q_poseidon2_internal.is_zero()); } /** - * @brief Expression for the poseidon2 internal round relation, based on I_i in Section 6 of + * @brief Expression for the Poseidon2 internal round relation, based on I_i in Section 6 of * https://eprint.iacr.org/2023/323.pdf. - * @details This relation is defined as C(in(X)...) := - * q_poseidon2_internal * ( (v1 - w_1_shift) + \alpha * (v2 - w_2_shift) + - * \alpha^2 * (v3 - w_3_shift) + \alpha^3 * (v4 - w_4_shift) ) = 0 where: - * u1 := (w_1 + q_1)^5 - * sum := u1 + w_2 + w_3 + w_4 - * v1 := u1 * D1 + sum - * v2 := w_2 * D2 + sum - * v3 := w_3 * D3 + sum - * v4 := w_4 * D4 + sum - * Di is the ith internal diagonal value - 1 of the internal matrix M_I * - * @param evals transformed to `evals + C(in(X)...)*scaling_factor` - * @param in an std::array containing the fully extended Univariate edges. - * @param parameters contains beta, gamma, and public_input_delta, .... - * @param scaling_factor optional term to scale the evaluation before adding to evals. + * @details Let the internal round matrix M_I be the 4×4 matrix + * \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 + * \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 + * \f$ d-k-1 \f$ dimensional hypercube, an array of restrictions of the prover polynomials + * \f$ P_i(u_{k}) \f$. + * @param parameters Not used in this relation. + * @param scaling_factor Scaling term \f$ \hat{g} \f$ from the GateSeparatorPolynomial. */ template void static accumulate(ContainerOverSubrelations& evals, @@ -55,59 +100,62 @@ template class Poseidon2InternalRelationImpl { const Parameters&, const FF& scaling_factor) { + // Univariates of degree 6 represented in Lagrange basis using Accumulator = std::tuple_element_t<0, ContainerOverSubrelations>; + // Low-degree univariates represented in monomial basis using CoefficientAccumulator = typename Accumulator::CoefficientAccumulator; - auto w_l_m = CoefficientAccumulator(in.w_l); - auto w_l_shift_m = CoefficientAccumulator(in.w_l_shift); - auto w_r_shift_m = CoefficientAccumulator(in.w_r_shift); - auto w_o_shift_m = CoefficientAccumulator(in.w_o_shift); - auto w_4_shift_m = CoefficientAccumulator(in.w_4_shift); - auto q_l_m = CoefficientAccumulator(in.q_l); - auto q_poseidon2_internal_m = CoefficientAccumulator(in.q_poseidon2_internal); - - // add round constants - auto s1 = Accumulator(w_l_m + q_l_m); - - // apply s-box round + // Current state + const auto w_1 = CoefficientAccumulator(in.w_l); + const auto w_2 = CoefficientAccumulator(in.w_r); + const auto w_3 = CoefficientAccumulator(in.w_o); + const auto w_4 = CoefficientAccumulator(in.w_4); + // Expected state, contained in the next row + const auto w_1_shift = CoefficientAccumulator(in.w_l_shift); + const auto w_2_shift = CoefficientAccumulator(in.w_r_shift); + const auto w_3_shift = CoefficientAccumulator(in.w_o_shift); + const auto w_4_shift = CoefficientAccumulator(in.w_4_shift); + // Poseidon2 internal relation selector + const auto q_poseidon2_internal_m = CoefficientAccumulator(in.q_poseidon2_internal); + // ĉ₀⁽ⁱ⁾ - the round constant in `i`-th internal round + const auto c_0_int = CoefficientAccumulator(in.q_l); + + Accumulator barycentric_term; + + // Add ĉ₀⁽ⁱ⁾ stored in the selector and convert to Lagrange basis + auto s1 = Accumulator(w_1 + c_0_int); + + // Apply S-box. Note that the multiplication is performed point-wise auto u1 = s1.sqr(); u1 = u1.sqr(); u1 *= s1; - auto u2_m = CoefficientAccumulator(in.w_r); - auto u3_m = CoefficientAccumulator(in.w_o); - auto u4_m = CoefficientAccumulator(in.w_4); - - auto q_pos_by_scaling_m = (q_poseidon2_internal_m * scaling_factor); - auto q_pos_by_scaling = Accumulator(q_pos_by_scaling_m); - // matrix mul with v = M_I * u 4 muls and 7 additions - auto partial_sum = u2_m + u3_m + u4_m; - auto scaled_u1 = u1 * q_pos_by_scaling; - - static const auto diagonal_term = FF(1) + crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal[0]; - auto barycentric_term = scaled_u1 * (diagonal_term); - auto monomial_term = partial_sum; - monomial_term -= w_l_shift_m; + + const auto q_pos_by_scaling_m = (q_poseidon2_internal_m * scaling_factor); + const auto q_pos_by_scaling = Accumulator(q_pos_by_scaling_m); + // Common terms + 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; + auto monomial_term = partial_sum - w_1_shift; barycentric_term += Accumulator(monomial_term * q_pos_by_scaling_m); std::get<0>(evals) += barycentric_term; - auto v2_m = u2_m * crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal[1]; - v2_m += partial_sum; - v2_m -= w_r_shift_m; + // Row 2: + auto v2_m = w_2 * D2 + 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; - auto v3_m = u3_m * crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal[2]; - v3_m += partial_sum; - v3_m -= w_o_shift_m; + // Row 3: + auto v3_m = w_3 * D3 + 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; - auto v4_m = u4_m * crypto::Poseidon2Bn254ScalarFieldParams::internal_matrix_diagonal[3]; - v4_m += partial_sum; - v4_m -= w_4_shift_m; - + // Row 4: + auto v4_m = w_4 * D4 + 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/stdlib/hash/pfuzzer/poseidon2_pedersen.fuzzer.cpp b/barretenberg/cpp/src/barretenberg/stdlib/hash/pfuzzer/poseidon2_pedersen.fuzzer.cpp index c4b3ad421ef5..9ca60f5c6998 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/hash/pfuzzer/poseidon2_pedersen.fuzzer.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/hash/pfuzzer/poseidon2_pedersen.fuzzer.cpp @@ -118,7 +118,7 @@ template bool test_poseidon2_circuit(const std::vector& i } // Compute hash using circuit - auto circuit_result = stdlib::poseidon2::hash(builder, circuit_inputs); + auto circuit_result = stdlib::poseidon2::hash(circuit_inputs); // Compute hash using native implementation auto native_result = native_poseidon2::hash(inputs); diff --git a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/README.md b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/README.md new file mode 100644 index 000000000000..ee780cb08547 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/README.md @@ -0,0 +1,153 @@ + +# stdlib Poseidon2 Hash Implementation + +Poseidon2 is a **SNARK-friendly cryptographic hash** designed to be efficient inside prime-field arithmetic circuits. +It follows the [Poseidon2 paper](https://eprint.iacr.org/2023/323.pdf) and refines the original Poseidon hash. + +This implementation includes: + +- A **sponge construction** over the BN254 scalar field following the (draft) C2SP Poseidon Sponge spec based on the [Duplex Sponge model](https://keccak.team/files/SpongeDuplex.pdf). +- The **Poseidon2 permutation**, i.e.\ the round function used by the sponge. +- **Circuit custom gate relations** that enforce the permutation’s correctness. + + +## The Sponge Construction + +The sponge absorbs input elements into an internal state, applies permutations, and squeezes output elements. + +#### Sponge constants. + - **State size (t)**: 4 field elements + - **Rate (r)**: 3 elements + - **Capacity (c)**: 1 element + + +### Details + +Let the input be +\f[ +\mathbf{a} = (a_0, a_1, \dots, a_{N-1}). +\f] +Partition it into blocks of size \f$r=3\f$: +\f[ +B_j = (a_{3j},, a_{3j+1},, a_{3j+2}) \quad\text{(pad missing entries with 0)},\qquad +m = \left\lceil \frac{N}{3}\right\rceil . +\f] + +### Padding +In Poseidon paper, the padding scheme for variable input length hashing suggests padding with \f$ 10^\ast\f$. + +"Domain Separation for Poseidon" section (see 4.2 in [Poseidon](https://eprint.iacr.org/2019/458.pdf)) suggests using domain separation IV defined as follows +\f[ + \mathrm{IV} = (\texttt{input_length}^{64}) +\f] +Initialize the state: +\f[ + \mathbf{s}^{(0)} = (0,0,0,\mathrm{IV}). +\f] + +Since we only use Poseidon2 sponge with variable length inputs and the length is a part of domain separation, we can pad the inputs with \f$ 0^\ast \f$, which would not lead to collisions (tested \ref StdlibPoseidon2< Builder >::test_padding_collisions "here"). + +Note that we initialize \f$ \mathrm{IV} \f$ as a fixed witness. It ensures that the first invocation of the Poseidon2 permutation leads to a state where all entries are **normalized** witnesses, i.e. they have `multiplicative_constant` equal 1, and `additive_constant` equal 0. + +#### Absorb phase + +For each block \f$j=0,\dots,m-1\f$, +\f[ +\mathbf{s}^{(j+1)} = P\left(\mathbf{s}^{(j)} + (B_j,0)\right), +\f] +where \f$P\f$ is the Poseidon2 permutation and \f$(B_j,0)\f$ is an array of size \f$ 4 \f$ with \f$r\f$ state elements and a \f$0\f$ capacity limb. + +#### Squeeze (single output) + +After absorption, produce one output field element via one duplex step: +\f[ +y_0 = \big(P(\mathbf{s}^{(m)})\big)_0. +\f] + +## The Poseidon2 Permutation + +Each permutation consists of: + +1. **Initial linear layer**: multiply state by external matrix \f$M_E\f$. Corresponds to \ref bb::stdlib::Poseidon2Permutation< Builder >::matrix_multiplication_external "matrix_multiplication_external" method. +2. **4 External rounds (full S-box)**: + - Record the state and the correspoding round constants \f$ c_{0}^{(i)} \f$ into a \ref bb::UltraCircuitBuilder_< FF >::create_poseidon2_external_gate "Poseidon2 External Gate". + - _Natively_ compute the next state. + - Re-write the state with the new witnesses. + - After the final round, \ref bb::stdlib::Poseidon2Permutation< Builder >::record_current_state_into_next_row "record the computed state" in the next row of the Poseidon2 **external** gates block, + as it is required for the custom gate relation. +3. **56 Internal rounds (partial S-box)**: + - Record the state and the correspoding round constants \f$ c_{0}^{(i)} \f$ into a \ref bb::UltraCircuitBuilder_< FF >::create_poseidon2_internal_gate "Poseidon2 Internal Gate". + - _Natively_ compute the next state. + - Re-write the state with the new witnesses. + - After the final round, \ref bb::stdlib::Poseidon2Permutation< Builder >::record_current_state_into_next_row "record the computed state" in the next row of the Poseidon2 **internal** gates block, +4. **Final external rounds** (same as step 2). + +Note that in general, step 1 requires 6 arithmetic gates, the steps 2-4 create total number of rounds + 3 gates. Hence a single invocation of Poseidon2 Permutation results in 73 gates. + +### External Matrix +As proposed in Section 5.1 of [Poseidon2 paper](https://eprint.iacr.org/2023/323.pdf), we set +\f[ +M_E = + \begin{bmatrix} + 5 & 7 & 1 & 3 \\ + 4 & 6 & 1 & 1 \\ + 1 & 3 & 5 & 7 \\ + 1 & 1 & 4 & 6 + \end{bmatrix} +\f] + + +### Internal 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} +\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. + +### Security Level +Based on Section 3.2 of [Poseidon2 paper](https://eprint.iacr.org/2023/323.pdf). + +Given \f$ R_P = 56 \f$, \f$ R_F = 8\f$, \f$ d = 5\f$, \f$ \log_2(p) \approx 254 \f$, we get \f$ 128 \f$ bits of security. + +## Custom Gate Relations + +For an external round with state \f$ \mathbf{u}=(u_1,u_2,u_3,u_4) \f$, define \f$ \mathbf{v}=M_E\cdot\mathbf{u}\f$. +\ref bb::Poseidon2ExternalRelationImpl< FF_ > "Poseidon2 External Relation" enforces that the permuted values equal the values in the next row (accessed via shifts): +\f[ +v_k = w_{k,\mathrm{shift}} \qquad \text{for } k \in \{1,2,3,4\}. +\f] + +We encode four independent constraints under a selector \f$ q_{\mathrm{poseidon2_external}}\f$ and aggregate them with +independent challenges \f$ \alpha_i = \alpha_{i, Poseidon2_ext}\f$ from `SubrelationSeparators`: +\f[ +q_{\mathrm{poseidon2_external}}\cdot +\Big( +\alpha_0\big(v_1 - w_{1,\mathrm{shift}}\big) + +\alpha_1\big(v_2 - w_{2,\mathrm{shift}}\big) + +\alpha_2\big(v_3 - w_{3,\mathrm{shift}}\big) + +\alpha_3\big(v_4 - w_{4,\mathrm{shift}}\big) +\Big) = 0. +\f] +To ensure that the relation holds point-wise on the hypercube, the equation above is also multiplied by the appropriate +scaling factor arising from \ref bb::GateSeparatorPolynomial< FF > "GateSeparatorPolynomial". + +\ref bb::Poseidon2InternalRelationImpl< FF_ > "Internal rounds" follow the same pattern, using \f$ M_I \f$ and the partial S-box on the first element. + + +## Number of Gates + +Hashing a single field element costs \f$ 73 \f$ gates. As above, let \f$ N > 1\f$ be the input size. Define \f$ m = \lceil N/3 \rceil \f$ and let \f$ N_3 = N\pmod{3} \f$. The number of gates depends on the number of padded fields equal to \f$ N_3 \f$. If \f$ N_3 = 0\f$, we get +\f[ 1 + 73\cdot m + 3\cdot (m - 1) \f] +gates, otherwise we get +\f[ 1 + 73\cdot m + 3\cdot (m - 2) + N_3.\f] + +According to TACEO blog post [Poseidon{2} for Noir](https://core.taceo.io/articles/poseidon2-for-noir/), a single permutation cost for \f$ t = 4 \f$ implemented without Poseidon2 custom gates is \f$ 2313 \f$ gates. 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 new file mode 100644 index 000000000000..f6140eaadaac --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.circuit.failure.test.cpp @@ -0,0 +1,183 @@ +#include "barretenberg/flavor/ultra_flavor.hpp" +#include "barretenberg/relations/relation_parameters.hpp" +#include "barretenberg/sumcheck/sumcheck.hpp" + +#include + +using namespace bb; + +class Poseidon2FailureTests : public ::testing::Test { + public: + using Flavor = UltraFlavor; + using DeciderProvingKey = DeciderProvingKey_; + using SumcheckProver = SumcheckProver; + using SumcheckVerifier = SumcheckVerifier; + using FF = Flavor::FF; + using Builder = Flavor::CircuitBuilder; + using Transcript = Flavor::Transcript; + using SubrelationSeparators = Flavor::SubrelationSeparators; + 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(); + size_t end_idx = selector.end_index(); + + size_t selector_enabled_idx{ 0 }; + // Find the first row index where the selector is enabled. + for (size_t idx = start_idx; idx < end_idx; idx++) { + if (selector.at(idx) == 1) { + selector_enabled_idx = idx; + break; + } + } + // Modify the witness + witness.at(selector_enabled_idx) += 1; + } + void tamper_with_shifts(const auto& selector, auto& witness, bool external) + { + size_t start_idx = selector.start_index(); + size_t end_idx = selector.end_index(); + + size_t selector_enabled_idx{ 0 }; + + for (size_t idx = start_idx; idx < end_idx; idx++) { + if (selector.at(idx) == 1) { + selector_enabled_idx = idx; + break; + } + } + const size_t round_size = external ? 4 : 56; + size_t shift_idx = selector_enabled_idx + round_size; + // The selector must be zero at the row corresponding to the shift. + EXPECT_EQ(selector.at(shift_idx), 0); + // Modify the witness value. As Poseidon2ExternalRelation is comparing this value to the result of applying the + // S-box and M_E to the previous row, this must lead to a sumcheck failure. + witness.at(shift_idx) += 1; + } + + void hash_single_input(Builder& builder) + { + stdlib::field_t random_input(stdlib::witness_t(&builder, fr::random_element())); + random_input.fix_witness(); + [[maybe_unused]] auto hash = stdlib::poseidon2::hash({ random_input }); + } + + void prove_and_verify(const std::shared_ptr& proving_key, 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. + SubrelationSeparators subrelation_separators{}; + for (auto& alpha : subrelation_separators) { + alpha = FF::random_element(); + } + + 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(); + } + + RelationParameters relation_parameters; + + for (auto& rel_param : relation_parameters.get_to_fold()) { + rel_param = FF::random_element(); + } + auto prover_transcript = std::make_shared(); + + SumcheckProver sumcheck_prover(proving_key->dyadic_size(), + proving_key->polynomials, + prover_transcript, + subrelation_separators, + gate_challenges, + relation_parameters, + virtual_log_n); + auto proof = sumcheck_prover.prove(); + + auto verifier_transcript = std::make_shared(); + verifier_transcript->load_proof(prover_transcript->export_proof()); + + SumcheckVerifier verifier(verifier_transcript, subrelation_separators, virtual_log_n); + auto result = verifier.verify(relation_parameters, gate_challenges, std::vector(virtual_log_n, 1)); + EXPECT_EQ(result.verified, expected_result); + }; +}; + +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 proving_key = std::make_shared>(builder); + { + // Disable Poseidon2 External selector in the first active row + modify_selector(proving_key->polynomials.q_poseidon2_external); + + // Run sumcheck on the invalidated data + prove_and_verify(proving_key, false); + } + { + // Disable Poseidon2 Internal selector in the first active row + modify_selector(proving_key->polynomials.q_poseidon2_internal); + + // Run sumcheck on the invalidated data + prove_and_verify(proving_key, false); + } +} + +TEST_F(Poseidon2FailureTests, WrongWitnessValues) +{ + Builder builder; + + hash_single_input(builder); + + auto proving_key = std::make_shared>(builder); + { + modify_witness(proving_key->polynomials.q_poseidon2_external, proving_key->polynomials.w_l); + prove_and_verify(proving_key, false); + } + { + modify_witness(proving_key->polynomials.q_poseidon2_internal, proving_key->polynomials.w_r); + prove_and_verify(proving_key, false); + } +} + +TEST_F(Poseidon2FailureTests, TamperingWithShifts) +{ + Builder builder; + + hash_single_input(builder); + + auto proving_key = std::make_shared>(builder); + { + bool external_round = true; + tamper_with_shifts(proving_key->polynomials.q_poseidon2_external, proving_key->polynomials.w_l, external_round); + prove_and_verify(proving_key, false); + } + + { + bool external_round = false; + tamper_with_shifts(proving_key->polynomials.q_poseidon2_internal, proving_key->polynomials.w_l, external_round); + prove_and_verify(proving_key, false); + } +} diff --git a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.cpp b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.cpp index 25d3b6ec4eb3..44a02087471e 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.cpp @@ -5,22 +5,18 @@ // ===================== #include "barretenberg/stdlib/hash/poseidon2/poseidon2.hpp" -#include "barretenberg/ecc/curves/grumpkin/grumpkin.hpp" namespace bb::stdlib { -using namespace bb; - /** * @brief Hash a vector of field_ct. */ -template field_t poseidon2::hash(C& builder, const std::vector& inputs) +template field_t poseidon2::hash(const std::vector& inputs) { /* Run the sponge by absorbing all the input and squeezing one output. - * This should just call the sponge variable length hash function * */ - return Sponge::hash_internal(builder, inputs); + return Sponge::hash_internal(inputs); } template class poseidon2; diff --git a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.hpp b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.hpp index 8998843311a5..1b84031a4dec 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.hpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.hpp @@ -7,11 +7,8 @@ #pragma once #include "barretenberg/crypto/poseidon2/poseidon2_params.hpp" #include "barretenberg/stdlib/hash/poseidon2/sponge/sponge.hpp" -#include "barretenberg/stdlib/primitives/byte_array/byte_array.hpp" #include "barretenberg/stdlib/primitives/field/field.hpp" -#include "../../primitives/circuit_builders/circuit_builders.hpp" - namespace bb::stdlib { /** @@ -24,15 +21,12 @@ template class poseidon2 { private: using field_ct = stdlib::field_t; - using bool_ct = stdlib::bool_t; using Params = crypto::Poseidon2Bn254ScalarFieldParams; - using Permutation = Poseidon2Permutation; - // We choose our rate to be t-1 and capacity to be 1. - using Sponge = FieldSponge; + using Permutation = Poseidon2Permutation; + using Sponge = FieldSponge; public: - static field_ct hash(Builder& builder, const std::vector& in); - static field_ct hash_buffer(Builder& builder, const stdlib::byte_array& input); + static field_ct hash(const std::vector& in); }; } // namespace bb::stdlib 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 e595eff07393..8538e9285d0b 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.test.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2.test.cpp @@ -3,7 +3,7 @@ #include "barretenberg/common/test.hpp" #include "barretenberg/crypto/poseidon2/poseidon2.hpp" #include "barretenberg/numeric/random/engine.hpp" -#include "barretenberg/stdlib/primitives/curves/bn254.hpp" +#include "barretenberg/ultra_honk/decider_proving_key.hpp" using namespace bb; namespace { @@ -14,13 +14,34 @@ template class StdlibPoseidon2 : public testing::Test { using _curve = stdlib::bn254; using byte_array_ct = typename _curve::byte_array_ct; - using fr_ct = typename _curve::ScalarField; + using field_ct = typename _curve::ScalarField; using witness_ct = typename _curve::witness_ct; using public_witness_ct = typename _curve::public_witness_ct; using poseidon2 = typename stdlib::poseidon2; using native_poseidon2 = crypto::Poseidon2; public: + static field_ct w_hex(Builder& builder, const char* hex) { return witness_ct(&builder, uint256_t(hex)); } + static field_ct w_u64(Builder& builder, uint64_t v) { return witness_ct(&builder, uint256_t(v)); } + + static std::size_t gate_count(std::size_t N) + { + if (N == 1) { + return 73; + } + const size_t P_cost = 73; + const size_t D_full_adds = 3; + + // Number of Poseidon2 permutation invocations + size_t P_N = (N + 2) / 3; + // Number of extra additions in sqeeze + size_t N_3 = N % 3; + if (N_3 == 0) { + return (1 + P_N * P_cost + (P_N - 1) * D_full_adds); + } else { + return (1 + P_N * P_cost + (P_N - 2) * D_full_adds + N_3); + } + } /** * @brief Call poseidon2 on a vector of inputs * @@ -28,8 +49,6 @@ template class StdlibPoseidon2 : public testing::Test { */ static void test_hash(size_t num_inputs) { - using field_ct = stdlib::field_t; - using witness_ct = stdlib::witness_t; auto builder = Builder(); std::vector inputs; @@ -40,10 +59,13 @@ template class StdlibPoseidon2 : public testing::Test { inputs_native.emplace_back(element); inputs.emplace_back(field_ct(witness_ct(&builder, element))); } + size_t num_gates_start = builder.get_estimated_num_finalized_gates(); - auto result = stdlib::poseidon2::hash(builder, inputs); + auto result = stdlib::poseidon2::hash(inputs); auto expected = crypto::Poseidon2::hash(inputs_native); + EXPECT_EQ(gate_count(num_inputs), builder.get_estimated_num_finalized_gates() - num_gates_start); + EXPECT_EQ(result.get_value(), expected); bool proof_result = CircuitChecker::check(builder); @@ -62,12 +84,12 @@ template class StdlibPoseidon2 : public testing::Test { fr left_in = fr::random_element(); fr right_in = fr::random_element(); - fr_ct left = witness_ct(&builder, left_in); - fr_ct right = witness_ct(&builder, right_in); + field_ct left = witness_ct(&builder, left_in); + field_ct right = witness_ct(&builder, right_in); // num_inputs - 1 iterations since the first hash hashes two elements for (size_t i = 0; i < num_inputs - 1; ++i) { - left = poseidon2::hash(builder, { left, right }); + left = poseidon2::hash({ left, right }); } builder.set_public_input(left.witness_index); @@ -92,7 +114,7 @@ template class StdlibPoseidon2 : public testing::Test { } fr expected = native_poseidon2::hash(inputs); - auto result = poseidon2::hash(builder, witness_inputs); + auto result = poseidon2::hash(witness_inputs); EXPECT_EQ(result.get_value(), expected); } @@ -102,20 +124,208 @@ template class StdlibPoseidon2 : public testing::Test { Builder builder; std::vector inputs; - std::vector> witness_inputs; + std::vector witness_inputs; for (size_t i = 0; i < 8; ++i) { inputs.push_back(bb::fr::random_element()); if (i % 2 == 1) { witness_inputs.push_back(witness_ct(&builder, inputs[i])); } else { - witness_inputs.push_back(fr_ct(&builder, inputs[i])); + witness_inputs.push_back(field_ct(&builder, inputs[i])); } } native_poseidon2::hash(inputs); - EXPECT_THROW_OR_ABORT(poseidon2::hash(builder, witness_inputs), - ".*Sponge inputs should not be stdlib constants.*"); + EXPECT_THROW_OR_ABORT(poseidon2::hash(witness_inputs), ".*Sponge inputs should not be stdlib constants.*"); + } + + static void test_padding_collisions() + { + Builder builder; + + const field_ct random_input(witness_ct(&builder, fr::random_element())); + const field_ct zero(witness_ct(&builder, 0)); + + std::vector witness_inputs_len_1{ random_input }; + std::vector witness_inputs_len_2{ random_input, zero }; + std::vector witness_inputs_len_3{ random_input, zero, zero }; + std::vector> inputs{ witness_inputs_len_1, witness_inputs_len_2, witness_inputs_len_3 }; + + std::vector hashes(3); + + for (size_t idx = 0; idx < 3; idx++) { + hashes[idx] = poseidon2::hash(inputs[idx]).get_value(); + } + + // The domain separation IV depends on the input size, therefore, the hashes must not coincide. + EXPECT_NE(hashes[1], hashes[2]); + EXPECT_NE(hashes[2], hashes[3]); + EXPECT_NE(hashes[1], hashes[3]); + } + + // Test vectors and the expected values are taken from https://github.com/zemse/poseidon2-evm + static void test_against_independent_values() + { + struct TV { + // inputs given as hex strings + std::vector in_hex; + // expected hash (hex) + const char* expected_hex; + }; + + std::vector vectors = { + // Hash single element + { { "0x0000000000000000000000000000000000000000000000000000000000000000" }, + "0x2710144414c3a5f2354f4c08d52ed655b9fe253b4bf12cb9ad3de693d9b1db11" }, + + { { "0x19f3d19e5dd8b29a42cea4f71ebc6b12a42a5edbafbcb89cf0cddf0995d44e7f" }, + "0x2e1874598412a3b19f824ff246a1949a38b365bcdd58807eedb9206288820232" }, + + { { "0x0049c16ff91dacf0cb573751342f7bfa0042819e3f15fce57d61339f6340b0c1" }, + "0x011b5e5c76aedd3a361d2c72c711425f5709988da3c226bb8536691d81b190ac" }, + + { { "0x02713077725e5498d596be781be4c9a7353dbfe70ff10dc17702e66d0b5d388c" }, + "0x2ee1f0fc41b250f0e26dbaa1e9dc0d8e27e9354baf3c11eca131994c119f5651" }, + + { { "0x2a84a08a631f4c391b02f4519720881a80c25eb6ba3b59b2ca8f3c0e22ebeebc" }, + "0x0cf97e83eb7aa42f60e85095836d3b0afe3e88cc5046e1bae7318a64a0d32fd5" }, + + // Length 2 inputs + { { "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000" }, + "0x0b63a53787021a4a962a452c2921b3663aff1ffd8d5510540f8e659e782956f1" }, + { { "0x1762d324c2db6a912e607fd09664aaa02dfe45b90711c0dae9627d62a4207788", + "0x1047bd52da536f6bdd26dfe642d25d9092c458e64a78211298648e81414cbf35" }, + "0x303cacb84a267e5f3f46914fd3262dcaa212930c27a2f9de22c080dd9857be35" }, + { { "0x0a529bb6bbbf25ed33a47a4637dc70eb469a29893047482866748ae7f3a5afe1", + "0x1f5189751e18cf788deb219bdb95461e86ca08a882e4af5f8a6ec478e1ec73b4" }, + "0x1f71000626ba87581561f94a17f7b9962be8f1aa8d0c69c6f95348c9ddffe542" }, + { { "0x14ba77172ab2278bdf5a087ca0bd400e936bafe6dfc092c4e7a1b0950f1b6dbe", + "0x195c41f12d4fbac5e194c201536f3094541e73bf27d9f2413f09e731b3838733" }, + "0x06b53c119381e6ccee8e3ac9845970ba96befbce39606fad3686a6e31ac7761e" }, + + { { "0x2079041f0d6becd26db3ec659c54f60464243d86c3982978f1217a5f1413ed3a", + "0x08146641a4e30689442ecd270a7efef725bdb3036bf3d837dff683161a455de1" }, + "0x07512888968a4cfc7e5da7537c7f4448bf04d3679e711c3f16ca86351b0d1e6b" }, + + // Length 3 inputs + { { "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000" }, + "0x2a5de47ed300af27b706aaa14762fc468f5cfc16cd8116eb6b09b0f2643ca2b9" }, + + { { "0x300ced31bf248a1a2d4ea02b5e9f302a9e34df3c2109d5f1046ee9f59de6f6f1", + "0x2e6eb409ed7f41949cdb1925ac3ec68132b2443d873589a8afde4c027c3c0b68", + "0x2f08443953fc54fb351e41a46da99bbec1d290dae2907d2baf5174ed28eee9ea" }, + "0x27e4cf07e4bf24219f6a2da9be19cea601313a95f8a1360cf8f15d474826bf49" }, + + { { "0x21c6e3a949ec9c622e7e062efab3d6e1d5ee62763f99366627de157e26d179b7", + "0x1369519755b97bf50d10be283f853c5607ed1308f8235cd34c915783cbf7c70d", + "0x00c632d6fe8be14eddb11aee10b63e017e7d1f1a275d01e9d215f4135e950e7d" }, + "0x0d72d2b806a47af41117f7de2147908eb0496f99c0f58fbd6f5e778174132451" }, + + { { "0x26001a9f75eddc947cfc2e7a5524a7fb32d226404468168d58ff2f918e027015", + "0x0080918fd6c6f3867a46a0260e2cc2bc6b0b6bcb4528d33ba2bb57555a12929a", + "0x05217f0ada44bebd06d9fd34358fe7213446bdce940d943916acb59cabdc0001" }, + "0x2015f0a35636e589ae36e65f00b8f8c8a0cab01dcd080c23c8e2274283a57c85" }, + + { { "0x2a7efeb117f6894cfa21375d4496249c43eda35b5d5d267b7154413fdaad44ea", + "0x0bb48b46f770205618e2350c2f9c294524425763fb7ef6156c33c5bc2b8c1cbf", + "0x0206c176b0c66212fd12a580a8dd38689ae29dfa15933ba1982f5d30ed0c2ea1" }, + "0x1c93b16c25fa1e7b4933425b32876fa97c2013d44214792231634d3c96b6cc3f" }, + + // Length 10 inputs + { { "0x187b4757bbf034ce7daaf3fdf2b735544d88da2033dea124500ff79484bce862" }, + "0x0961b57effa18e2dcbf5671e9400d10bc9214fbf39b149cfd8949731f61d2bfa" }, + + { { "0x0143bc5d854ca7c33c27ad96dfe2c2443dd272a30a99f90f8f892ac7bbb4370e", + "0x2eb3d5587861dac05dfed4b7099dbdef57fc644504f297afb890c2df0c7212e7" }, + "0x0d34db78174c82c6a7d60cbf21f0fce80ef4ddc67e65881d6daca4e5ad8cd52d" }, + + { { "0x030341179f0ef88190aa557f9b20bb4aa31f805f214e72741b5d3b3a2bccf5b6", + "0x00b88879f5765438ef691e40ad4a6223d421317c342888de56c7f8b995e27688", + "0x0727b4522492cfbe1b5be97a17ed5672487fd1dc2ad3d09bd033c55d1ba40c70" }, + "0x22024dabdd6a9dfb47eb26f9569fd048968a0db30c60f3f38d8b61274458437e" }, + + { { "0x0891e9efa2b82224dccfee5171614168f84c4c99443c7e6e2753433a978f5955", + "0x01c81114d1f4eb857dfe3a8479760fd0c8e33d9ed6f42f8ee3eef974b85ef937", + "0x03a2238b91de1214a385af17ade25f2e71b6364b4d54dfb6e7ec96fd12be5a65", + "0x24cc93df58f07c156dd648edac3318420325db58ff1cccbc3d9a3cdb529f8469" }, + "0x24f3009e0089df4ae82f5dcde988fd9738ede4a6f51788c11c69b3e43a01b42b" }, + + { { "0x283318d1c946e5a788c59267712f20383a610189d89d39c161d53e2ae22b9bb5", + "0x2f51c6b6cc030846b73d449e6f8f9cbe609038bc4ccdb4268dc486e1a0d9694f", + "0x04c4fb583a2d9d9ceb1927de6d251e09fa01950313d6640863de039a37bd376a", + "0x18f1ec5070a8f50dbb71bf03d130fccc420161b8ee5e6c6ffdb676c7a7d33189", + "0x11e539f3dd6bb505dde162c84f22eee58f2a95a62027f230442b26c8dc3f96fc" }, + "0x1cbfcd7746c46fcfa7ae67d32e0cafb6ac348ebcb6f5a5e8c579ec6daa96362b" }, + + { { "0x01c93b681eb77d4d6af59d8f753d49d3a8839208f44471e68d67e52758cc923d", + "0x2a8f0abbca50b48935358452633b55e694d4d03357d7d5bdfddf08a117ada3b9", + "0x0c1a9c0b4e4a5315f111c078e411360bf54af39bfe2ffa2ce094aa6d57aa3343", + "0x12e80d94354e709fdc78c0d646a3763dd4dbeada2c7b27553f23cc6c32823e82", + "0x065929de60742283ec95df48428ca27e72bc8d4d114f172aff17c237b208d056", + "0x23ceb931dc1b76a8915466e0faedf56a5fe2169e650248663d9fecb75e5fa156" }, + "0x145023e2318ab81ba31e50cc62713441762c5132d5e6acbe6e88fd9f816473f1" }, + + { { "0x213bba8cab8dac55a2a86deaabcd4303ad2b0762fd1d11950d54bc54aa056623", + "0x00872f0dd93bd2868b85a7822d07d31d18ccf92228c38167de7804319a019fa3", + "0x198ea672f81fd916f47ae9a5f24d6740c5e5e5c8a389166ce02d85731e71d8af", + "0x0f362421b36759e1364d4ba7e5894381f56009843afe307aee6cb28a58ab4702", + "0x28a750154b9935407757f85a9f2596ddf082ce5f74256fd8c1200fc04a5f6548", + "0x0044d022f6220947659be7ed057a37adebc8468fce1bc365b76b7664595dd31d", + "0x22a2c8eff174ea66dee3d53dda9d45d37b90b3c2d6820f233fb868f4b41fc83c" }, + "0x16f71f10bf199529eadbbf20b0aefd1aae7afdd756c385da64d4e74474b9623c" }, + + { { "0x1d40c16d71a56182ae856c7f69412c62b29f1afd21376c9fe615c6bcd013723d", + "0x03281f89b9ac18f3a8199b7116453790560d43f2d1b70debb10379b6702b5e31", + "0x2391e7ffcf81c4fbe0bd44cd89ad173d6d1d9bf7c4325ca4b195f863b9fb9da9", + "0x0ecbc17f1a32f3163bc8bc704711aebedb0ad69d29f224fc8fb0851e7fc9c8c4", + "0x1ea5f8133edf27df211a66c7ba4304b9131cbde0b1832c2a182d9869a77c491b", + "0x224b14a1040873fd8f154e6d7b65db283ed2c422e5d14aa06ce3ed9d3cc69743", + "0x1bda12ff5af5e9a1b1f7dd8febb87e253ca0a4e43b16cd3b79818007f6f8d1bb", + "0x24a873345d569136d18164069fc60749aa57f8930ec8a52adde1f01967afbb7c" }, + "0x1b63be96f9b6bdeb09f103968aabac69252137ec863177a93a516e8120d662c4" }, + + { { "0x04e673b5f868ff850c5db58bb9f32b60c564b09b57261fdcb45c32013379ec21", + "0x0ff51c3255f4bd470e3263e5295b757d091fc015b1348b0b30b8fcfa5cc9b818", + "0x08f7a1ec99bf817777942e1132ccd237980a01f1d3d9914bbaf4d8e74320a1ee", + "0x2f6deadbdaa28308134784b3615596492085de03f479f59e79044e5860c76b27", + "0x141a2f00b0cbc606fe887c806d46659a6dac8a0d15829e36f06e43eec13eb324", + "0x1a0eb99c6f3c44026e61a5e6e36c806a25db6cf674c5f0075210dfdd00122264", + "0x0be48f952b90e3dfad74a2b04ade94e7b02b677702bf32d94389ff1669fa9911", + "0x18de646adb3e2f5e2ac7cd21dbfbf9dbe91d97b9cfb5afc2e7735c6f292d4ffe", + "0x174bfedb2323aecff5c4952313b81d9b3fcda8ff71a4b762bd16bb9779afb731" }, + "0x2a33a41e1e3cca17e7b7a000ac5cfcb7f7783023c0563acb39ae5bfe2e0d3c8e" }, + + { { "0x041035d2043ca613be600dc3643dff43a343548bfeb85f19ab2ff31dfec42fd1", + "0x0233cd5d2fff9f11b3675f38a7fd7f04efbfe9a95f114824c2e9c98e97a52d3e", + "0x2251141b94a8f419455457672f188b942079916217c2e1eefd7eceab022e8ba1", + "0x08d2cfe2bd8e054aa3488c2d8a6f7628503da3f4d450e30d4fea46a8700d4a26", + "0x1eb978a79a501b8df8c6312f3474e1f41d439492fdfed4240cbfef6b0a7b47a2", + "0x05e385c9b9093003379e7111b3d83846694b15a3072355bc1a6df3eeff6e95f4", + "0x0d91626c7f7e0ff655a973452f3eae713893f57ce2e078978e00ef0ffab48f67", + "0x0d51fcc8cdc21676e2d0d44d2caebcbedbd0c7f3149e7a2cb24f0f923c718f50", + "0x23cf8f1eda161dc7114e4774216a96a51430a4ed00bb94a9c22ffaf8158d9331", + "0x2060c8e16eaa344a1eb20bbc4179ad36c6c3d503716f329ce268677ecb76172f" }, + "0x1eab26c4915afff7148c904edac0220dc6b86dca67ee342db5705027c4e489f1" }, + }; + + for (const auto& tv : vectors) { + Builder builder; + // build inputs as witnesses + std::vector ins; + ins.reserve(tv.in_hex.size()); + for (auto* h : tv.in_hex) { + ins.emplace_back(w_hex(builder, h)); + } + + // compute poseidon2 hash (variable-length API) + auto got = poseidon2::hash(ins).get_value(); + fr expected(uint256_t(tv.expected_hex)); + + EXPECT_EQ(got, expected); + } } }; @@ -130,7 +340,14 @@ TYPED_TEST(StdlibPoseidon2, TestHashZeros) TYPED_TEST(StdlibPoseidon2, TestHashSmall) { + TestFixture::test_hash(1); + TestFixture::test_hash(6); TestFixture::test_hash(10); + TestFixture::test_hash(16); + TestFixture::test_hash(17); + TestFixture::test_hash(18); + TestFixture::test_hash(23); + TestFixture::test_hash(24); } TYPED_TEST(StdlibPoseidon2, TestHashLarge) @@ -147,3 +364,12 @@ TYPED_TEST(StdlibPoseidon2, TestHashConstants) { TestFixture::test_hash_constants(); }; +TYPED_TEST(StdlibPoseidon2, TestHashPadding) +{ + TestFixture::test_padding_collisions(); +}; +TYPED_TEST(StdlibPoseidon2, Consistency) +{ + + TestFixture::test_against_independent_values(); +} 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 671b3b096948..f1a1a45152c4 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2_permutation.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2_permutation.cpp @@ -7,41 +7,30 @@ #include "poseidon2_permutation.hpp" #include "barretenberg/honk/execution_trace/gate_data.hpp" -#include "barretenberg/stdlib/primitives/circuit_builders/circuit_builders.hpp" -#include "barretenberg/stdlib_circuit_builders/mega_circuit_builder.hpp" namespace bb::stdlib { -/** - * @brief Circuit form of Poseidon2 permutation from https://eprint.iacr.org/2023/323 for MegaCircuitBuilder. - * @details The permutation consists of one initial linear layer, then a set of external rounds, a set of internal - * rounds, and a set of external rounds. - * @param builder - * @param input - * @return State - */ -template -typename Poseidon2Permutation::State Poseidon2Permutation::permutation( - Builder* builder, const typename Poseidon2Permutation::State& input) +template +typename Poseidon2Permutation::State Poseidon2Permutation::permutation( + Builder* builder, const typename Poseidon2Permutation::State& input) { - // deep copy State current_state(input); NativeState current_native_state; for (size_t i = 0; i < t; ++i) { current_native_state[i] = current_state[i].get_value(); } - // Apply 1st linear layer + // Apply 1st linear layer both natively and in-circuit. NativePermutation::matrix_multiplication_external(current_native_state); - matrix_multiplication_external(builder, current_state); + matrix_multiplication_external(current_state); // First set of external rounds constexpr size_t rounds_f_beginning = rounds_f / 2; for (size_t i = 0; i < rounds_f_beginning; ++i) { - poseidon2_external_gate_ in{ current_state[0].witness_index, - current_state[1].witness_index, - current_state[2].witness_index, - current_state[3].witness_index, + poseidon2_external_gate_ in{ current_state[0].get_witness_index(), + current_state[1].get_witness_index(), + current_state[2].get_witness_index(), + current_state[3].get_witness_index(), i }; builder->create_poseidon2_external_gate(in); // calculate the new witnesses @@ -53,21 +42,15 @@ typename Poseidon2Permutation::State Poseidon2Permutationcreate_dummy_gate(builder->blocks.poseidon2_external, - current_state[0].witness_index, - current_state[1].witness_index, - current_state[2].witness_index, - current_state[3].witness_index); + propagate_current_state_to_next_row(builder, current_state, builder->blocks.poseidon2_external); // Internal rounds const size_t p_end = rounds_f_beginning + rounds_p; for (size_t i = rounds_f_beginning; i < p_end; ++i) { - poseidon2_internal_gate_ in{ current_state[0].witness_index, - current_state[1].witness_index, - current_state[2].witness_index, - current_state[3].witness_index, + poseidon2_internal_gate_ in{ current_state[0].get_witness_index(), + current_state[1].get_witness_index(), + current_state[2].get_witness_index(), + current_state[3].get_witness_index(), i }; builder->create_poseidon2_internal_gate(in); current_native_state[0] += round_constants[i][0]; @@ -78,20 +61,14 @@ typename Poseidon2Permutation::State Poseidon2Permutationcreate_dummy_gate(builder->blocks.poseidon2_internal, - current_state[0].witness_index, - current_state[1].witness_index, - current_state[2].witness_index, - current_state[3].witness_index); + propagate_current_state_to_next_row(builder, current_state, builder->blocks.poseidon2_internal); // Remaining external rounds for (size_t i = p_end; i < NUM_ROUNDS; ++i) { - poseidon2_external_gate_ in{ current_state[0].witness_index, - current_state[1].witness_index, - current_state[2].witness_index, - current_state[3].witness_index, + poseidon2_external_gate_ in{ current_state[0].get_witness_index(), + current_state[1].get_witness_index(), + current_state[2].get_witness_index(), + current_state[3].get_witness_index(), i }; builder->create_poseidon2_external_gate(in); // calculate the new witnesses @@ -102,131 +79,47 @@ typename Poseidon2Permutation::State Poseidon2Permutation(builder, current_native_state[j]); } } - // The Poseidon2 permutation is 64 rounds, but needs to be a block of 65 rows, since the result of - // applying a round of Poseidon2 is stored in the next row (the shifted row). As a result, we need this end row to - // compare with the result from the 64th round of Poseidon2. Note that it does not activate any selectors since it - // only serves as a comparison through the shifted wires. - builder->create_dummy_gate(builder->blocks.poseidon2_external, - current_state[0].witness_index, - current_state[1].witness_index, - current_state[2].witness_index, - current_state[3].witness_index); + + propagate_current_state_to_next_row(builder, current_state, builder->blocks.poseidon2_external); + return current_state; } /** * @brief Separate function to do just the first linear layer (equivalent to external matrix mul). - * @details We use 6 arithmetic gates to implement: - * gate 1: Compute tmp1 = state[0] + state[1] + 2 * state[3] - * gate 2: Compute tmp2 = 2 * state[1] + state[2] + state[3] - * gate 3: Compute v2 = 4 * state[0] + 4 * state[1] + tmp2 - * gate 4: Compute v1 = v2 + tmp1 - * gate 5: Compute v4 = tmp1 + 4 * state[2] + 4 * state[3] - * gate 6: Compute v3 = v4 + tmp2 - * output state is [v1, v2, v3, v4] - * @param builder - * @param state + * @details Update the state with \f$ M_E \cdot (\text{state}[0], \text{state}[1], \text{state}[2], + * \text{state}[3])^{\top}\f$. Where \f$ M_E \f$ is the external round matrix. See `Poseidon2ExternalRelationImpl`. */ -template -void Poseidon2Permutation::matrix_multiplication_external( - Builder* builder, typename Poseidon2Permutation::State& state) +template +void Poseidon2Permutation::matrix_multiplication_external(typename Poseidon2Permutation::State& state) { + const bb::fr two(2); + const bb::fr four(4); // create the 6 gates for the initial matrix multiplication // gate 1: Compute tmp1 = state[0] + state[1] + 2 * state[3] - field_t tmp1 = - witness_t(builder, state[0].get_value() + state[1].get_value() + FF(2) * state[3].get_value()); - builder->create_big_add_gate({ - .a = state[0].witness_index, - .b = state[1].witness_index, - .c = state[3].witness_index, - .d = tmp1.witness_index, - .a_scaling = 1, - .b_scaling = 1, - .c_scaling = 2, - .d_scaling = -1, - .const_scaling = 0, - }); + field_t tmp1 = state[0].add_two(state[1], state[3] * two); // gate 2: Compute tmp2 = 2 * state[1] + state[2] + state[3] - field_t tmp2 = - witness_t(builder, FF(2) * state[1].get_value() + state[2].get_value() + state[3].get_value()); - builder->create_big_add_gate({ - .a = state[1].witness_index, - .b = state[2].witness_index, - .c = state[3].witness_index, - .d = tmp2.witness_index, - .a_scaling = 2, - .b_scaling = 1, - .c_scaling = 1, - .d_scaling = -1, - .const_scaling = 0, - }); + field_t tmp2 = state[2].add_two(state[1] * two, state[3]); // gate 3: Compute v2 = 4 * state[0] + 4 * state[1] + tmp2 - field_t v2 = - witness_t(builder, FF(4) * state[0].get_value() + FF(4) * state[1].get_value() + tmp2.get_value()); - builder->create_big_add_gate({ - .a = state[0].witness_index, - .b = state[1].witness_index, - .c = tmp2.witness_index, - .d = v2.witness_index, - .a_scaling = 4, - .b_scaling = 4, - .c_scaling = 1, - .d_scaling = -1, - .const_scaling = 0, - }); + state[1] = tmp2.add_two(state[0] * four, state[1] * four); // gate 4: Compute v1 = v2 + tmp1 - field_t v1 = witness_t(builder, v2.get_value() + tmp1.get_value()); - builder->create_big_add_gate({ - .a = v2.witness_index, - .b = tmp1.witness_index, - .c = v1.witness_index, - .d = builder->zero_idx, - .a_scaling = 1, - .b_scaling = 1, - .c_scaling = -1, - .d_scaling = 0, - .const_scaling = 0, - }); + state[0] = state[1] + tmp1; // gate 5: Compute v4 = tmp1 + 4 * state[2] + 4 * state[3] - field_t v4 = - witness_t(builder, tmp1.get_value() + FF(4) * state[2].get_value() + FF(4) * state[3].get_value()); - builder->create_big_add_gate({ - .a = tmp1.witness_index, - .b = state[2].witness_index, - .c = state[3].witness_index, - .d = v4.witness_index, - .a_scaling = 1, - .b_scaling = 4, - .c_scaling = 4, - .d_scaling = -1, - .const_scaling = 0, - }); + state[3] = tmp1.add_two(state[2] * four, state[3] * four); // gate 6: Compute v3 = v4 + tmp2 - field_t v3 = witness_t(builder, v4.get_value() + tmp2.get_value()); - builder->create_big_add_gate({ - .a = v4.witness_index, - .b = tmp2.witness_index, - .c = v3.witness_index, - .d = builder->zero_idx, - .a_scaling = 1, - .b_scaling = 1, - .c_scaling = -1, - .d_scaling = 0, - .const_scaling = 0, - }); - - state[0] = v1; - state[1] = v2; - state[2] = v3; - state[3] = v4; + state[2] = state[3] + tmp2; + + // This can only happen if the input contained constant `field_t` elements. + ASSERT(state[0].is_normalized() && state[1].is_normalized() && state[2].is_normalized() && + state[3].is_normalized()); } -template class Poseidon2Permutation; -template class Poseidon2Permutation; +template class Poseidon2Permutation; +template class Poseidon2Permutation; } // namespace bb::stdlib diff --git a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2_permutation.hpp b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2_permutation.hpp index 25122eeae134..e5805a9bcca4 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2_permutation.hpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/poseidon2_permutation.hpp @@ -10,24 +10,32 @@ #include #include "barretenberg/crypto/poseidon2/poseidon2_permutation.hpp" -#include "barretenberg/stdlib/primitives/circuit_builders/circuit_builders.hpp" #include "barretenberg/stdlib/primitives/field/field.hpp" namespace bb::stdlib { -template class Poseidon2Permutation { +/** + * @brief Circuit form of Poseidon2 permutation from https://eprint.iacr.org/2023/323. + * @details The permutation consists of one initial linear layer, then a set of external rounds, a set of internal + * rounds, and a set of external rounds. + * + * Note that except for the inital linear layer, we compute the round results natively and record them into Poseidon2 + * custom gates. This allows us to heavily reduce the number of arithmetic gates, that would have been otherwise + * required to perform expensive non-linear S-box operations in-circuit. + * + * The external rounds are constrained via `Poseidon2ExternalRelationImpl`. + * The internal rounds are constrained via `Poseidon2InternalRelationImpl`. + * + */ +template class Poseidon2Permutation { public: + using Params = crypto::Poseidon2Bn254ScalarFieldParams; using NativePermutation = crypto::Poseidon2Permutation; // t = sponge permutation size (in field elements) // t = rate + capacity - // capacity = 1 field element (256 bits) + // capacity = 1 field element // rate = number of field elements that can be compressed per permutation static constexpr size_t t = Params::t; - // d = degree of s-box polynomials. For a given field, `d` is the smallest element of `p` such that gdc(d, p - 1) = - // 1 (excluding 1) For bn254/grumpkin, d = 5 - static constexpr size_t d = Params::d; - // sbox size = number of bits in p - static constexpr size_t sbox_size = Params::sbox_size; // number of full sbox rounds static constexpr size_t rounds_f = Params::rounds_f; // number of partial sbox rounds @@ -53,19 +61,28 @@ template class Poseidon2Permutation { static State permutation(Builder* builder, const State& input); /** - * @brief Separate function to do just the first linear layer (equivalent to external matrix mul). - * @details We use 6 arithmetic gates to implement: - * gate 1: Compute tmp1 = state[0] + state[1] + 2 * state[3] - * gate 2: Compute tmp2 = 2 * state[1] + state[2] + state[3] - * gate 3: Compute v2 = 4 * state[0] + 4 * state[1] + tmp2 - * gate 4: Compute v1 = v2 + tmp1 - * gate 5: Compute v4 = tmp1 + 4 * state[2] + 4 * state[3] - * gate 6: Compute v3 = v4 + tmp2 - * output state is [v1, v2, v3, v4] + * @brief In-circuit method to efficiently multiply the inital state by the external matrix \f$ M_E \f$. Uses 6 + * aritmetic gates. + */ + static void matrix_multiplication_external(State& state); + + /** + * @brief The result of applying a round of Poseidon2 is stored in the next row and is accessed by Poseidon2 + * Internal and External Relations via the shifts mechanism. Note that it does not activate any selectors since it + * only serves to store the values. See `Poseidon2ExternalRelationImpl` and `Poseidon2InternalRelationImpl` docs. + * * @param builder - * @param state + * @param state an array of `t` field_t elements + * @param block Either `poseidon2_external` or `poseidon2_internal` block of the Execution Trace */ - static void matrix_multiplication_external(Builder* builder, State& state); + static void propagate_current_state_to_next_row(Builder* builder, const State& state, auto& block) + { + builder->create_dummy_gate(block, + state[0].get_witness_index(), + state[1].get_witness_index(), + state[2].get_witness_index(), + state[3].get_witness_index()); + }; }; } // namespace bb::stdlib 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 7200ca09d39b..3c24b29d6e94 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/sponge/sponge.hpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/hash/poseidon2/sponge/sponge.hpp @@ -13,171 +13,111 @@ #include "barretenberg/numeric/uint256/uint256.hpp" #include "barretenberg/stdlib/hash/poseidon2/poseidon2_permutation.hpp" -#include "barretenberg/stdlib/primitives/field/field.hpp" namespace bb::stdlib { /** * @brief Implements the circuit form of a cryptographic sponge over prime fields. - * Implements the sponge specification from the Community Cryptographic Specification Project - * see https://github.com/C2SP/C2SP/blob/792c1254124f625d459bfe34417e8f6bdd02eb28/poseidon-sponge.md - * (Note: this spec was not accepted into the C2SP repo, we might want to reference something else!) * - * Note: If we ever use this sponge class for more than 1 hash functions, we should move this out of `poseidon2` - * and into its own directory - * @tparam field_t - * @tparam rate - * @tparam capacity - * @tparam t - * @tparam Permutation + * @tparam Builder A circuit builder class. Can be Ultra- or MegaCircuitBuilder. */ -template class FieldSponge { - public: - /** - * @brief Defines what phase of the sponge algorithm we are in. - * - * ABSORB: 'absorbing' field elements into the sponge - * SQUEEZE: compressing the sponge and extracting a field element - * - */ - enum Mode { - ABSORB, - SQUEEZE, - }; +template class FieldSponge { + private: + using Permutation = Poseidon2Permutation; + static constexpr size_t t = crypto::Poseidon2Bn254ScalarFieldParams::t; // = 4 + static constexpr size_t capacity = 1; + static constexpr size_t rate = t - capacity; // = 3 + using field_t = stdlib::field_t; // sponge state. t = rate + capacity. capacity = 1 field element (~256 bits) - std::array state; + std::array state{}; // cached elements that have been absorbed. - std::array cache; + std::array cache{}; size_t cache_size = 0; - Mode mode = Mode::ABSORB; Builder* builder; - FieldSponge(Builder& builder_, field_t domain_iv = 0) - : builder(&builder_) + FieldSponge(Builder* builder_, size_t in_len) + : builder(builder_) { - for (size_t i = 0; i < rate; ++i) { - state[i] = witness_t::create_constant_witness(builder, 0); - } - state[rate] = witness_t::create_constant_witness(builder, domain_iv.get_value()); + // Add the domain separation to the initial state. + field_t iv(static_cast(in_len) << 64); + iv.convert_constant_to_fixed_witness(builder); + state[rate] = iv; } - std::array perform_duplex() + void perform_duplex() { - // zero-pad the cache - for (size_t i = cache_size; i < rate; ++i) { - cache[i] = witness_t::create_constant_witness(builder, 0); - } - // add the cache into sponge state + // Add the cache into sponge state for (size_t i = 0; i < rate; ++i) { state[i] += cache[i]; } + + // Apply Poseidon2 permutation state = Permutation::permutation(builder, state); - // return `rate` number of field elements from the sponge state. - std::array output; - for (size_t i = 0; i < rate; ++i) { - output[i] = state[i]; - } - // variables with indices from rate to size of state - 1 won't be used anymore - // after permutation. But they aren't dangerous and needed to put in used witnesses - if constexpr (IsUltraBuilder) { - for (size_t i = rate; i < t; i++) { - builder->update_used_witnesses(state[i].witness_index); - } - } - return output; + + // Reset the cache + cache = {}; } void absorb(const field_t& input) { - if (mode == Mode::ABSORB && cache_size == rate) { + if (cache_size == rate) { // If we're absorbing, and the cache is full, apply the sponge permutation to compress the cache perform_duplex(); cache[0] = input; cache_size = 1; - } else if (mode == Mode::ABSORB && cache_size < rate) { + } else { // If we're absorbing, and the cache is not full, add the input into the cache cache[cache_size] = input; cache_size += 1; - } else if (mode == Mode::SQUEEZE) { - // If we're in squeeze mode, switch to absorb mode and add the input into the cache. - // N.B. I don't think this code path can be reached?! - cache[0] = input; - cache_size = 1; - mode = Mode::ABSORB; } } field_t squeeze() { - if (mode == Mode::SQUEEZE && cache_size == 0) { - // If we're in squeze mode and the cache is empty, there is nothing left to squeeze out of the sponge! - // Switch to absorb mode. - mode = Mode::ABSORB; - cache_size = 0; - } - if (mode == Mode::ABSORB) { - // If we're in absorb mode, apply sponge permutation to compress the cache, populate cache with compressed - // state and switch to squeeze mode. Note: this code block will execute if the previous `if` condition was - // matched - auto new_output_elements = perform_duplex(); - mode = Mode::SQUEEZE; - for (size_t i = 0; i < rate; ++i) { - cache[i] = new_output_elements[i]; - } - cache_size = rate; - } - // By this point, we should have a non-empty cache. Pop one item off the top of the cache and return it. - field_t result = cache[0]; - for (size_t i = 1; i < cache_size; ++i) { - cache[i - 1] = cache[i]; - } - cache_size -= 1; - cache[cache_size] = witness_t::create_constant_witness(builder, 0); - return result; + + perform_duplex(); + + return state[0]; } + public: /** - * @brief Use the sponge to hash an input string + * @brief Use the sponge to hash an input vector. * - * @tparam out_len - * @tparam is_variable_length. Distinguishes between hashes where the preimage length is constant/not constant - * @param input - * @return std::array + * @param input Circuit witnesses (a_0, ..., a_{N-1}) + * @return Hash of the input, a single witness field element. */ - template - static std::array hash_internal(Builder& builder, std::span input) + static field_t hash_internal(std::span input) { - size_t in_len = input.size(); - const uint256_t iv = (static_cast(in_len) << 64) + out_len - 1; - FieldSponge sponge(builder, iv); + // Ensure that all inputs belong to the same circuit and extract a pointer to the circuit object. + Builder* builder = validate_context(input); + // Ensure that the pointer is not a `nullptr` + ASSERT(builder); + + // Initialize the sponge state. Input length is used for domain separation. + const size_t in_len = input.size(); + FieldSponge sponge(builder, in_len); + + // Absorb inputs in blocks of size r = 3. Make sure that all inputs are witneesses. for (size_t i = 0; i < in_len; ++i) { - BB_ASSERT_EQ(input[i].witness_index == IS_CONSTANT, false, "Sponge inputs should not be stdlib constants."); + BB_ASSERT_EQ(input[i].is_constant(), false, "Sponge inputs should not be stdlib constants."); sponge.absorb(input[i]); } - std::array output; - for (size_t i = 0; i < out_len; ++i) { - output[i] = sponge.squeeze(); - } - // variables with indices won't be used in the circuit. - // but they aren't dangerous and needed to put in used witnesses - if constexpr (IsUltraBuilder) { - for (const auto& elem : sponge.cache) { - if (elem.witness_index != IS_CONSTANT) { - builder.update_used_witnesses(elem.witness_index); - } - } + // Perform final duplex call. At this point, cache contains `m = in_len % 3` input elements and 3 - m constant + // zeroes served as padding. + 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) { + builder->update_used_witnesses(elem.witness_index); } return output; } - - static field_t hash_internal(Builder& builder, std::span input) - { - return hash_internal<1>(builder, input)[0]; - } }; -} // namespace bb::stdlib \ No newline at end of file +} // namespace bb::stdlib diff --git a/barretenberg/cpp/src/barretenberg/stdlib/honk_verifier/ultra_recursive_verifier.test.cpp b/barretenberg/cpp/src/barretenberg/stdlib/honk_verifier/ultra_recursive_verifier.test.cpp index 069a74055cbd..bc839029be3f 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/honk_verifier/ultra_recursive_verifier.test.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/honk_verifier/ultra_recursive_verifier.test.cpp @@ -291,7 +291,7 @@ template class RecursiveVerifierTest : public testing } // Check the size of the recursive verifier if constexpr (std::same_as>) { - uint32_t NUM_GATES_EXPECTED = 797170; + uint32_t NUM_GATES_EXPECTED = 796978; ASSERT_EQ(static_cast(outer_circuit.get_num_finalized_gates()), NUM_GATES_EXPECTED) << "MegaZKHonk Recursive verifier changed in Ultra gate count! Update this value if you " "are sure this is expected."; diff --git a/barretenberg/cpp/src/barretenberg/stdlib/transcript/transcript.hpp b/barretenberg/cpp/src/barretenberg/stdlib/transcript/transcript.hpp index dc5562072a18..2326ceab03e4 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/transcript/transcript.hpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/transcript/transcript.hpp @@ -22,10 +22,8 @@ template struct StdlibTranscriptParams { { ASSERT(!data.empty()); - ASSERT(data[0].get_context() != nullptr); - Builder* builder = data[0].get_context(); - return stdlib::poseidon2::hash(*builder, data); + return stdlib::poseidon2::hash(data); } /** * @brief Split a challenge field element into two half-width challenges diff --git a/barretenberg/cpp/src/barretenberg/vm2/constraining/recursion/goblin_avm_recursive_verifier.hpp b/barretenberg/cpp/src/barretenberg/vm2/constraining/recursion/goblin_avm_recursive_verifier.hpp index e8bc2d34ff9c..1f4a37a489ea 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/constraining/recursion/goblin_avm_recursive_verifier.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2/constraining/recursion/goblin_avm_recursive_verifier.hpp @@ -159,7 +159,7 @@ class AvmGoblinRecursiveVerifier { // Validate the consistency of the AVM2 verifier inputs {\pi, pub_inputs, VK}_{AVM2} between the inner (Mega) // circuit and the outer (Ultra) by asserting equality on the independently computed hashes of this data. - const FF ultra_hash = stdlib::poseidon2::hash(ultra_builder, hash_buffer); + const FF ultra_hash = stdlib::poseidon2::hash(hash_buffer); mega_verifier_output.mega_hash.assert_equal(ultra_hash); // Return ipa proof, ipa claim and output aggregation object produced from verifying the Mega + Goblin proofs @@ -220,7 +220,7 @@ class AvmGoblinRecursiveVerifier { mega_vk_hash.fix_witness(); // fix witness because vk hash should be a circuit constant // Compute the hash and set it public - const FF mega_hash = stdlib::poseidon2::hash(mega_builder, mega_hash_buffer); + const FF mega_hash = stdlib::poseidon2::hash(mega_hash_buffer); // Construct a Mega-arithmetized AVM2 recursive verifier circuit auto stdlib_key = std::make_shared(mega_builder, std::span(key_fields));