Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 20-30% cost reduction in recursive ipa algorithm #9420

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ jobs:
barretenberg-cpp: ${{ steps.filter.outputs.barretenberg-cpp }}
noir: ${{ steps.filter.outputs.noir }}
noir-projects: ${{ steps.filter.outputs.noir-projects }}
yarn-project: ${{ steps.filter.outputs.yarn-project }}
txe: ${{ steps.filter.outputs.txe }}
l1-contracts: ${{ steps.filter.outputs.l1-contracts }}
non-docs: ${{ steps.filter.outputs.non-docs }}
Expand Down Expand Up @@ -95,6 +96,8 @@ jobs:
- 'l1-contracts/**'
noir-projects:
- 'noir-projects/**'
yarn-project:
- 'yarn-project/**'
txe:
- 'yarn-project/txe/**'
non-barretenberg-cpp:
Expand Down Expand Up @@ -155,7 +158,7 @@ jobs:
# prepare images locally, tagged by commit hash
- name: "Build E2E Image"
timeout-minutes: 40
if: (needs.configure.outputs.non-docs == 'true' && needs.configure.outputs.non-misc-ci == 'true' && needs.configure.outputs.non-barretenberg-cpp == 'true') || github.ref_name == 'master'
if: (needs.configure.outputs.non-docs == 'true' && needs.configure.outputs.non-barretenberg-cpp == 'true') || github.ref_name == 'master'
run: |
earthly-ci ./yarn-project+export-e2e-test-images
# We base our e2e list used in e2e-x86 off the targets in ./yarn-project/end-to-end
Expand Down Expand Up @@ -184,7 +187,7 @@ jobs:
# all the non-bench end-to-end integration tests for aztec
e2e:
needs: [build, configure]
if: (needs.configure.outputs.non-docs == 'true' && needs.configure.outputs.non-misc-ci == 'true' && needs.configure.outputs.non-barretenberg-cpp == 'true') || github.ref_name == 'master' || contains(github.event.pull_request.labels.*.name, 'e2e')
if: (needs.configure.outputs.non-docs == 'true' && needs.configure.outputs.non-barretenberg-cpp == 'true') || github.ref_name == 'master' || contains(github.event.pull_request.labels.*.name, 'e2e')
runs-on: ubuntu-20.04
strategy:
fail-fast: false
Expand Down Expand Up @@ -216,7 +219,7 @@ jobs:
# all the benchmarking end-to-end integration tests for aztec (not required to merge)
bench-e2e:
needs: [build, configure]
if: needs.build.outputs.bench_list != '[]' && ((needs.configure.outputs.non-docs == 'true' && needs.configure.outputs.non-misc-ci == 'true' && needs.configure.outputs.non-barretenberg-cpp == 'true') || github.ref_name == 'master' || contains(github.event.pull_request.labels.*.name, 'bench'))
if: needs.build.outputs.bench_list != '[]' && ((needs.configure.outputs.non-docs == 'true' && needs.configure.outputs.non-barretenberg-cpp == 'true') || github.ref_name == 'master' || contains(github.event.pull_request.labels.*.name, 'bench'))
runs-on: ubuntu-20.04
strategy:
fail-fast: false
Expand Down Expand Up @@ -525,7 +528,7 @@ jobs:
noir-examples:
needs: [build, configure]
runs-on: ${{ needs.configure.outputs.username }}-x86
if: needs.configure.outputs.barretenberg == 'true' || needs.configure.outputs.noir == 'true'
if: needs.configure.outputs.noir == 'true'
steps:
- uses: actions/checkout@v4
with: { ref: "${{ env.GIT_COMMIT }}" }
Expand All @@ -552,7 +555,7 @@ jobs:
noir-projects:
needs: [build, configure]
runs-on: ${{ needs.configure.outputs.username }}-x86
if: needs.configure.outputs.barretenberg == 'true' || needs.configure.outputs.noir == 'true' || needs.configure.outputs.noir-projects == 'true' || needs.configure.outputs.txe == 'true'
if: needs.configure.outputs.noir == 'true' || needs.configure.outputs.noir-projects == 'true' || needs.configure.outputs.txe == 'true'
steps:
- uses: actions/checkout@v4
with: { ref: "${{ env.GIT_COMMIT }}" }
Expand Down Expand Up @@ -581,6 +584,7 @@ jobs:

yarn-project-formatting:
needs: [build, configure]
if: needs.configure.outputs.yarn-project == 'true' || github.ref_name == 'master'
runs-on: ${{ needs.configure.outputs.username }}-x86
steps:
- uses: actions/checkout@v4
Expand All @@ -594,6 +598,7 @@ jobs:

yarn-project-test:
needs: [build, configure]
if: needs.configure.outputs.yarn-project == 'true' || github.ref_name == 'master'
runs-on: ${{ needs.configure.outputs.username }}-x86
steps:
- uses: actions/checkout@v4
Expand All @@ -608,6 +613,7 @@ jobs:
prover-client-test:
needs: [build, configure]
runs-on: ${{ needs.configure.outputs.username }}-x86
if: needs.configure.outputs.yarn-project == 'true' || github.ref_name == 'master'
steps:
- uses: actions/checkout@v4
with: { ref: "${{ env.GIT_COMMIT }}" }
Expand All @@ -621,6 +627,7 @@ jobs:
# proving disabled
network-test:
needs: [build, configure]
if: needs.configure.outputs.yarn-project == 'true'
runs-on: ${{ needs.configure.outputs.username }}-x86
strategy:
max-parallel: 1
Expand All @@ -642,6 +649,7 @@ jobs:
# note: proving disabled
kind-network-test:
needs: [build, configure]
if: needs.configure.output.yarn-project == 'true'
runs-on: ${{ needs.configure.outputs.username }}-x86
strategy:
fail-fast: false
Expand Down Expand Up @@ -797,7 +805,7 @@ jobs:

protocol-circuits-gates-report:
needs: [build, configure]
if: needs.configure.outputs.non-docs == 'true' && needs.configure.outputs.non-misc-ci == 'true'
if: needs.configure.outputs.non-docs == 'true' && needs.configure.outputs.non-barretenberg-cpp == 'true'
runs-on: ${{ needs.configure.outputs.username }}-x86
permissions:
pull-requests: write
Expand Down Expand Up @@ -1000,4 +1008,4 @@ jobs:
"url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_NOTIFY_WORKFLOW_TRIGGER_URL }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_NOTIFY_WORKFLOW_TRIGGER_URL }}
131 changes: 71 additions & 60 deletions barretenberg/cpp/src/barretenberg/commitment_schemes/ipa/ipa.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -369,20 +369,29 @@ template <typename Curve_> class IPA {
// Construct vector s
std::vector<Fr> s_vec(poly_length, Fr::one());

// TODO(https://github.com/AztecProtocol/barretenberg/issues/857): This code is not efficient as its
// O(nlogn). This can be optimized to be linear by computing a tree of products. Its very readable, so we're
// leaving it unoptimized for now.
parallel_for_heuristic(
poly_length,
[&](size_t i) {
for (size_t j = (log_poly_degree - 1); j != static_cast<size_t>(-1); j--) {
auto bit = (i >> j) & 1;
bool b = static_cast<bool>(bit);
if (b) {
s_vec[i] *= round_challenges_inv[log_poly_degree - 1 - j];
}
}
}, thread_heuristics::FF_MULTIPLICATION_COST * log_poly_degree);
std::vector<Fr> s_vec_temporaries(poly_length / 2);

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_degree & 1) == 0)
{
std::swap(previous_round_s, current_round_s);
}
previous_round_s[0] = Fr(1);
for (size_t i = 0; i < log_poly_degree; ++i)
{
const size_t round_size = 1 << (i + 1);
const Fr round_challenge = round_challenges_inv[i];
parallel_for_heuristic(
round_size / 2,
[&](size_t j) {
current_round_s[j * 2] = previous_round_s[j];
current_round_s[j * 2 + 1] = previous_round_s[j] * round_challenge;
}, thread_heuristics::FF_MULTIPLICATION_COST * 2);
std::swap(current_round_s, previous_round_s);
}


std::span<const Commitment> srs_elements = vk->get_monomial_points();
if (poly_length * 2 > srs_elements.size()) {
Expand Down Expand Up @@ -454,28 +463,20 @@ template <typename Curve_> class IPA {
const Fr generator_challenge = transcript->template get_challenge<Fr>("IPA:generator_challenge");
auto builder = generator_challenge.get_context();

Commitment aux_generator = Commitment::one(builder) * generator_challenge;

const auto log_poly_degree = numeric::get_msb(static_cast<uint32_t>(poly_length));

// Step 3.
// Compute C' = C + f(\beta) ⋅ U
GroupElement C_prime = opening_claim.commitment + aux_generator * opening_claim.opening_pair.evaluation;

auto pippenger_size = 2 * log_poly_degree;
std::vector<Fr> round_challenges(log_poly_degree);
std::vector<Fr> round_challenges_inv(log_poly_degree);
std::vector<Commitment> msm_elements(pippenger_size);
std::vector<Fr> msm_scalars(pippenger_size);

// Step 4.
// Step 3.
// Receive all L_i and R_i and prepare for MSM
for (size_t i = 0; i < log_poly_degree; i++) {
std::string index = std::to_string(log_poly_degree - 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;
Expand All @@ -484,63 +485,73 @@ template <typename Curve_> class IPA {
msm_scalars[2 * i + 1] = round_challenges[i];
}

// Step 5.
// Compute C₀ = C' + ∑_{j ∈ [k]} u_j^{-1}L_j + ∑_{j ∈ [k]} u_jR_j
GroupElement LR_sums = GroupElement::batch_mul(msm_elements, msm_scalars);

GroupElement C_zero = C_prime + LR_sums;

// Step 6.
// 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 < log_poly_degree; i++) {
b_zero *= Fr(1) + (round_challenges_inv[log_poly_degree - 1 - i] *
opening_claim.opening_pair.challenge.pow(1 << i));
b_zero *= Fr(1) + (round_challenges_inv[log_poly_degree - 1 - i] * challenge);
if (i != log_poly_degree - 1)
{
challenge = challenge * challenge;
}
}

// Step 7.

// 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);

// TODO(https://github.com/AztecProtocol/barretenberg/issues/857): This code is not efficient as its
// O(nlogn). This can be optimized to be linear by computing a tree of products.
for (size_t i = 0; i < poly_length; i++) {
Fr s_vec_scalar = Fr(1);
for (size_t j = (log_poly_degree - 1); j != static_cast<size_t>(-1); j--) {
auto bit = (i >> j) & 1;
bool b = static_cast<bool>(bit);
if (b) {
s_vec_scalar *= round_challenges_inv[log_poly_degree - 1 - j];
}
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_degree & 1) == 0)
{
std::swap(previous_round_s, current_round_s);
}
previous_round_s[0] = Fr(1);
for (size_t i = 0; i < log_poly_degree; ++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;
}
s_vec[i] = s_vec_scalar;
std::swap(current_round_s, previous_round_s);
}

auto srs_elements = vk->get_monomial_points();

// TODO(https://github.com/AztecProtocol/barretenberg/issues/1023): Unify the two batch_muls
// Step 6.
// Receive a₀ from the prover
auto a_zero = transcript->template receive_from_prover<Fr>("IPA:a_0");

// Step 8.
// Step 7.
// Compute G₀
// Unlike the native verification function, the verifier commitment key only containts the SRS so we can apply
// batch_mul directly on it.
auto srs_elements = vk->get_monomial_points();
Commitment G_zero = Commitment::batch_mul(srs_elements, s_vec);

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

// Step 10.
// Compute C_right
GroupElement right_hand_side = G_zero * a_zero + aux_generator * a_zero * b_zero;

// Step 11.
// Check if C_right == C₀
C_zero.assert_equal(right_hand_side);
return (C_zero.get_value() == right_hand_side.get_value());
// Step 8.
// 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);

return (ipa_relation.get_value() == -opening_claim.commitment.get_value());
}

public:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ template <typename RecursiveFlavor> class ECCVMRecursiveTests : public ::testing
OuterBuilder outer_circuit;
RecursiveVerifier verifier{ &outer_circuit, verification_key };
verifier.verify_proof(proof);
info("Recursive Verifier: num gates = ", outer_circuit.num_gates);
info("Recursive Verifier: num gates = ", outer_circuit.get_estimated_num_finalized_gates());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you print out that this number is an estimate? or finalize the circuit and print out the actual finalized gate count

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's quite expensive to actually finalise the circuit - is it important for this number to be 100% accurate?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't be too expensive right? Its fine to just keep it as is but say that its num estimated finalized gates instead of just num gates though.


// Check for a failure flag in the recursive verifier circuit
EXPECT_EQ(outer_circuit.failed(), false) << outer_circuit.err();
Expand Down Expand Up @@ -135,10 +135,10 @@ template <typename RecursiveFlavor> class ECCVMRecursiveTests : public ::testing
OuterBuilder outer_circuit;
RecursiveVerifier verifier{ &outer_circuit, verification_key };
verifier.verify_proof(proof);
info("Recursive Verifier: num gates = ", outer_circuit.num_gates);
info("Recursive Verifier: num gates = ", outer_circuit.get_estimated_num_finalized_gates());

// Check for a failure flag in the recursive verifier circuit
EXPECT_EQ(outer_circuit.failed(), true) << outer_circuit.err();
EXPECT_FALSE(CircuitChecker::check(outer_circuit));
}
};
using FlavorTypes = testing::Types<ECCVMRecursiveFlavor_<UltraCircuitBuilder>>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,17 +113,18 @@ TEST_F(GoblinRecursiveVerifierTests, ECCVMFailure)

// Tamper with the ECCVM proof
for (auto& val : proof.eccvm_proof) {
if (val > 0) { // tamper by finding the first non-zero value and incrementing it by 1
if (val > 0) { // tamper by finding the tenth non-zero value and incrementing it by 1
// tamper by finding the first non-zero value
// and incrementing it by 1
val += 1;
break;
}
}

Builder builder;
GoblinRecursiveVerifier verifier{ &builder, verifier_input };
verifier.verify(proof);

EXPECT_FALSE(CircuitChecker::check(builder));
EXPECT_DEBUG_DEATH(verifier.verify(proof), "(sumcheck_verified && batched_opening_verified)");
}

/**
Expand Down
Loading
Loading