diff --git a/cpp/src/aztec/ecc/curves/bn254/scalar_multiplication/scalar_multiplication.cpp b/cpp/src/aztec/ecc/curves/bn254/scalar_multiplication/scalar_multiplication.cpp index cc2b74ec19..6e9fb9dde6 100644 --- a/cpp/src/aztec/ecc/curves/bn254/scalar_multiplication/scalar_multiplication.cpp +++ b/cpp/src/aztec/ecc/curves/bn254/scalar_multiplication/scalar_multiplication.cpp @@ -933,5 +933,14 @@ g1::element pippenger_unsafe(fr* scalars, { return pippenger(scalars, points, num_initial_points, state, false); } +g1::element pippenger_without_endomorphism_basis_points(fr* scalars, + g1::affine_element* points, + const size_t num_initial_points, + pippenger_runtime_state& state) +{ + std::vector G_mod(num_initial_points * 2); + barretenberg::scalar_multiplication::generate_pippenger_point_table(points, &G_mod[0], num_initial_points); + return pippenger(scalars, &G_mod[0], num_initial_points, state, false); +} } // namespace scalar_multiplication } // namespace barretenberg \ No newline at end of file diff --git a/cpp/src/aztec/ecc/curves/bn254/scalar_multiplication/scalar_multiplication.hpp b/cpp/src/aztec/ecc/curves/bn254/scalar_multiplication/scalar_multiplication.hpp index c5ea7f472b..36613a47a9 100644 --- a/cpp/src/aztec/ecc/curves/bn254/scalar_multiplication/scalar_multiplication.hpp +++ b/cpp/src/aztec/ecc/curves/bn254/scalar_multiplication/scalar_multiplication.hpp @@ -146,6 +146,10 @@ g1::element pippenger_unsafe(fr* scalars, g1::affine_element* points, const size_t num_initial_points, pippenger_runtime_state& state); +g1::element pippenger_without_endomorphism_basis_points(fr* scalars, + g1::affine_element* points, + const size_t num_initial_points, + pippenger_runtime_state& state); } // namespace scalar_multiplication } // namespace barretenberg diff --git a/cpp/src/aztec/honk/commitment_scheme/ipa/ipa.hpp b/cpp/src/aztec/honk/commitment_scheme/ipa/ipa.hpp new file mode 100644 index 0000000000..991d264328 --- /dev/null +++ b/cpp/src/aztec/honk/commitment_scheme/ipa/ipa.hpp @@ -0,0 +1,275 @@ +#pragma once +#include +#include +#include +#include +#include "../commitment_scheme.hpp" +#include "stdlib/primitives/curves/bn254.hpp" + +// Suggested by Zac: Future optimisations +// 1: write a program that generates a large set of generator points (2^23?) and writes to a file on disk +// 2: create a SRS class for IPA similar to existing SRS class, that loads these points from disk +// and stores in struct *and* applies the pippenger point table endomorphism transforation +// 3: when constructing a InnerProductArgument class, pass std::shared_ptr as input param and store as member +// variable +// 4: (SRS class should contain a `pippenger_runtime_state` object so it does not need to be repeatedly +// generated) + +/** + * @brief IPA (inner-product argument) commitment scheme class. Conforms to the specification + * https://hackmd.io/q-A8y6aITWyWJrvsGGMWNA?view. + * + * @tparam Fr: The underlying field + * @tparam Fq: The field corresponding to the elliptic curve + * @tparam G1: The elliptic curve group + */ +template class InnerProductArgument { + public: + using element = typename G1::element; + using affine_element = typename G1::affine_element; + struct IpaProof { + std::vector L_vec; + std::vector R_vec; + Fr a_zero; + }; + // To contain the public inputs for IPA proof + // For now we are including the aux_generator and round_challenges in public input. They will be computed by the + // prover and the verifier by Fiat-Shamir when the challengeGenerator is defined. + struct IpaPubInput { + element commitment; + Fr challenge_point; + Fr evaluation; + size_t poly_degree; + element aux_generator; // To be removed + std::vector round_challenges; // To be removed + }; + + /** + * @brief Commit to a polynomial + * + * @param polynomial The input polynomial in the coefficient form + * @param poly_degree The degree of the polynomial + * @param G_vector The common set of generators required to compute the commitment, to be replaced by srs + * + * @return a group element + */ + static element commit(std::vector& polynomial, const size_t poly_degree, std::vector& G_vector) + { + auto pippenger_runtime_state = barretenberg::scalar_multiplication::pippenger_runtime_state(poly_degree); + auto commitment = barretenberg::scalar_multiplication::pippenger_without_endomorphism_basis_points( + &polynomial[0], &G_vector[0], poly_degree, pippenger_runtime_state); + return commitment; + } + + /** + * @brief Compute an IpaProof for opening a single polynomial at a single evaluation point + * + * @param ipa_pub_input Data required to compute the opening proof. See spec for more details + * @param polynomial The witness polynomial whose opening proof needs to be computed + * @param G_vector the common set of generators, to be replaced by the srs + * + * @return an IpaProof, containing information required to verify whether the commitment is computed correctly and + * the polynomial evaluation is correct in the given challenge point. + */ + static IpaProof ipa_prove(const IpaPubInput& ipa_pub_input, + const std::vector& polynomial, + const std::vector& G_vector) + { + IpaProof proof; + auto& challenge_point = ipa_pub_input.challenge_point; + ASSERT(challenge_point != 0 && "The challenge point should not be zero"); + const size_t poly_degree = ipa_pub_input.poly_degree; + // To check poly_degree is greater than zero and a power of two + // Todo: To accomodate non power of two poly_degree + ASSERT((poly_degree > 0) && (!(poly_degree & (poly_degree - 1))) && + "The poly_degree should be positive and a power of two"); + auto& aux_generator = ipa_pub_input.aux_generator; + auto a_vec = polynomial; + // Todo: to make it more efficient by directly using G_vector for the input points when i = 0 and write the + // output points to G_vec_local. Then use G_vec_local for rounds where i>0, this can be done after we use SRS + // instead of G_vector. + std::vector G_vec_local(poly_degree); + for (size_t i = 0; i < poly_degree; i++) { + G_vec_local[i] = G_vector[i]; + } + // Construct b vector + // Todo: For round i=0, b_vec can be derived in-place. + // This means that the size of b_vec can be 50% of the current size (i.e. we only write values to b_vec at the + // end of round 0) + std::vector b_vec(poly_degree); + Fr b_power = 1; + for (size_t i = 0; i < poly_degree; i++) { + b_vec[i] = b_power; + b_power *= challenge_point; + } + // Iterate for log_2(poly_degree) rounds to compute the round commitments. + const size_t log_poly_degree = static_cast(numeric::get_msb(poly_degree)); + std::vector L_elements(log_poly_degree); + std::vector R_elements(log_poly_degree); + size_t round_size = poly_degree; + + for (size_t i = 0; i < log_poly_degree; i++) { + round_size >>= 1; + // Compute inner_prod_L := < a_vec_lo, b_vec_hi > and inner_prod_R := < a_vec_hi, b_vec_lo > + Fr inner_prod_L = Fr::zero(); + Fr inner_prod_R = Fr::zero(); + for (size_t j = 0; j < round_size; j++) { + inner_prod_L += a_vec[j] * b_vec[round_size + j]; + inner_prod_R += a_vec[round_size + j] * b_vec[j]; + } + // L_i = < a_vec_lo, G_vec_hi > + inner_prod_L * aux_generator + // Todo: Remove usage of multiple runtime_state, pass it as an element of the SRS. + auto pippenger_runtime_state = barretenberg::scalar_multiplication::pippenger_runtime_state(round_size); + element partial_L = barretenberg::scalar_multiplication::pippenger_without_endomorphism_basis_points( + &a_vec[0], &G_vec_local[round_size], round_size, pippenger_runtime_state); + partial_L += aux_generator * inner_prod_L; + + // R_i = < a_vec_hi, G_vec_lo > + inner_prod_R * aux_generator + element partial_R = barretenberg::scalar_multiplication::pippenger_without_endomorphism_basis_points( + &a_vec[round_size], &G_vec_local[0], round_size, pippenger_runtime_state); + partial_R += aux_generator * inner_prod_R; + + L_elements[i] = affine_element(partial_L); + R_elements[i] = affine_element(partial_R); + + // Generate the round challenge. Todo: Use Fiat-Shamir + const Fr round_challenge = ipa_pub_input.round_challenges[i]; + const Fr round_challenge_inv = round_challenge.invert(); + + // Update the vectors a_vec, b_vec and G_vec. + // a_vec_next = a_vec_lo * round_challenge + a_vec_hi * round_challenge_inv + // b_vec_next = b_vec_lo * round_challenge_inv + b_vec_hi * round_challenge + // G_vec_next = G_vec_lo * round_challenge_inv + G_vec_hi * round_challenge + for (size_t j = 0; j < round_size; j++) { + a_vec[j] *= round_challenge; + a_vec[j] += round_challenge_inv * a_vec[round_size + j]; + b_vec[j] *= round_challenge_inv; + b_vec[j] += round_challenge * b_vec[round_size + j]; + + /* + Todo: (performance improvement suggested by Zac): We can improve performance here by using + element::batch_mul_with_endomorphism. This method takes a vector of input points points and a scalar x + and outputs a vector containing points[i]*x. It's 30% faster than a basic mul operation due to + performing group additions in 2D affine coordinates instead of 3D projective coordinates (affine point + additions are usually more expensive than projective additions due to the need to compute a modular + inverse. However we get around this by computing a single batch inverse. This only works if you are + adding a lot of independent point pairs so you can amortise the cost of the single batch inversion + across multiple points). + */ + + auto G_lo = element(G_vec_local[j]) * round_challenge_inv; + auto G_hi = element(G_vec_local[round_size + j]) * round_challenge; + auto temp = G_lo + G_hi; + G_vec_local[j] = temp.normalize(); + } + } + proof.L_vec = std::vector(log_poly_degree); + proof.R_vec = std::vector(log_poly_degree); + for (size_t i = 0; i < log_poly_degree; i++) { + proof.L_vec[i] = L_elements[i]; + proof.R_vec[i] = R_elements[i]; + } + proof.a_zero = a_vec[0]; + return proof; + } + + /** + * @brief Verify the correctness of an IpaProof + * + * @param ipa_proof The proof containg L_vec, R_vec and a_zero + * @param Ipa_pub_input Data required to verify the ipa_proof + * @param G_vector The common set of generators, to be replaced by the srs + * + * @return true/false depending on if the proof verifies + */ + static bool ipa_verify(const IpaProof& ipa_proof, + const IpaPubInput& ipa_pub_input, + const std::vector& G_vector) + { + // Local copies of public inputs + auto& a_zero = ipa_proof.a_zero; + auto& commitment = ipa_pub_input.commitment; + auto& challenge_point = ipa_pub_input.challenge_point; + auto& evaluation = ipa_pub_input.evaluation; + auto& poly_degree = ipa_pub_input.poly_degree; + auto& aux_generator = ipa_pub_input.aux_generator; + + // Compute C_prime + element C_prime = commitment + (aux_generator * evaluation); + + // Compute the round challeneges and their inverses. + const size_t log_poly_degree = static_cast(numeric::get_msb(poly_degree)); + std::vector round_challenges(log_poly_degree); + for (size_t i = 0; i < log_poly_degree; i++) { + round_challenges[i] = ipa_pub_input.round_challenges[i]; + } + std::vector round_challenges_inv(log_poly_degree); + for (size_t i = 0; i < log_poly_degree; i++) { + round_challenges_inv[i] = ipa_pub_input.round_challenges[i]; + } + Fr::batch_invert(&round_challenges_inv[0], log_poly_degree); + std::vector L_vec(log_poly_degree); + std::vector R_vec(log_poly_degree); + for (size_t i = 0; i < log_poly_degree; i++) { + L_vec[i] = ipa_proof.L_vec[i]; + R_vec[i] = ipa_proof.R_vec[i]; + } + + // Compute C_zero = C_prime + ∑_{j ∈ [k]} u_j^2L_j + ∑_{j ∈ [k]} u_j^{-2}R_j + const size_t pippenger_size = 2 * log_poly_degree; + std::vector msm_elements(pippenger_size); + std::vector msm_scalars(pippenger_size); + for (size_t i = 0; i < log_poly_degree; i++) { + msm_elements[size_t(2) * i] = L_vec[i]; + msm_elements[size_t(2) * i + 1] = R_vec[i]; + msm_scalars[size_t(2) * i] = round_challenges[i] * round_challenges[i]; + msm_scalars[size_t(2) * i + 1] = round_challenges_inv[i] * round_challenges_inv[i]; + } + auto pippenger_runtime_state_1 = barretenberg::scalar_multiplication::pippenger_runtime_state(pippenger_size); + element LR_sums = barretenberg::scalar_multiplication::pippenger_without_endomorphism_basis_points( + &msm_scalars[0], &msm_elements[0], pippenger_size, pippenger_runtime_state_1); + element C_zero = C_prime + LR_sums; + + /** + * Compute b_zero where b_zero can be computed using the polynomial: + * + * g(X) = ∏_{i ∈ [k]} (u_{k-i}^{-1} + u_{k-i}.X^{2^{i-1}}). + * + * b_zero = g(evaluation) = ∏_{i ∈ [k]} (u_{k-i}^{-1} + u_{k-i}. (evaluation)^{2^{i-1}}) + */ + Fr b_zero = Fr::one(); + for (size_t i = 0; i < log_poly_degree; i++) { + auto exponent = static_cast(Fr(2).pow(i)); + b_zero *= round_challenges_inv[log_poly_degree - 1 - i] + + (round_challenges[log_poly_degree - 1 - i] * challenge_point.pow(exponent)); + } + // Compute G_zero + // First construct s_vec + std::vector s_vec(poly_degree); + for (size_t i = 0; i < poly_degree; i++) { + Fr s_vec_scalar = Fr::one(); + for (size_t j = (log_poly_degree - 1); j != size_t(-1); j--) { + auto bit = (i >> j) & 1; + bool b = static_cast(bit); + if (b) { + s_vec_scalar *= round_challenges[log_poly_degree - 1 - j]; + } else { + s_vec_scalar *= round_challenges_inv[log_poly_degree - 1 - j]; + } + } + s_vec[i] = s_vec_scalar; + } + // Copy the G_vector to local memory. + std::vector G_vec_local(poly_degree); + for (size_t i = 0; i < poly_degree; i++) { + G_vec_local[i] = G_vector[i]; + } + auto pippenger_runtime_state_2 = barretenberg::scalar_multiplication::pippenger_runtime_state(poly_degree); + auto G_zero = barretenberg::scalar_multiplication::pippenger_without_endomorphism_basis_points( + &s_vec[0], &G_vec_local[0], poly_degree, pippenger_runtime_state_2); + element right_hand_side = G_zero * a_zero; + Fr a_zero_b_zero = a_zero * b_zero; + right_hand_side += aux_generator * a_zero_b_zero; + return (C_zero.normalize() == right_hand_side.normalize()); + } +}; \ No newline at end of file diff --git a/cpp/src/aztec/honk/commitment_scheme/ipa/ipa.test.cpp b/cpp/src/aztec/honk/commitment_scheme/ipa/ipa.test.cpp new file mode 100644 index 0000000000..0c7eb51aa7 --- /dev/null +++ b/cpp/src/aztec/honk/commitment_scheme/ipa/ipa.test.cpp @@ -0,0 +1,66 @@ +#include "ipa.hpp" +#include +#include +#include "./polynomials/polynomial_arithmetic.hpp" +#include "./polynomials/polynomial.hpp" +#include +#include +using namespace barretenberg; + +TEST(honk_commitment_scheme, ipa_commit) +{ + constexpr size_t n = 1024; + std::vector scalars(n); + std::vector points(n); + + for (size_t i = 0; i < n; i++) { + scalars[i] = barretenberg::fr::random_element(); + points[i] = barretenberg::g1::affine_element(barretenberg::g1::element::random_element()); + } + + barretenberg::g1::element expected = points[0] * scalars[0]; + for (size_t i = 1; i < n; i++) { + expected += points[i] * scalars[i]; + } + + InnerProductArgument newIpa; + barretenberg::g1::element result = newIpa.commit(scalars, n, points); + EXPECT_EQ(expected.normalize(), result.normalize()); +} + +TEST(honk_commitment_scheme, ipa_open) +{ + // generate a random polynomial coeff, degree needs to be a power of two + size_t n = 1024; + std::vector coeffs(n); + for (size_t i = 0; i < n; ++i) { + coeffs[i] = barretenberg::fr::random_element(); + } + // generate random evaluation point x and the evaluation + auto x = barretenberg::fr::random_element(); + auto eval = polynomial_arithmetic::evaluate(&coeffs[0], x, n); + // generate G_vec for testing, bypassing srs + std::vector G_vec(n); + for (size_t i = 0; i < n; ++i) { + auto scalar = fr::random_element(); + G_vec[i] = barretenberg::g1::affine_element(barretenberg::g1::one * scalar); + } + InnerProductArgument newIpa; + auto C = newIpa.commit(coeffs, n, G_vec); + InnerProductArgument::IpaPubInput pub_input; + pub_input.commitment = C; + pub_input.challenge_point = x; + pub_input.evaluation = eval; + pub_input.poly_degree = n; + auto aux_scalar = fr::random_element(); + pub_input.aux_generator = barretenberg::g1::one * aux_scalar; + const size_t log_n = static_cast(numeric::get_msb(n)); + pub_input.round_challenges = std::vector(log_n); + for (size_t i = 0; i < log_n; i++) { + pub_input.round_challenges[i] = barretenberg::fr::random_element(); + } + auto proof = newIpa.ipa_prove(pub_input, coeffs, G_vec); + auto result = + InnerProductArgument::ipa_verify(proof, pub_input, G_vec); + EXPECT_TRUE(result); +}