Skip to content

Commit

Permalink
feat: Full IPA Recursive Verifier (#10189)
Browse files Browse the repository at this point in the history
Adds back the full IPA recursive verifier implementation in preparation
of adding it to the root rollup circuit. Creates new tests for the full
verification and also new tests for accumulation with different sizes.
  • Loading branch information
lucasxia01 authored Nov 27, 2024
1 parent b773c14 commit b5783d3
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 3 deletions.
145 changes: 143 additions & 2 deletions barretenberg/cpp/src/barretenberg/commitment_schemes/ipa/ipa.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ template <typename Curve_> class IPA {
return (C_zero.normalize() == right_hand_side.normalize());
}
/**
* @brief Recursively verify the correctness of an IPA proof. Unlike native verification, there is no
* @brief Recursively verify the correctness of an IPA proof, without computing G_zero. Unlike native verification, there is no
* parallelisation in this function as our circuit construction does not currently support parallelisation.
*
* @details batch_mul is used instead of pippenger as pippenger is not implemented to be used in stdlib context for
Expand Down Expand Up @@ -591,6 +591,144 @@ template <typename Curve_> class IPA {
{
return reduce_verify_internal_recursive(opening_claim, transcript);
}

/**
* @brief Fully recursively verify the correctness of an IPA proof, including computing G_zero. Unlike native verification, there is no
* parallelisation in this function as our circuit construction does not currently support parallelisation.
*
* @details batch_mul is used instead of pippenger as pippenger is not implemented to be used in stdlib context for
* now and under the hood we perform bigfield to cycle_scalar conversions for the batch_mul. That is because
* cycle_scalar has very reduced functionality at the moment and doesn't support basic arithmetic operations between
* two cycle_scalar operands (just for one cycle_group and one cycle_scalar to enable batch_mul).
* @param vk
* @param opening_claim
* @param transcript
* @return VerifierAccumulator
* @todo (https://github.com/AztecProtocol/barretenberg/issues/1018): simulator should use the native verify
* function with parallelisation
*/
static bool full_verify_recursive(const std::shared_ptr<VK>& vk,
const OpeningClaim<Curve>& opening_claim,
auto& transcript)
requires Curve::is_stdlib_type
{
// Step 1.
// Receive polynomial_degree + 1 = d from the prover
auto poly_length_var = transcript->template receive_from_prover<typename Curve::BaseField>(
"IPA:poly_degree_plus_1"); // note this is base field because this is a uint32_t, which should map
// to a bb::fr, not a grumpkin::fr, which is a BaseField element for
// Grumpkin

// TODO(https://github.com/AztecProtocol/barretenberg/issues/1144): need checks here on poly_length.
const auto poly_length = static_cast<uint32_t>(poly_length_var.get_value());
info("poly_length = ", poly_length);
// Step 2.
// Receive generator challenge u and compute auxiliary generator
const Fr generator_challenge = transcript->template get_challenge<Fr>("IPA:generator_challenge");
typename Curve::Builder* builder = generator_challenge.get_context();

const auto log_poly_length = numeric::get_msb(static_cast<uint32_t>(poly_length));
if (log_poly_length > CONST_ECCVM_LOG_N) {
throw_or_abort("IPA log_poly_length is too large");
}
auto pippenger_size = 2 * CONST_ECCVM_LOG_N;
std::vector<Fr> round_challenges(CONST_ECCVM_LOG_N);
std::vector<Fr> round_challenges_inv(CONST_ECCVM_LOG_N);
std::vector<Commitment> msm_elements(pippenger_size);
std::vector<Fr> msm_scalars(pippenger_size);


// Step 3.
// Receive all L_i and R_i and prepare for MSM
for (size_t i = 0; i < CONST_ECCVM_LOG_N; i++) {
// TODO(https://github.com/AztecProtocol/barretenberg/issues/1114): insecure dummy_round derivation!
stdlib::bool_t<typename Curve::Builder> dummy_round = stdlib::witness_t(builder, i >= log_poly_length);

std::string index = std::to_string(CONST_ECCVM_LOG_N - i - 1);
auto element_L = transcript->template receive_from_prover<Commitment>("IPA:L_" + index);
auto element_R = transcript->template receive_from_prover<Commitment>("IPA:R_" + index);
round_challenges[i] = transcript->template get_challenge<Fr>("IPA:round_challenge_" + index);
round_challenges_inv[i] = round_challenges[i].invert();

msm_elements[2 * i] = element_L;
msm_elements[2 * i + 1] = element_R;
msm_scalars[2 * i] = Fr::conditional_assign(dummy_round, Fr(0), round_challenges_inv[i]);
msm_scalars[2 * i + 1] = Fr::conditional_assign(dummy_round, Fr(0), round_challenges[i]);
}

// Step 4.
// Compute b_zero where b_zero can be computed using the polynomial:
// g(X) = ∏_{i ∈ [k]} (1 + u_{i-1}^{-1}.X^{2^{i-1}}).
// b_zero = g(evaluation) = ∏_{i ∈ [k]} (1 + u_{i-1}^{-1}. (evaluation)^{2^{i-1}})

Fr b_zero = Fr(1);
Fr challenge = opening_claim.opening_pair.challenge;
for (size_t i = 0; i < CONST_ECCVM_LOG_N; i++) {
stdlib::bool_t<typename Curve::Builder> dummy_round = stdlib::witness_t(builder, i < CONST_ECCVM_LOG_N - log_poly_length);

Fr monomial = Fr::conditional_assign(dummy_round, Fr(0), round_challenges_inv[CONST_ECCVM_LOG_N - 1 - i] * challenge);
b_zero *= Fr(1) + monomial;
if (i != CONST_ECCVM_LOG_N - 1) // this if statement is fine because the number of iterations is constant
{
challenge = Fr::conditional_assign(dummy_round, challenge, challenge * challenge);
}
}

// Step 5.
// Construct vector s
// We implement a linear-time algorithm to optimally compute this vector
// Note: currently requires an extra vector of size `poly_length / 2` to cache temporaries
// this might able to be optimized if we care enough, but the size of this poly shouldn't be large relative to the builder polynomial sizes
std::vector<Fr> s_vec_temporaries(poly_length / 2);
std::vector<Fr> s_vec(poly_length);

Fr* previous_round_s = &s_vec_temporaries[0];
Fr* current_round_s = &s_vec[0];
// if number of rounds is even we need to swap these so that s_vec always contains the result
if ((log_poly_length & 1) == 0)
{
std::swap(previous_round_s, current_round_s);
}
previous_round_s[0] = Fr(1);
for (size_t i = 0; i < log_poly_length; ++i)
{
const size_t round_size = 1 << (i + 1);
const Fr round_challenge = round_challenges_inv[i];
for (size_t j = 0; j < round_size / 2; ++j)
{
current_round_s[j * 2] = previous_round_s[j];
current_round_s[j * 2 + 1] = previous_round_s[j] * round_challenge;
}
std::swap(current_round_s, previous_round_s);
}
// Receive G₀ from the prover
Commitment transcript_G_zero = transcript->template receive_from_prover<Commitment>("IPA:G_0");
// Compute G₀
// Unlike the native verification function, the verifier commitment key only containts the SRS so we can apply
// batch_mul directly on it.
const std::vector<Commitment> srs_elements = vk->get_monomial_points();
Commitment G_zero = Commitment::batch_mul(srs_elements, s_vec);
ASSERT(G_zero.get_value() == transcript_G_zero.get_value() && "G_zero doesn't match received G_zero failed.");

// Step 6.
// Receive a₀ from the prover
const auto a_zero = transcript->template receive_from_prover<Fr>("IPA:a_0");

// Step 7.
// Compute R = C' + ∑_{j ∈ [k]} u_j^{-1}L_j + ∑_{j ∈ [k]} u_jR_j - G₀ * a₀ - (f(\beta) + a₀ * b₀) ⋅ U
// This is a combination of several IPA relations into a large batch mul
// which should be equal to -C
msm_elements.emplace_back(-G_zero);
msm_elements.emplace_back(-Commitment::one(builder));
msm_scalars.emplace_back(a_zero);
msm_scalars.emplace_back(generator_challenge * a_zero.madd(b_zero, {-opening_claim.opening_pair.evaluation}));
GroupElement ipa_relation = GroupElement::batch_mul(msm_elements, msm_scalars);
ipa_relation.assert_equal(-opening_claim.commitment);

ASSERT(ipa_relation.get_value() == -opening_claim.commitment.get_value() && "IPA relation failed.");
return (ipa_relation.get_value() == -opening_claim.commitment.get_value());
}

/**
* @brief A method that produces an IPA opening claim from Shplemini accumulator containing vectors of commitments
* and scalars and a Shplonk evaluation challenge.
Expand Down Expand Up @@ -742,8 +880,11 @@ template <typename Curve_> class IPA {
* @return Polynomial<bb::fq>
*/
static Polynomial<bb::fq> create_challenge_poly(const size_t log_poly_length_1, const std::vector<bb::fq>& u_challenges_inv_1, const size_t log_poly_length_2, const std::vector<bb::fq>& u_challenges_inv_2, bb::fq alpha) {
Polynomial challenge_poly = construct_poly_from_u_challenges_inv(log_poly_length_1, u_challenges_inv_1);
// Always extend each to 1<<CONST_ECCVM_LOG_N length
Polynomial<bb::fq> challenge_poly(1<<CONST_ECCVM_LOG_N);
Polynomial challenge_poly_1 = construct_poly_from_u_challenges_inv(log_poly_length_1, u_challenges_inv_1);
Polynomial challenge_poly_2 = construct_poly_from_u_challenges_inv(log_poly_length_2, u_challenges_inv_2);
challenge_poly += challenge_poly_1;
challenge_poly.add_scaled(challenge_poly_2, alpha);
return challenge_poly;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,100 @@ TEST_F(IPARecursiveTests, AccumulateMedium)
TEST_F(IPARecursiveTests, ConstantVerifier)
{
test_fixed_ipa_recursive_verifier();
}

TEST_F(IPARecursiveTests, FullRecursiveVerifier)
{
const size_t POLY_LENGTH = 1024;
Builder builder;
auto [stdlib_transcript, stdlib_claim] = create_ipa_claim(builder, POLY_LENGTH);

auto stdlib_pcs_vkey = std::make_shared<VerifierCommitmentKey<Curve>>(&builder, POLY_LENGTH, this->vk());
auto result = RecursiveIPA::full_verify_recursive(stdlib_pcs_vkey, stdlib_claim, stdlib_transcript);
EXPECT_TRUE(result);
builder.finalize_circuit(/*ensure_nonzero=*/true);
info("Full IPA Recursive Verifier num finalized gates for length ",
POLY_LENGTH,
" = ",
builder.get_num_finalized_gates());
EXPECT_TRUE(CircuitChecker::check(builder));
}

TEST_F(IPARecursiveTests, AccumulationAndFullRecursiveVerifier)
{
const size_t POLY_LENGTH = 1024;

// We create a circuit that does two IPA verifications. However, we don't do the full verifications and instead
// accumulate the claims into one claim. This accumulation is done in circuit. Create two accumulators, which
// contain the commitment and an opening claim.
Builder builder;

auto [transcript_1, claim_1] = create_ipa_claim(builder, POLY_LENGTH);
auto [transcript_2, claim_2] = create_ipa_claim(builder, POLY_LENGTH);

// Creates two IPA accumulators and accumulators from the two claims. Also constructs the accumulated h
// polynomial.
auto [output_claim, ipa_proof] = RecursiveIPA::accumulate(this->ck(), transcript_1, claim_1, transcript_2, claim_2);
builder.finalize_circuit(/*ensure_nonzero=*/false);
info("Circuit with 2 IPA Recursive Verifiers and IPA Accumulation num finalized gates = ",
builder.get_num_finalized_gates());

EXPECT_TRUE(CircuitChecker::check(builder));

Builder root_rollup;
// Fully recursively verify this proof to check it.
auto stdlib_pcs_vkey =
std::make_shared<VerifierCommitmentKey<Curve>>(&root_rollup, 1 << CONST_ECCVM_LOG_N, this->vk());
auto stdlib_verifier_transcript =
std::make_shared<StdlibTranscript>(convert_native_proof_to_stdlib(&root_rollup, ipa_proof));
OpeningClaim<Curve> ipa_claim;
ipa_claim.opening_pair.challenge =
Curve::ScalarField::create_from_u512_as_witness(&root_rollup, output_claim.opening_pair.challenge.get_value());
ipa_claim.opening_pair.evaluation =
Curve::ScalarField::create_from_u512_as_witness(&root_rollup, output_claim.opening_pair.evaluation.get_value());
ipa_claim.commitment = Curve::AffineElement::from_witness(&root_rollup, output_claim.commitment.get_value());
auto result = RecursiveIPA::full_verify_recursive(stdlib_pcs_vkey, ipa_claim, stdlib_verifier_transcript);
root_rollup.finalize_circuit(/*ensure_nonzero=*/true);
EXPECT_TRUE(result);
info("Full IPA Recursive Verifier num finalized gates for length ",
1 << CONST_ECCVM_LOG_N,
" = ",
root_rollup.get_num_finalized_gates());
}

/**
* @brief Test accumulation of IPA claims with different polynomial lengths
*
*/
TEST_F(IPARecursiveTests, AccumulationWithDifferentSizes)
{
// We create a circuit that does two IPA verifications of different sizes. However, we don't do the full
// verifications and instead accumulate the claims into one claim. This accumulation is done in circuit. Create two
// accumulators, which contain the commitment and an opening claim.
const size_t POLY_LENGTH_1 = 16;
const size_t POLY_LENGTH_2 = 32;
Builder builder;

auto [transcript_1, claim_1] = create_ipa_claim(builder, POLY_LENGTH_1);
auto [transcript_2, claim_2] = create_ipa_claim(builder, POLY_LENGTH_2);

// Creates two IPA accumulators and accumulators from the two claims. Also constructs the accumulated h
// polynomial.
auto [output_claim, ipa_proof] = RecursiveIPA::accumulate(this->ck(), transcript_1, claim_1, transcript_2, claim_2);
builder.finalize_circuit(/*ensure_nonzero=*/false);
info("Circuit with 2 IPA Recursive Verifiers and IPA Accumulation num finalized gates = ",
builder.get_num_finalized_gates());

EXPECT_TRUE(CircuitChecker::check(builder));

const OpeningPair<NativeCurve> opening_pair{ bb::fq(output_claim.opening_pair.challenge.get_value()),
bb::fq(output_claim.opening_pair.evaluation.get_value()) };
Commitment native_comm = output_claim.commitment.get_value();
const OpeningClaim<NativeCurve> opening_claim{ opening_pair, native_comm };

// Natively verify this proof to check it.
auto verifier_transcript = std::make_shared<NativeTranscript>(ipa_proof);

auto result = NativeIPA::reduce_verify(this->vk(), opening_claim, verifier_transcript);
EXPECT_TRUE(result);
}
2 changes: 1 addition & 1 deletion barretenberg/cpp/src/barretenberg/constants.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ static constexpr uint32_t CONST_PROOF_SIZE_LOG_N = 28;
// circuits being folded.
static constexpr uint32_t CONST_PG_LOG_N = 20;

static constexpr uint32_t CONST_ECCVM_LOG_N = 16;
static constexpr uint32_t CONST_ECCVM_LOG_N = 15;

static constexpr uint32_t MAX_LOOKUP_TABLES_SIZE = 70000;

Expand Down

0 comments on commit b5783d3

Please sign in to comment.