diff --git a/barretenberg/cpp/scripts/bench_batch_verifier.sh b/barretenberg/cpp/scripts/bench_batch_verifier.sh new file mode 100644 index 000000000000..9559cc2a9b4f --- /dev/null +++ b/barretenberg/cpp/scripts/bench_batch_verifier.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Benchmark the batch verifier service with realistic pinned IVC inputs. +# +# Usage: +# ./scripts/bench_batch_verifier.sh # Run locally +# ./scripts/bench_batch_verifier.sh --remote # Run on remote benchmark machine +# +# This script: +# 1. Downloads pinned IVC inputs from S3 (if not already cached) +# 2. Builds and runs chonk_bench with IVC_INPUTS_DIR pointing to the inputs +# 3. Filters to only run the BatchVerifyService benchmark +set -eu + +cd "$(dirname "$0")/.." + +# Pinned inputs config (kept in sync with test_chonk_standalone_vks_havent_changed.sh) +pinned_short_hash="b99f5b94" +pinned_url="https://aztec-ci-artifacts.s3.us-east-2.amazonaws.com/protocol/bb-chonk-inputs-${pinned_short_hash}.tar.gz" +inputs_cache_dir="/tmp/bb-chonk-inputs-${pinned_short_hash}" + +function download_inputs { + local dest="$1" + if [[ -d "$dest" ]] && ls "$dest"/*/ivc-inputs.msgpack &>/dev/null; then + echo "Using cached pinned inputs at $dest" + return + fi + echo "Downloading pinned IVC inputs (hash: $pinned_short_hash)..." + mkdir -p "$dest" + curl -s -f "$pinned_url" | tar -xz -C "$dest" + echo "Inputs downloaded to $dest ($(ls "$dest" | wc -l) flows)" +} + +PRESET=${PRESET:-clang20-no-avm} +BUILD_DIR=${BUILD_DIR:-build} +BENCHMARK=chonk_bench +FILTER="BatchVerify" + +if [[ "${1:-}" == "--remote" ]]; then + # Remote mode: build locally, download inputs on remote, run there + cmake --preset "$PRESET" + cmake --build --preset "$PRESET" --target "$BENCHMARK" + + source scripts/_benchmark_remote_lock.sh + + cd "$BUILD_DIR" + scp $BB_SSH_KEY ./bin/$BENCHMARK "$BB_SSH_INSTANCE:$BB_SSH_CPP_PATH/build/" + + # Download inputs on remote and run + ssh $BB_SSH_KEY "$BB_SSH_INSTANCE" bash -c "' + set -eu + INPUTS_DIR=/tmp/bb-chonk-inputs-${pinned_short_hash} + if [[ ! -d \$INPUTS_DIR ]] || ! ls \$INPUTS_DIR/*/ivc-inputs.msgpack &>/dev/null; then + echo \"Downloading pinned inputs on remote...\" + mkdir -p \$INPUTS_DIR + curl -s -f \"${pinned_url}\" | tar -xz -C \$INPUTS_DIR + fi + cd $BB_SSH_CPP_PATH/build + HARDWARE_CONCURRENCY=${HARDWARE_CONCURRENCY:-16} IVC_INPUTS_DIR=\$INPUTS_DIR ./chonk_bench --benchmark_filter=$FILTER + '" +else + # Local mode + download_inputs "$inputs_cache_dir" + + cmake --preset "$PRESET" + cmake --build --preset "$PRESET" --target "$BENCHMARK" + + cd "$BUILD_DIR" + IVC_INPUTS_DIR="$inputs_cache_dir" ./bin/$BENCHMARK --benchmark_filter="$FILTER" +fi diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_chonk.cpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_chonk.cpp index 0fb97f41e8cb..8a4a978b76a6 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_chonk.cpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_chonk.cpp @@ -2,6 +2,8 @@ #include "barretenberg/chonk/chonk_verifier.hpp" #include "barretenberg/chonk/mock_circuit_producer.hpp" #include "barretenberg/chonk/proof_compression.hpp" +#include "barretenberg/commitment_schemes/ipa/ipa.hpp" +#include "barretenberg/commitment_schemes/verification_key.hpp" #include "barretenberg/common/log.hpp" #include "barretenberg/common/serialize.hpp" #include "barretenberg/common/throw_or_abort.hpp" @@ -9,9 +11,15 @@ #include "barretenberg/dsl/acir_format/acir_to_constraint_buf.hpp" #include "barretenberg/dsl/acir_format/hypernova_recursion_constraint.hpp" #include "barretenberg/dsl/acir_format/serde/witness_stack.hpp" +#include "barretenberg/eccvm/eccvm_flavor.hpp" #include "barretenberg/serialize/msgpack_check_eq.hpp" #include "barretenberg/stdlib_circuit_builders/mega_circuit_builder.hpp" +#ifndef __wasm__ +#include +#include +#endif + namespace bb::bbapi { ChonkStart::Response ChonkStart::execute(BBApiRequest& request) && @@ -267,4 +275,253 @@ ChonkDecompressProof::Response ChonkDecompressProof::execute(const BBApiRequest& return { .proof = ProofCompressor::decompress_chonk_proof(compressed_proof, mega_num_pub) }; } +ChonkBatchVerify::Response ChonkBatchVerify::execute(const BBApiRequest& /*request*/) && +{ + BB_BENCH_NAME(MSGPACK_SCHEMA_NAME); + + if (proofs.size() != vks.size()) { + throw_or_abort("ChonkBatchVerify: proofs.size() (" + std::to_string(proofs.size()) + ") != vks.size() (" + + std::to_string(vks.size()) + ")"); + } + if (proofs.empty()) { + throw_or_abort("ChonkBatchVerify: no proofs provided"); + } + + using VerificationKey = Chonk::MegaVerificationKey; + + // Phase 1: Run all non-IPA verification for each proof, collecting IPA claims + std::vector> ipa_claims; + std::vector> ipa_transcripts; + ipa_claims.reserve(proofs.size()); + ipa_transcripts.reserve(proofs.size()); + + for (size_t i = 0; i < proofs.size(); ++i) { + validate_vk_size(vks[i]); + auto hiding_kernel_vk = std::make_shared(from_buffer(vks[i])); + + const size_t expected_proof_size = + static_cast(hiding_kernel_vk->num_public_inputs) + ChonkProof::PROOF_LENGTH_WITHOUT_PUB_INPUTS; + if (proofs[i].size() != expected_proof_size) { + throw_or_abort("ChonkBatchVerify: proof[" + std::to_string(i) + "] has wrong size: expected " + + std::to_string(expected_proof_size) + ", got " + std::to_string(proofs[i].size())); + } + + auto vk_and_hash = std::make_shared(hiding_kernel_vk); + ChonkNativeVerifier verifier(vk_and_hash); + auto result = verifier.reduce_to_ipa_claim(std::move(proofs[i])); + if (!result.all_checks_passed) { + return { .valid = false }; + } + ipa_claims.push_back(std::move(result.ipa_claim)); + ipa_transcripts.push_back(std::make_shared(std::move(result.ipa_proof))); + } + + // Phase 2: Batch IPA verification with single SRS MSM + auto ipa_vk = VerifierCommitmentKey{ ECCVMFlavor::ECCVM_FIXED_SIZE }; + const bool verified = IPA::batch_reduce_verify(ipa_vk, ipa_claims, ipa_transcripts); + + return { .valid = verified }; +} + +// ── Batch Verifier Service ────────────────────────────────────────────────── + +#ifndef __wasm__ + +void ChonkBatchVerifierService::start(std::vector> vks, + BatchVerifierConfig config, + const std::string& fifo_path) +{ + if (running_) { + info("ChonkBatchVerifierService: already running, ignoring start()"); + return; + } + + uint32_t num_cores = config.num_cores; + if (num_cores == 0) { + num_cores = static_cast(std::thread::hardware_concurrency()); + if (num_cores == 0) { + num_cores = 1; + } + } + + writer_shutdown_ = false; + running_ = true; + + writer_thread_ = std::thread([this, path = fifo_path]() { writer_loop(path); }); + + verifier_.start(std::move(vks), num_cores, config.batch_size, [this](VerifyResult result) { + { + std::lock_guard lock(result_mutex_); + result_queue_.push(std::move(result)); + } + result_cv_.notify_one(); + }); + + info("ChonkBatchVerifierService started, fifo=", fifo_path); +} + +void ChonkBatchVerifierService::enqueue(VerifyRequest request) +{ + verifier_.enqueue(std::move(request)); +} + +void ChonkBatchVerifierService::stop() +{ + if (!running_) { + return; + } + + verifier_.stop(); + + { + std::lock_guard lock(result_mutex_); + writer_shutdown_ = true; + } + result_cv_.notify_one(); + + if (writer_thread_.joinable()) { + writer_thread_.join(); + } + + running_ = false; + info("ChonkBatchVerifierService stopped"); +} + +ChonkBatchVerifierService::~ChonkBatchVerifierService() +{ + if (running_) { + stop(); + } +} + +void ChonkBatchVerifierService::writer_loop(const std::string& fifo_path) +{ + int fd = open(fifo_path.c_str(), O_WRONLY); + if (fd < 0) { + info("ChonkBatchVerifierService: failed to open FIFO '", fifo_path, "': ", strerror(errno)); + return; + } + + auto write_all = [fd](const void* data, size_t len) -> bool { + const auto* ptr = static_cast(data); + size_t remaining = len; + while (remaining > 0) { + auto written = ::write(fd, ptr, remaining); + if (written <= 0) { + return false; + } + ptr += written; + remaining -= static_cast(written); + } + return true; + }; + + while (true) { + VerifyResult result; + { + std::unique_lock lock(result_mutex_); + result_cv_.wait(lock, [this] { return writer_shutdown_ || !result_queue_.empty(); }); + + if (!result_queue_.empty()) { + result = std::move(result_queue_.front()); + result_queue_.pop(); + } else if (writer_shutdown_) { + break; + } else { + continue; + } + } + + msgpack::sbuffer buf; + msgpack::pack(buf, result); + + uint32_t len = static_cast(buf.size()); + uint8_t len_bytes[4] = { + static_cast((len >> 24) & 0xFF), + static_cast((len >> 16) & 0xFF), + static_cast((len >> 8) & 0xFF), + static_cast(len & 0xFF), + }; + + if (!write_all(len_bytes, 4) || !write_all(buf.data(), buf.size())) { + info("ChonkBatchVerifierService: FIFO write failed, stopping writer"); + break; + } + } + + close(fd); +} + +static ChonkBatchVerifierService service_; + +ChonkBatchVerifierStart::Response ChonkBatchVerifierStart::execute(const BBApiRequest& /*request*/) && +{ + if (service_.is_running()) { + throw_or_abort("ChonkBatchVerifierStart: service already running. Call ChonkBatchVerifierStop first."); + } + + using VerificationKey = Chonk::MegaVerificationKey; + + std::vector> parsed_vks; + parsed_vks.reserve(vks.size()); + + for (size_t i = 0; i < vks.size(); ++i) { + validate_vk_size(vks[i]); + auto vk = std::make_shared(from_buffer(vks[i])); + parsed_vks.push_back(std::make_shared(vk)); + } + + BatchVerifierConfig config{ + .num_cores = num_cores, + .batch_size = batch_size, + }; + + service_.start(std::move(parsed_vks), config, fifo_path); + return {}; +} + +ChonkBatchVerifierQueue::Response ChonkBatchVerifierQueue::execute(const BBApiRequest& /*request*/) && +{ + if (!service_.is_running()) { + throw_or_abort("ChonkBatchVerifierQueue: service not running. Call ChonkBatchVerifierStart first."); + } + + service_.enqueue(VerifyRequest{ + .request_id = request_id, + .vk_index = vk_index, + .proof = ChonkProof::from_field_elements(proof_fields), + }); + + return {}; +} + +ChonkBatchVerifierStop::Response ChonkBatchVerifierStop::execute(const BBApiRequest& /*request*/) && +{ + if (!service_.is_running()) { + throw_or_abort("ChonkBatchVerifierStop: service not running."); + } + + service_.stop(); + return {}; +} + +#else // __wasm__ + +ChonkBatchVerifierStart::Response ChonkBatchVerifierStart::execute(const BBApiRequest& /*request*/) && +{ + throw_or_abort("ChonkBatchVerifierStart is not supported in WASM builds"); +} + +ChonkBatchVerifierQueue::Response ChonkBatchVerifierQueue::execute(const BBApiRequest& /*request*/) && +{ + throw_or_abort("ChonkBatchVerifierQueue is not supported in WASM builds"); +} + +ChonkBatchVerifierStop::Response ChonkBatchVerifierStop::execute(const BBApiRequest& /*request*/) && +{ + throw_or_abort("ChonkBatchVerifierStop is not supported in WASM builds"); +} + +#endif // __wasm__ + } // namespace bb::bbapi diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_chonk.hpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_chonk.hpp index f880eef3d849..2e39cf8788b1 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_chonk.hpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_chonk.hpp @@ -11,8 +11,19 @@ #include "barretenberg/common/named_union.hpp" #include "barretenberg/honk/proof_system/types/proof.hpp" #include "barretenberg/serialize/msgpack.hpp" +#include #include +#ifndef __wasm__ +#include "barretenberg/chonk/batch_verifier_types.hpp" +#include "barretenberg/chonk/chonk_batch_verifier.hpp" +#include "barretenberg/chonk/chonk_proof.hpp" +#include +#include +#include +#include +#endif + namespace bb::bbapi { /** @@ -283,4 +294,127 @@ struct ChonkDecompressProof { bool operator==(const ChonkDecompressProof&) const = default; }; +/** + * @struct ChonkBatchVerify + * @brief Batch-verify multiple Chonk proofs with a single IPA SRS MSM + */ +struct ChonkBatchVerify { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "ChonkBatchVerify"; + + struct Response { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "ChonkBatchVerifyResponse"; + bool valid; + MSGPACK_FIELDS(valid); + bool operator==(const Response&) const = default; + }; + + std::vector proofs; + std::vector> vks; + Response execute(const BBApiRequest& request = {}) &&; + MSGPACK_FIELDS(proofs, vks); + bool operator==(const ChonkBatchVerify&) const = default; +}; + +#ifndef __wasm__ +/** + * @brief FIFO-streaming batch verification service for Chonk proofs. + * + * Wraps ChonkBatchVerifier and streams results over a named pipe (FIFO) + * as size-delimited msgpack payloads: [4-byte big-endian length][msgpack payload]. + * + * Lifecycle: start() → enqueue() × N → stop() + */ +class ChonkBatchVerifierService { + public: + ChonkBatchVerifierService() = default; + ~ChonkBatchVerifierService(); + + ChonkBatchVerifierService(const ChonkBatchVerifierService&) = delete; + ChonkBatchVerifierService& operator=(const ChonkBatchVerifierService&) = delete; + + void start(std::vector> vks, + BatchVerifierConfig config, + const std::string& fifo_path); + void enqueue(VerifyRequest request); + void stop(); + bool is_running() const { return running_; } + + private: + void writer_loop(const std::string& fifo_path); + + ChonkBatchVerifier verifier_; + + std::mutex result_mutex_; + std::condition_variable result_cv_; + std::queue result_queue_; + bool writer_shutdown_ = false; + std::thread writer_thread_; + + bool running_ = false; +}; +#endif // __wasm__ + +/** + * @struct ChonkBatchVerifierStart + * @brief Start the batch verifier service. + */ +struct ChonkBatchVerifierStart { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "ChonkBatchVerifierStart"; + + struct Response { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "ChonkBatchVerifierStartResponse"; + void msgpack(auto&& pack_fn) { pack_fn(); } + bool operator==(const Response&) const = default; + }; + + std::vector> vks; // Serialized verification keys + uint32_t num_cores = 0; // 0 = auto + uint32_t batch_size = 4; + std::string fifo_path; + + Response execute(const BBApiRequest& request = {}) &&; + MSGPACK_FIELDS(vks, num_cores, batch_size, fifo_path); + bool operator==(const ChonkBatchVerifierStart&) const = default; +}; + +/** + * @struct ChonkBatchVerifierQueue + * @brief Enqueue a proof for batch verification. + */ +struct ChonkBatchVerifierQueue { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "ChonkBatchVerifierQueue"; + + struct Response { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "ChonkBatchVerifierQueueResponse"; + void msgpack(auto&& pack_fn) { pack_fn(); } + bool operator==(const Response&) const = default; + }; + + uint64_t request_id = 0; + uint32_t vk_index = 0; + std::vector proof_fields; + + Response execute(const BBApiRequest& request = {}) &&; + MSGPACK_FIELDS(request_id, vk_index, proof_fields); + bool operator==(const ChonkBatchVerifierQueue&) const = default; +}; + +/** + * @struct ChonkBatchVerifierStop + * @brief Stop the batch verifier service. + */ +struct ChonkBatchVerifierStop { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "ChonkBatchVerifierStop"; + + struct Response { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "ChonkBatchVerifierStopResponse"; + void msgpack(auto&& pack_fn) { pack_fn(); } + bool operator==(const Response&) const = default; + }; + + Response execute(const BBApiRequest& request = {}) &&; + void msgpack(auto&& pack_fn) { pack_fn(); } + bool operator==(const ChonkBatchVerifierStop&) const = default; +}; + } // namespace bb::bbapi diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_execute.hpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_execute.hpp index ab16da99a508..2c94bbbda103 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_execute.hpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_execute.hpp @@ -23,6 +23,7 @@ using Command = NamedUnion; using CommandResponse = NamedUnion; /** diff --git a/barretenberg/cpp/src/barretenberg/chonk/batch_verifier_types.hpp b/barretenberg/cpp/src/barretenberg/chonk/batch_verifier_types.hpp new file mode 100644 index 000000000000..b244629f9475 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/chonk/batch_verifier_types.hpp @@ -0,0 +1,64 @@ +#pragma once +#include +#include +#include +#include + +#include "barretenberg/chonk/chonk_proof.hpp" +#include "barretenberg/serialize/msgpack.hpp" + +namespace bb { + +/** + * @brief Configuration for the batch verifier service. + */ +struct BatchVerifierConfig { + uint32_t num_cores = 0; // 0 = auto-detect + uint32_t batch_size = 4; // Number of proofs to accumulate before batch-checking +}; + +/** + * @brief Status codes for verification results. + */ +enum class VerifyStatus : uint8_t { + OK = 0, + FAILED = 1, +}; + +/** + * @brief Result of verifying a single proof within a batch. + */ +struct VerifyResult { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "VerifyResult"; + + uint64_t request_id = 0; + uint8_t status = static_cast(VerifyStatus::FAILED); + std::string error_message; + double time_in_queue_ms = 0; + double time_in_verify_ms = 0; + uint32_t batch_failure_count = 0; // Number of bisection levels to identify failure + + bool verified() const { return status == static_cast(VerifyStatus::OK); } + + static VerifyResult failed(uint64_t id, std::string msg) + { + return { .request_id = id, + .status = static_cast(VerifyStatus::FAILED), + .error_message = std::move(msg) }; + } + + MSGPACK_FIELDS(request_id, status, error_message, time_in_queue_ms, time_in_verify_ms, batch_failure_count); + bool operator==(const VerifyResult&) const = default; +}; + +/** + * @brief A request to verify a single Chonk proof. + */ +struct VerifyRequest { + uint64_t request_id = 0; + uint32_t vk_index = 0; // Index into the VK list provided at start + ChonkProof proof; + std::chrono::steady_clock::time_point enqueue_time; +}; + +} // namespace bb diff --git a/barretenberg/cpp/src/barretenberg/chonk/chonk_batch_verifier.cpp b/barretenberg/cpp/src/barretenberg/chonk/chonk_batch_verifier.cpp new file mode 100644 index 000000000000..959dc43718cd --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/chonk/chonk_batch_verifier.cpp @@ -0,0 +1,292 @@ +#ifndef __wasm__ +#include "chonk_batch_verifier.hpp" +#include "barretenberg/chonk/chonk_verifier.hpp" +#include "barretenberg/commitment_schemes/ipa/ipa.hpp" +#include "barretenberg/commitment_schemes/verification_key.hpp" +#include "barretenberg/common/log.hpp" +#include "barretenberg/common/thread.hpp" +#include "barretenberg/eccvm/eccvm_flavor.hpp" + +namespace bb { + +void ChonkBatchVerifier::start(std::vector> vks, + uint32_t num_cores, + uint32_t batch_size, + ResultCallback on_result) +{ + vks_ = std::move(vks); + num_cores_ = std::max(1u, num_cores); + batch_size_ = std::max(1u, batch_size); + on_result_ = std::move(on_result); + shutdown_ = false; + + coordinator_thread_ = std::thread([this]() { coordinator_loop(); }); + info("ChonkBatchVerifier started with ", num_cores_, " cores, batch_size=", batch_size_); +} + +void ChonkBatchVerifier::enqueue(VerifyRequest request) +{ + { + std::lock_guard lock(mutex_); + request.enqueue_time = std::chrono::steady_clock::now(); + queue_.push_back(std::move(request)); + } + cv_.notify_one(); +} + +void ChonkBatchVerifier::stop() +{ + { + std::lock_guard lock(mutex_); + shutdown_ = true; + } + cv_.notify_one(); + if (coordinator_thread_.joinable()) { + coordinator_thread_.join(); + } + info("ChonkBatchVerifier stopped"); +} + +ChonkBatchVerifier::~ChonkBatchVerifier() +{ + if (!shutdown_) { + stop(); + } +} + +void ChonkBatchVerifier::coordinator_loop() +{ + while (true) { + // ── Collect a batch ────────────────────────────────────────────── + std::vector batch; + { + std::unique_lock lock(mutex_); + + // Wait until we have work or are told to shut down. + // No timeout needed: while we're processing a batch, new proofs + // accumulate in the queue. When idle, process whatever arrives immediately. + cv_.wait(lock, [this] { return shutdown_ || !queue_.empty(); }); + + // Take up to batch_size_ items (may be a partial batch) + size_t take = std::min(queue_.size(), static_cast(batch_size_)); + if (take > 0) { + auto end = queue_.begin() + static_cast(take); + batch.assign(std::make_move_iterator(queue_.begin()), std::make_move_iterator(end)); + queue_.erase(queue_.begin(), end); + } + + if (batch.empty()) { + if (shutdown_) { + break; + } + continue; + } + + // Filter invalid-VK requests before releasing the lock + auto it = batch.begin(); + while (it != batch.end()) { + if (it->vk_index >= vks_.size()) { + on_result_( + VerifyResult::failed(it->request_id, "invalid vk_index: " + std::to_string(it->vk_index))); + it = batch.erase(it); + } else { + ++it; + } + } + } + + if (batch.empty()) { + continue; + } + + // ── Phase 1: parallel reduce (all cores, work-stealing) ────────── + auto reduce_start = std::chrono::steady_clock::now(); + auto reduce_results = parallel_reduce(batch); + + // Separate passed from failed (emit failures immediately) + std::vector passed_indices; + passed_indices.reserve(reduce_results.size()); + for (size_t i = 0; i < reduce_results.size(); ++i) { + auto& rr = reduce_results[i]; + if (!rr.all_checks_passed) { + auto result = VerifyResult::failed(rr.request_id, rr.error_message); + result.time_in_queue_ms = ms_between(rr.enqueue_time, reduce_start); + result.time_in_verify_ms = rr.reduce_ms; + on_result_(std::move(result)); + } else { + passed_indices.push_back(i); + } + } + + if (passed_indices.empty()) { + continue; + } + + // ── Phase 2: batch IPA verification ──────────────────────────── + set_parallel_for_concurrency(num_cores_); + + auto ipa_start = std::chrono::steady_clock::now(); + bool ok = batch_check(reduce_results, passed_indices); + double ipa_ms = ms_since(ipa_start); + double reduce_ms = ms_between(reduce_start, ipa_start); + + info("ChonkBatchVerifier: batch of ", + passed_indices.size(), + ": reduce=", + reduce_ms, + "ms, batch_check=", + ipa_ms, + "ms, result=", + ok ? "OK" : "BISECTING"); + + if (ok) { + emit_ok(reduce_results, passed_indices, reduce_start, ipa_ms, 0); + } else { + bisect(reduce_results, passed_indices, 0, reduce_start); + } + } +} + +std::vector ChonkBatchVerifier::parallel_reduce( + const std::vector& batch) +{ + const size_t num_proofs = batch.size(); + std::vector results(num_proofs); + std::atomic work_index{ 0 }; + + uint32_t num_workers = std::min(num_cores_, static_cast(num_proofs)); + std::vector workers; + workers.reserve(num_workers); + + for (uint32_t w = 0; w < num_workers; ++w) { + workers.emplace_back([&]() { + // Each worker thread is single-threaded for reduce_to_ipa_claim + set_parallel_for_concurrency(1); + while (true) { + size_t idx = work_index.fetch_add(1, std::memory_order_relaxed); + if (idx >= num_proofs) { + break; + } + auto& req = batch[idx]; + auto t0 = std::chrono::steady_clock::now(); + + try { + ChonkNativeVerifier verifier(vks_[req.vk_index]); + auto reduced = verifier.reduce_to_ipa_claim(req.proof); + + results[idx] = ReduceResult{ + .request_id = req.request_id, + .ipa_claim = std::move(reduced.ipa_claim), + .ipa_proof = std::move(reduced.ipa_proof), + .all_checks_passed = reduced.all_checks_passed, + .error_message = reduced.all_checks_passed ? "" : "reduction failed", + .enqueue_time = req.enqueue_time, + .reduce_ms = ms_since(t0), + }; + } catch (const std::exception& e) { + results[idx] = ReduceResult{ + .request_id = req.request_id, + .all_checks_passed = false, + .error_message = std::string("reduce_to_ipa_claim threw: ") + e.what(), + .enqueue_time = req.enqueue_time, + .reduce_ms = ms_since(t0), + }; + } catch (...) { + results[idx] = ReduceResult{ + .request_id = req.request_id, + .all_checks_passed = false, + .error_message = "reduce_to_ipa_claim threw unknown exception", + .enqueue_time = req.enqueue_time, + .reduce_ms = ms_since(t0), + }; + } + } + }); + } + for (auto& t : workers) { + t.join(); + } + + return results; +} + +bool ChonkBatchVerifier::batch_check(const std::vector& results, const std::vector& indices) +{ + if (indices.empty()) { + return true; + } + + // Collect IPA claims and transcripts for batch verification + std::vector> claims; + std::vector> transcripts; + claims.reserve(indices.size()); + transcripts.reserve(indices.size()); + for (size_t idx : indices) { + claims.push_back(results[idx].ipa_claim); + transcripts.push_back(std::make_shared(results[idx].ipa_proof)); + } + + auto ipa_vk = VerifierCommitmentKey{ ECCVMFlavor::ECCVM_FIXED_SIZE }; + return IPA::batch_reduce_verify(ipa_vk, claims, transcripts); +} + +void ChonkBatchVerifier::bisect(std::vector& results, + std::vector indices, + uint32_t depth, + std::chrono::steady_clock::time_point reduce_start) +{ + // Base case: single proof identified as the failure + if (indices.size() == 1) { + auto& rr = results[indices[0]]; + auto result = VerifyResult::failed(rr.request_id, "batch check failed (bisected to individual)"); + result.time_in_queue_ms = ms_between(rr.enqueue_time, std::chrono::steady_clock::now()); + result.time_in_verify_ms = rr.reduce_ms; + result.batch_failure_count = depth + 1; + on_result_(std::move(result)); + return; + } + + info("ChonkBatchVerifier: bisecting ", indices.size(), " proofs at depth ", depth); + + size_t mid = indices.size() / 2; + std::vector left(indices.begin(), indices.begin() + static_cast(mid)); + std::vector right(indices.begin() + static_cast(mid), indices.end()); + + // Check each half and recurse on failures + auto check_half = [&](std::vector half) { + set_parallel_for_concurrency(num_cores_); + auto t0 = std::chrono::steady_clock::now(); + bool ok = batch_check(results, half); + double check_ms = ms_since(t0); + + if (ok) { + emit_ok(results, half, reduce_start, check_ms, depth + 1); + } else { + bisect(results, std::move(half), depth + 1, reduce_start); + } + }; + + check_half(std::move(left)); + check_half(std::move(right)); +} + +void ChonkBatchVerifier::emit_ok(const std::vector& results, + const std::vector& indices, + std::chrono::steady_clock::time_point reduce_start, + double ipa_ms, + uint32_t depth) +{ + for (size_t idx : indices) { + auto& rr = results[idx]; + on_result_(VerifyResult{ + .request_id = rr.request_id, + .status = static_cast(VerifyStatus::OK), + .time_in_queue_ms = ms_between(rr.enqueue_time, reduce_start), + .time_in_verify_ms = rr.reduce_ms + ipa_ms, + .batch_failure_count = depth, + }); + } +} + +} // namespace bb +#endif diff --git a/barretenberg/cpp/src/barretenberg/chonk/chonk_batch_verifier.hpp b/barretenberg/cpp/src/barretenberg/chonk/chonk_batch_verifier.hpp new file mode 100644 index 000000000000..170c6be8f7b0 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/chonk/chonk_batch_verifier.hpp @@ -0,0 +1,107 @@ +#pragma once +#ifndef __wasm__ +#include "barretenberg/commitment_schemes/ipa/ipa.hpp" +#include "barretenberg/flavor/mega_zk_flavor.hpp" +#include "barretenberg/honk/proof_system/types/proof.hpp" +#include "batch_verifier_types.hpp" + +#include +#include +#include +#include +#include +#include + +namespace bb { + +/** + * @brief Asynchronous batch verifier for Chonk IVC proofs. + * + * Pipeline: + * Phase 1 (parallel reduce): Work-stealing threads each run full non-IPA verification + * (MegaZK sumcheck, databus, Goblin merge/eccvm/translator). + * Phase 2 (batch check): Batch IPA verification via IPA::batch_reduce_verify on all passed proofs. + * Phase 3 (emit/bisect): On success, emit OK for all. On failure, binary-search to isolate bad proofs. + */ +class ChonkBatchVerifier { + public: + using ResultCallback = std::function; + + /** + * @brief Per-proof result from the reduce phase. + */ + struct ReduceResult { + uint64_t request_id = 0; + OpeningClaim ipa_claim; + ::bb::HonkProof ipa_proof; + bool all_checks_passed = false; + std::string error_message; + std::chrono::steady_clock::time_point enqueue_time; + double reduce_ms = 0; + }; + + ChonkBatchVerifier() = default; + ~ChonkBatchVerifier(); + + ChonkBatchVerifier(const ChonkBatchVerifier&) = delete; + ChonkBatchVerifier& operator=(const ChonkBatchVerifier&) = delete; + + /** + * @brief Start the coordinator thread. + * @param vks Verification keys indexed by VerifyRequest::vk_index + * @param num_cores Number of cores for parallel reduce + * @param batch_size Number of proofs to accumulate before batch-checking + * @param on_result Callback invoked for each completed verification + */ + void start(std::vector> vks, + uint32_t num_cores, + uint32_t batch_size, + ResultCallback on_result); + + /** + * @brief Enqueue a proof for verification. + */ + void enqueue(VerifyRequest request); + + /** + * @brief Stop the processor, flushing remaining proofs. + */ + void stop(); + + private: + void coordinator_loop(); + std::vector parallel_reduce(const std::vector& batch); + bool batch_check(const std::vector& results, const std::vector& indices); + void bisect(std::vector& results, + std::vector indices, + uint32_t depth, + std::chrono::steady_clock::time_point reduce_start); + void emit_ok(const std::vector& results, + const std::vector& indices, + std::chrono::steady_clock::time_point reduce_start, + double ipa_ms, + uint32_t depth); + + static double ms_since(std::chrono::steady_clock::time_point t) + { + return std::chrono::duration(std::chrono::steady_clock::now() - t).count(); + } + static double ms_between(std::chrono::steady_clock::time_point from, std::chrono::steady_clock::time_point to) + { + return std::chrono::duration(to - from).count(); + } + + std::vector> vks_; + uint32_t num_cores_ = 1; + uint32_t batch_size_ = 4; + ResultCallback on_result_; + + std::mutex mutex_; + std::condition_variable cv_; + std::vector queue_; + bool shutdown_ = false; + std::thread coordinator_thread_; +}; + +} // namespace bb +#endif // __wasm__ diff --git a/barretenberg/cpp/src/barretenberg/chonk/chonk_batch_verifier.test.cpp b/barretenberg/cpp/src/barretenberg/chonk/chonk_batch_verifier.test.cpp new file mode 100644 index 000000000000..b93ee418c2f1 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/chonk/chonk_batch_verifier.test.cpp @@ -0,0 +1,171 @@ +#ifndef __wasm__ +#include "chonk_batch_verifier.hpp" +#include "barretenberg/chonk/chonk.hpp" +#include "barretenberg/chonk/mock_circuit_producer.hpp" +#include "barretenberg/common/test.hpp" + +#include +#include +#include + +using namespace bb; + +static constexpr size_t SMALL_LOG_2_NUM_GATES = 5; + +class ChonkBatchVerifierTests : public ::testing::Test { + protected: + static void SetUpTestSuite() { bb::srs::init_file_crs_factory(bb::srs::bb_crs_path()); } + + using CircuitProducer = PrivateFunctionExecutionMockCircuitProducer; + + static std::pair> generate_chonk_proof( + size_t num_app_circuits = 1) + { + CircuitProducer circuit_producer(num_app_circuits); + const size_t num_circuits = circuit_producer.total_num_circuits; + Chonk ivc{ num_circuits }; + TestSettings settings{ .log2_num_gates = SMALL_LOG_2_NUM_GATES }; + for (size_t j = 0; j < num_circuits; ++j) { + circuit_producer.construct_and_accumulate_next_circuit(ivc, settings); + } + return { ivc.prove(), ivc.get_hiding_kernel_vk_and_hash() }; + } + + /** + * @brief Helper: collect results from the processor via callback. + */ + struct ResultCollector { + std::mutex mutex; + std::condition_variable cv; + std::vector results; + size_t expected = 0; + + void on_result(VerifyResult r) + { + std::lock_guard lock(mutex); + results.push_back(std::move(r)); + cv.notify_one(); + } + + void wait_for(size_t count, std::chrono::seconds timeout = std::chrono::seconds(120)) + { + expected = count; + std::unique_lock lock(mutex); + ASSERT_TRUE(cv.wait_for(lock, timeout, [&] { return results.size() >= expected; })) + << "Timed out waiting for " << expected << " results, got " << results.size(); + } + }; +}; + +TEST_F(ChonkBatchVerifierTests, BatchOfTwoValidProofs) +{ + auto [proof1, vk1] = generate_chonk_proof(); + auto [proof2, vk2] = generate_chonk_proof(); + + ResultCollector collector; + ChonkBatchVerifier verifier; + + // Both proofs use VK index 0 (same VK for simplicity) + verifier.start( + { vk1 }, /*num_cores=*/2, /*batch_size=*/2, [&](VerifyResult r) { collector.on_result(std::move(r)); }); + + verifier.enqueue(VerifyRequest{ .request_id = 1, .vk_index = 0, .proof = std::move(proof1) }); + verifier.enqueue(VerifyRequest{ .request_id = 2, .vk_index = 0, .proof = std::move(proof2) }); + + collector.wait_for(2); + verifier.stop(); + + ASSERT_EQ(collector.results.size(), 2); + for (auto& r : collector.results) { + EXPECT_TRUE(r.verified()) << "request_id=" << r.request_id << " error=" << r.error_message; + EXPECT_GT(r.time_in_verify_ms, 0); + } +} + +TEST_F(ChonkBatchVerifierTests, FlushOnShutdown) +{ + // Enqueue 1 proof with batch_size=4, then stop. The proof should be flushed. + auto [proof, vk] = generate_chonk_proof(); + + ResultCollector collector; + ChonkBatchVerifier verifier; + + verifier.start( + { vk }, /*num_cores=*/1, /*batch_size=*/4, [&](VerifyResult r) { collector.on_result(std::move(r)); }); + verifier.enqueue(VerifyRequest{ .request_id = 42, .vk_index = 0, .proof = std::move(proof) }); + + // Stop triggers flush of remaining items + verifier.stop(); + + ASSERT_EQ(collector.results.size(), 1); + EXPECT_TRUE(collector.results[0].verified()); + EXPECT_EQ(collector.results[0].request_id, 42); +} + +TEST_F(ChonkBatchVerifierTests, TamperedProofBisected) +{ + BB_DISABLE_ASSERTS(); + + auto [good_proof, vk1] = generate_chonk_proof(); + auto [bad_proof, vk2] = generate_chonk_proof(); + + // Corrupt the IPA proof portion (inside goblin_proof) + ASSERT_FALSE(bad_proof.goblin_proof.ipa_proof.empty()); + bad_proof.goblin_proof.ipa_proof[0] = bad_proof.goblin_proof.ipa_proof[0] + bb::fr(1); + + ResultCollector collector; + ChonkBatchVerifier verifier; + + verifier.start( + { vk1 }, /*num_cores=*/2, /*batch_size=*/2, [&](VerifyResult r) { collector.on_result(std::move(r)); }); + + verifier.enqueue(VerifyRequest{ .request_id = 1, .vk_index = 0, .proof = std::move(good_proof) }); + verifier.enqueue(VerifyRequest{ .request_id = 2, .vk_index = 0, .proof = std::move(bad_proof) }); + + collector.wait_for(2); + verifier.stop(); + + ASSERT_EQ(collector.results.size(), 2); + + // Find good and bad results by request_id + const VerifyResult* good = nullptr; + const VerifyResult* bad = nullptr; + for (auto& r : collector.results) { + if (r.request_id == 1) { + good = &r; + } + if (r.request_id == 2) { + bad = &r; + } + } + + ASSERT_NE(good, nullptr); + ASSERT_NE(bad, nullptr); + EXPECT_TRUE(good->verified()) << "good proof should verify, error=" << good->error_message; + EXPECT_FALSE(bad->verified()) << "bad proof should fail"; + EXPECT_GT(bad->batch_failure_count, 0u) << "bisection should have occurred"; +} + +TEST_F(ChonkBatchVerifierTests, InvalidVkIndex) +{ + auto [proof, vk] = generate_chonk_proof(); + + ResultCollector collector; + ChonkBatchVerifier verifier; + + verifier.start( + { vk }, /*num_cores=*/1, /*batch_size=*/1, [&](VerifyResult r) { collector.on_result(std::move(r)); }); + + // vk_index=99 is out of range + verifier.enqueue(VerifyRequest{ .request_id = 7, .vk_index = 99, .proof = std::move(proof) }); + + collector.wait_for(1); + verifier.stop(); + + ASSERT_EQ(collector.results.size(), 1); + EXPECT_FALSE(collector.results[0].verified()); + EXPECT_EQ(collector.results[0].request_id, 7); + EXPECT_NE(collector.results[0].error_message.find("invalid vk_index"), std::string::npos); +} + +#endif // __wasm__ diff --git a/barretenberg/cpp/src/barretenberg/chonk/chonk_verifier.cpp b/barretenberg/cpp/src/barretenberg/chonk/chonk_verifier.cpp index 7f5d8caf2f4b..0cfca290dc7c 100644 --- a/barretenberg/cpp/src/barretenberg/chonk/chonk_verifier.cpp +++ b/barretenberg/cpp/src/barretenberg/chonk/chonk_verifier.cpp @@ -11,16 +11,18 @@ namespace bb { /** - * @brief Verifies a Chonk IVC proof (Native specialization). + * @brief Run Chonk verification up to but not including IPA. + * @details Verifies MegaZK proof, databus consistency, and Goblin proof, + * returning the IPA claim for deferred batch verification. */ -template <> ChonkVerifier::Output ChonkVerifier::verify(const Proof& proof) +template <> ChonkVerifier::IPAReductionResult ChonkVerifier::reduce_to_ipa_claim(const Proof& proof) { // Step 1: Verify the Hiding kernel proof (includes pairing check) HidingKernelVerifier verifier{ vk_and_hash, transcript }; auto verifier_output = verifier.verify_proof(proof.mega_proof); if (!verifier_output.result) { info("ChonkVerifier: verification failed at MegaZK verification step"); - return false; + return { {}, {}, false }; } // Extract public inputs and kernel data @@ -34,7 +36,7 @@ template <> ChonkVerifier::Output ChonkVerifier::verify(const Proo vinfo("ChonkVerifier: databus consistency verified: ", databus_consistency_verified); if (!databus_consistency_verified) { info("Chonk Verifier: verification failed at databus consistency check"); - return false; + return { {}, {}, false }; } // Step 3: Goblin verification (merge, eccvm, translator) @@ -45,13 +47,35 @@ template <> ChonkVerifier::Output ChonkVerifier::verify(const Proo if (!goblin_output.all_checks_passed) { info("ChonkVerifier: chonk verification failed at Goblin checks (merge/eccvm/translator reduction + pairing)"); + return { {}, {}, false }; + } + + return { std::move(goblin_output.ipa_claim), std::move(goblin_output.ipa_proof), true }; +} + +/** + * @brief Stub for recursive mode (reduce_to_ipa_claim is only used in native batch verification). + */ +template <> +ChonkVerifier::IPAReductionResult ChonkVerifier::reduce_to_ipa_claim([[maybe_unused]] const Proof& proof) +{ + throw_or_abort("reduce_to_ipa_claim is only available for native (non-recursive) ChonkVerifier"); +} + +/** + * @brief Verifies a Chonk IVC proof (Native specialization). + */ +template <> ChonkVerifier::Output ChonkVerifier::verify(const Proof& proof) +{ + auto result = reduce_to_ipa_claim(proof); + if (!result.all_checks_passed) { return false; } // Step 4: Verify IPA opening - auto ipa_transcript = std::make_shared(goblin_output.ipa_proof); + auto ipa_transcript = std::make_shared(result.ipa_proof); auto ipa_vk = VerifierCommitmentKey{ ECCVMFlavor::ECCVM_FIXED_SIZE }; - bool ipa_verified = IPA::reduce_verify(ipa_vk, goblin_output.ipa_claim, ipa_transcript); + bool ipa_verified = IPA::reduce_verify(ipa_vk, result.ipa_claim, ipa_transcript); vinfo("ChonkVerifier: Goblin IPA verified: ", ipa_verified); if (!ipa_verified) { info("ChonkVerifier: Chonk verification failed at IPA check"); diff --git a/barretenberg/cpp/src/barretenberg/chonk/chonk_verifier.hpp b/barretenberg/cpp/src/barretenberg/chonk/chonk_verifier.hpp index c9b86a2c1a80..04aa84585cd5 100644 --- a/barretenberg/cpp/src/barretenberg/chonk/chonk_verifier.hpp +++ b/barretenberg/cpp/src/barretenberg/chonk/chonk_verifier.hpp @@ -69,6 +69,17 @@ template class ChonkVerifier { bool all_checks_passed; // Reduction checks passed (sumcheck, evaluations, etc.) }; + /** + * @brief Result of reducing Chonk verification to an IPA opening claim (native mode only). + * @details Contains the IPA claim and proof from non-IPA verification, + * allowing batch IPA verification across multiple Chonk proofs. + */ + struct IPAReductionResult { + IPAClaim ipa_claim; + IPAProof ipa_proof; + bool all_checks_passed; + }; + using Output = std::conditional_t; using VKAndHash = typename HidingKernelVerifier::VKAndHash; using VK = typename HidingKernelVerifier::VerificationKey; @@ -104,6 +115,17 @@ template class ChonkVerifier { */ [[nodiscard("IPA claim and pairing points must be accumulated")]] Output verify(const Proof& proof); + /** + * @brief Run Chonk verification up to but not including IPA, returning the IPA claim for deferred verification. + * @details Verifies the MegaZK proof, databus consistency, and Goblin proof (merge/eccvm/translator), + * then returns the IPA opening claim and proof without performing the final IPA MSM. + * This enables batch IPA verification across multiple Chonk proofs. + * + * @param proof The Chonk proof to partially verify + * @return IPAReductionResult containing the IPA claim/proof and whether all non-IPA checks passed + */ + IPAReductionResult reduce_to_ipa_claim(const Proof& proof); + private: // VK and hash of the hiding kernel std::shared_ptr vk_and_hash; diff --git a/barretenberg/cpp/src/barretenberg/commitment_schemes/ipa/ipa.hpp b/barretenberg/cpp/src/barretenberg/commitment_schemes/ipa/ipa.hpp index 500ba9ff4356..9cfbec6a0ede 100644 --- a/barretenberg/cpp/src/barretenberg/commitment_schemes/ipa/ipa.hpp +++ b/barretenberg/cpp/src/barretenberg/commitment_schemes/ipa/ipa.hpp @@ -325,59 +325,55 @@ template class IPA } /** - * @brief Natively verify the correctness of a Proof + * @brief Per-proof data extracted from an IPA transcript. + * @details Contains all intermediate values computed during steps 2–7, 9 of the IPA verification protocol. + * Does not include the MSM (step 8). + */ + struct TranscriptData { + GroupElement C_zero; ///< \f$C_0 = C' + \sum_{j=0}^{k-1}(u_j^{-1}L_j + u_jR_j)\f$ + Fr b_zero; ///< \f$b_0 = g(\beta) = \prod_{i=0}^{k-1}(1+u_{i}^{-1}x^{2^{i}})\f$ + Polynomial s_vec; ///< \f$\vec{s}=(1,u_{0}^{-1},u_{1}^{-1},u_{0}^{-1}u_{1}^{-1},..., + ///< \prod_{i=0}^{k-1}u_{i}^{-1})\f$ + Fr gen_challenge; ///< Generator challenge \f$u\f$ where \f$U = u \cdot G\f$ + Commitment G_zero_from_prover; ///< \f$G_0\f$ received from prover (not recomputed) + Fr a_zero; ///< \f$a_0\f$ received from prover + }; + + /** + * @brief Process a single IPA proof's transcript, extracting all per-proof verification data. * - * @tparam Transcript Allows to specify a transcript class. Useful for testing - * @param vk Verification_key containing srs - * @param opening_claim Contains the commitment C and opening pair \f$(\beta, f(\beta))\f$ + * @param opening_claim Contains the commitment \f$C\f$ and opening pair \f$(\beta, f(\beta))\f$ * @param transcript Transcript with elements from the prover and generated challenges * - * @return true/false depending on if the proof verifies + * @return TranscriptData containing \f$C_0, b_0, \vec{s}, u, G_0, a_0\f$ + * + * @details Performs steps 2–7 and 9 of the IPA verification protocol. + * Does NOT compute \f$G_s=\langle \vec{s},\vec{G}\rangle\f$ (step 8, the MSM) + * or perform the final verification check (steps 10–11). * - * @details The procedure runs as follows: - * - *1. Receive commitment, challenge, and claimed evaluation from the prover - *2. Receive the generator challenge \f$u\f$, abort if it's zero, otherwise compute \f$U=u\cdot G\f$ - *3. Compute \f$C'=C+f(\beta)\cdot U\f$. (Recall that \f$f(\beta)\f$ is the claimed evaluation.) - *4. Receive \f$L_j, R_j\f$ and compute challenges \f$u_j\f$ for \f$j \in {k-1,..,0}\f$, abort immediately on - receiving a \f$u_j=0\f$ - *5. Compute \f$C_0 = C' + \sum_{j=0}^{k-1}(u_j^{-1}L_j + u_jR_j)\f$ - *6. Compute \f$b_0=g(\beta)=\prod_{i=0}^{k-1}(1+u_{i}^{-1}x^{2^{i}})\f$ - *7. Compute vector \f$\vec{s}=(1,u_{0}^{-1},u_{1}^{-1},u_{0}^{-1}u_{1}^{-1},...,\prod_{i=0}^{k-1}u_{i}^{-1})\f$ - *8. Compute \f$G_s=\langle \vec{s},\vec{G}\rangle\f$ - *9. Receive \f$\vec{a}_{0}\f$ of length 1 - *10. Compute \f$C_{right}=a_{0}G_{s}+a_{0}b_{0}U\f$ - *11. Check that \f$C_{right} = C_0\f$. If they match, return true. Otherwise return false. + * @pre add_claim_to_hash_buffer must have been called on the transcript (step 1). */ - static bool reduce_verify_internal_native(const VK& vk, const OpeningClaim& opening_claim, auto& transcript) + template + static TranscriptData read_transcript_data(const OpeningClaim& opening_claim, + const std::shared_ptr& transcript) requires(!Curve::is_stdlib_type) { - // Step 1 - // Done by `add_claim_to_hash_buffer`. - // Step 2. - // Receive generator challenge u and compute auxiliary generator const Fr generator_challenge = transcript->template get_challenge("IPA:generator_challenge"); - if (generator_challenge.is_zero()) { throw_or_abort("The generator challenge can't be zero"); } - const Commitment aux_generator = Commitment::one() * generator_challenge; // Step 3. - // Compute C' = C + f(\beta) ⋅ U, i.e., the _joint_ commitment of f and f(\beta). const GroupElement C_prime = opening_claim.commitment + (aux_generator * opening_claim.opening_pair.evaluation); const auto pippenger_size = 2 * log_poly_length; std::vector round_challenges(log_poly_length); - // the group elements that will participate in our MSM. - std::vector msm_elements(pippenger_size); // L_{k-1}, R_{k-1}, L_{k-2}, ..., L_0, R_0. - // the scalars that will participate in our MSM. - std::vector msm_scalars(pippenger_size); // w_{k-1}^{-1}, w_{k-1}, ..., w_{0}^{-1}, w_{0}. + std::vector msm_elements(pippenger_size); + std::vector msm_scalars(pippenger_size); // Step 4. - // Receive all L_i and R_i and populate msm_elements. for (size_t i = 0; i < log_poly_length; i++) { std::string index = std::to_string(log_poly_length - i - 1); const auto element_L = transcript->template receive_from_prover("IPA:L_" + index); @@ -393,50 +389,64 @@ template class IPA std::vector round_challenges_inv = round_challenges; Fr::batch_invert(round_challenges_inv); - // populate msm_scalars. for (size_t i = 0; i < log_poly_length; i++) { msm_scalars[2 * i] = round_challenges_inv[i]; msm_scalars[2 * i + 1] = round_challenges[i]; } // Step 5. - // Compute C_zero = C' + ∑_{j ∈ [k]} u_j^{-1}L_j + ∑_{j ∈ [k]} u_jR_j GroupElement LR_sums = scalar_multiplication::pippenger_unsafe( { 0, { &msm_scalars[0], /*size*/ pippenger_size } }, { &msm_elements[0], /*size*/ pippenger_size }); GroupElement C_zero = C_prime + LR_sums; - // Step 6. - // Compute b_zero succinctly + // Step 6. const Fr b_zero = evaluate_challenge_poly(round_challenges_inv, opening_claim.opening_pair.challenge); // Step 7. - // Construct vector s Polynomial s_vec( construct_poly_from_u_challenges_inv(std::span(round_challenges_inv).subspan(0, log_poly_length))); + // Receive G_0 and a_0 from prover (advances transcript; G_0 not recomputed here) + Commitment G_zero_from_prover = transcript->template receive_from_prover("IPA:G_0"); + Fr a_zero = transcript->template receive_from_prover("IPA:a_0"); + + return { C_zero, b_zero, std::move(s_vec), generator_challenge, G_zero_from_prover, a_zero }; + } + + /** + * @brief Natively verify the correctness of a Proof + * + * @tparam Transcript Allows to specify a transcript class. Useful for testing + * @param vk Verification_key containing srs + * @param opening_claim Contains the commitment C and opening pair \f$(\beta, f(\beta))\f$ + * @param transcript Transcript with elements from the prover and generated challenges + * + * @return true/false depending on if the proof verifies + */ + static bool reduce_verify_internal_native(const VK& vk, const OpeningClaim& opening_claim, auto& transcript) + requires(!Curve::is_stdlib_type) + { + // Steps 2–7, 9: Process transcript and extract per-proof data (step 1 done by add_claim_to_hash_buffer) + auto data = read_transcript_data(opening_claim, transcript); + + // Step 8. + // Compute G_s = via SRS MSM and verify against prover's G_0 std::span srs_elements = vk.get_monomial_points(); if (poly_length > srs_elements.size()) { throw_or_abort("potential bug: Not enough SRS points for IPA!"); } - - // Step 8. - // Compute G_zero Commitment G_zero = - scalar_multiplication::pippenger_unsafe(s_vec, { &srs_elements[0], /*size*/ poly_length }); - Commitment G_zero_sent = transcript->template receive_from_prover("IPA:G_0"); - BB_ASSERT_EQ(G_zero, G_zero_sent, "G_0 should be equal to G_0 sent in transcript. IPA verification fails."); - - // Step 9. - // Receive a_zero from the prover - auto a_zero = transcript->template receive_from_prover("IPA:a_0"); + scalar_multiplication::pippenger_unsafe(data.s_vec, { &srs_elements[0], /*size*/ poly_length }); + BB_ASSERT_EQ( + G_zero, data.G_zero_from_prover, "G_0 should be equal to G_0 sent in transcript. IPA verification fails."); // Step 10. - // Compute C_right. Implicitly, this is an IPA statement for the length 1 vectors and together with - // the URS G_0. - GroupElement right_hand_side = G_zero * a_zero + aux_generator * a_zero * b_zero; + // Compute C_right = a_0 * G_s + a_0 * b_0 * U + Commitment aux_generator = Commitment::one() * data.gen_challenge; + GroupElement right_hand_side = G_zero * data.a_zero + aux_generator * data.a_zero * data.b_zero; + // Step 11. - // Check if C_right == C_zero - return (C_zero.normalize() == right_hand_side.normalize()); + return (data.C_zero.normalize() == right_hand_side.normalize()); } /** @@ -594,6 +604,84 @@ template class IPA return reduce_verify_internal_native(vk, opening_claim, transcript); } + /** + * @brief Batch-verify multiple IPA proofs with a single SRS MSM. + * + * @details Processes each proof's transcript independently (cheap per-proof work), + * then combines all proofs using random challenge alpha and performs one large MSM. + * + * @param vk Verification key containing SRS + * @param opening_claims The opening claims for each proof + * @param transcripts The transcripts containing each proof's data + * @return true if all proofs verify + */ + template + static bool batch_reduce_verify(const VK& vk, + const std::vector>& opening_claims, + const std::vector>& transcripts) + requires(!Curve::is_stdlib_type) + { + const size_t num_claims = opening_claims.size(); + BB_ASSERT(num_claims == transcripts.size()); + BB_ASSERT(num_claims > 0); + + // Phase 1: Per-proof transcript processing (sequential, each proof is cheap) + std::vector C_zeros(num_claims); + std::vector a_zeros(num_claims); + std::vector b_zeros(num_claims); + std::vector gen_challenges(num_claims); + std::vector> s_vecs(num_claims); + + for (size_t i = 0; i < num_claims; i++) { + add_claim_to_hash_buffer(opening_claims[i], transcripts[i]); + auto data = read_transcript_data(opening_claims[i], transcripts[i]); + C_zeros[i] = std::move(data.C_zero); + b_zeros[i] = data.b_zero; + s_vecs[i] = std::move(data.s_vec); + gen_challenges[i] = data.gen_challenge; + a_zeros[i] = data.a_zero; + } + + // Phase 2: Batched computation using random challenge alpha + Fr alpha = Fr::random_element(); + std::vector alpha_pows(num_claims); + alpha_pows[0] = Fr::one(); + for (size_t i = 1; i < num_claims; i++) { + alpha_pows[i] = alpha_pows[i - 1] * alpha; + } + + // Combined s_vec: combined_s[j] = \sum \alpha^i * a_zero_i * s_vec_i[j] + Polynomial combined_s(poly_length); + for (size_t i = 0; i < num_claims; i++) { + Fr scalar = alpha_pows[i] * a_zeros[i]; + combined_s.add_scaled(s_vecs[i], scalar); + } + + // Single MSM over combined scalars + std::span srs_elements = vk.get_monomial_points(); + if (poly_length > srs_elements.size()) { + throw_or_abort("potential bug: Not enough SRS points for IPA!"); + } + Commitment G_batch = + scalar_multiplication::pippenger_unsafe(combined_s, { &srs_elements[0], /*size*/ poly_length }); + + // Combined LHS: C_batch = \sum \alpha^i * C_zero_i + GroupElement C_batch = C_zeros[0]; + for (size_t i = 1; i < num_claims; i++) { + C_batch = C_batch + C_zeros[i] * alpha_pows[i]; + } + + // Combined scalar for U terms: bU_scalar = \sum \alpha^i * a_zero_i * b_zero_i * gen_challenge_i + Fr bU_scalar = Fr::zero(); + for (size_t i = 0; i < num_claims; i++) { + bU_scalar += alpha_pows[i] * a_zeros[i] * b_zeros[i] * gen_challenges[i]; + } + + // Check: C_batch == G_batch + bU_scalar * G + GroupElement right_hand_side = G_batch + Commitment::one() * bU_scalar; + return (C_batch.normalize() == right_hand_side.normalize()); + } + /** * @brief Recursively _partially_ verify the correctness of an IPA proof. * diff --git a/yarn-project/aztec-node/src/aztec-node/server.test.ts b/yarn-project/aztec-node/src/aztec-node/server.test.ts index 96119e91cbdc..839e11067471 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -1,4 +1,3 @@ -import { TestCircuitVerifier } from '@aztec/bb-prover'; import { EpochCache } from '@aztec/epoch-cache'; import type { RollupContract } from '@aztec/ethereum/contracts'; import { BlockNumber } from '@aztec/foundation/branded-types'; @@ -188,7 +187,8 @@ describe('aztec node', () => { globalVariablesBuilder, epochCache, getPackageVersion() ?? '', - new TestCircuitVerifier(), + undefined, + undefined, ); }); @@ -589,7 +589,8 @@ describe('aztec node', () => { globalVariablesBuilder, epochCache, getPackageVersion() ?? '', - new TestCircuitVerifier(), + undefined, + undefined, undefined, undefined, undefined, @@ -777,7 +778,8 @@ describe('aztec node', () => { globalVariablesBuilder, epochCache, getPackageVersion() ?? '', - new TestCircuitVerifier(), + undefined, + undefined, undefined, undefined, undefined, diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 203d2b4f866a..212461cb9fde 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -1,5 +1,5 @@ import { Archiver, createArchiver } from '@aztec/archiver'; -import { BBCircuitVerifier, QueuedIVCVerifier, TestCircuitVerifier } from '@aztec/bb-prover'; +import { BatchChonkVerifier, QueuedIVCVerifier } from '@aztec/bb-prover'; import { type BlobClientInterface, createBlobClientWithFileStores } from '@aztec/blob-client/client'; import { Blob } from '@aztec/blob-lib'; import { ARCHIVE_HEIGHT, type L1_TO_L2_MSG_TREE_HEIGHT, type NOTE_HASH_TREE_HEIGHT } from '@aztec/constants'; @@ -150,7 +150,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { protected readonly globalVariableBuilder: GlobalVariableBuilderInterface, protected readonly epochCache: EpochCacheInterface, protected readonly packageVersion: string, - private proofVerifier: ClientProtocolCircuitVerifier, + private peerChonkVerifier: ClientProtocolCircuitVerifier | undefined, + private rpcChonkVerifier: ClientProtocolCircuitVerifier | undefined, private telemetry: TelemetryClient = getTelemetryClient(), private log = createLogger('node'), private blobClient?: BlobClientInterface, @@ -306,10 +307,14 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { options.prefilledPublicData, telemetry, ); - const circuitVerifier = - config.realProofs || config.debugForceTxProofVerification - ? await BBCircuitVerifier.new(config) - : new TestCircuitVerifier(config.proverTestVerificationDelayMs); + const useRealVerifiers = config.realProofs || config.debugForceTxProofVerification; + let peerChonkVerifier: ClientProtocolCircuitVerifier | undefined; + let rpcChonkVerifier: ClientProtocolCircuitVerifier | undefined; + if (useRealVerifiers) { + peerChonkVerifier = await BatchChonkVerifier.new(config, telemetry, config.bbPeerVerifyBatchSize, 'peer'); + const rpcBatchVerifier = await BatchChonkVerifier.new(config, telemetry, 1, 'rpc'); + rpcChonkVerifier = new QueuedIVCVerifier(rpcBatchVerifier, config.bbRpcVerifyBatchSize, telemetry); + } let debugLogStore: DebugLogStore; if (!config.realProofs) { @@ -323,8 +328,6 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { debugLogStore = new NullDebugLogStore(); } - const proofVerifier = new QueuedIVCVerifier(config, circuitVerifier); - const proverOnly = config.enableProverNode && config.disableValidator; if (proverOnly) { log.info('Starting in prover-only mode: skipping validator, sequencer, sentinel, and slasher subsystems'); @@ -334,7 +337,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { const p2pClient = await createP2PClient( config, archiver, - proofVerifier, + peerChonkVerifier, worldStateSynchronizer, epochCache, packageVersion, @@ -575,7 +578,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { globalVariableBuilder, epochCache, packageVersion, - proofVerifier, + peerChonkVerifier, + rpcChonkVerifier, telemetry, log, blobClient, @@ -914,7 +918,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { await tryStop(this.validatorsSentinel); await tryStop(this.epochPruneWatcher); await tryStop(this.slasherClient); - await tryStop(this.proofVerifier); + await Promise.all([tryStop(this.peerChonkVerifier), tryStop(this.rpcChonkVerifier)]); await tryStop(this.sequencer); await tryStop(this.proverNode); await tryStop(this.p2pClient); @@ -1323,7 +1327,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { { isSimulation, skipFeeEnforcement }: { isSimulation?: boolean; skipFeeEnforcement?: boolean } = {}, ): Promise { const db = this.worldStateSynchronizer.getCommitted(); - const verifier = isSimulation ? undefined : this.proofVerifier; + const verifier = isSimulation ? undefined : this.rpcChonkVerifier; // We accept transactions if they are not expired by the next slot (checked based on the ExpirationTimestamp field) const { ts: nextSlotTimestamp } = this.epochCache.getEpochAndSlotInNextL1Slot(); @@ -1372,7 +1376,20 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { archiver.updateConfig(config); } if (newConfig.realProofs !== this.config.realProofs) { - this.proofVerifier = config.realProofs ? await BBCircuitVerifier.new(newConfig) : new TestCircuitVerifier(); + // TODO: handle dynamic config change for split verifiers + if (config.realProofs) { + this.peerChonkVerifier = await BatchChonkVerifier.new( + newConfig, + this.telemetry, + newConfig.bbPeerVerifyBatchSize, + 'peer', + ); + const rpcBatch = await BatchChonkVerifier.new(newConfig, this.telemetry, 1, 'rpc'); + this.rpcChonkVerifier = new QueuedIVCVerifier(rpcBatch, newConfig.bbRpcVerifyBatchSize, this.telemetry); + } else { + this.peerChonkVerifier = undefined; + this.rpcChonkVerifier = undefined; + } } this.config = newConfig; diff --git a/yarn-project/bb-prover/package.json b/yarn-project/bb-prover/package.json index 04774fdf6ea8..5e08beeba10c 100644 --- a/yarn-project/bb-prover/package.json +++ b/yarn-project/bb-prover/package.json @@ -80,6 +80,7 @@ "@aztec/telemetry-client": "workspace:^", "@aztec/world-state": "workspace:^", "commander": "^12.1.0", + "msgpackr": "^1.11.2", "pako": "^2.1.0", "source-map-support": "^0.5.21", "tslib": "^2.4.0" diff --git a/yarn-project/bb-prover/src/config.ts b/yarn-project/bb-prover/src/config.ts index 60a33c9a67b6..3f778ea4e72d 100644 --- a/yarn-project/bb-prover/src/config.ts +++ b/yarn-project/bb-prover/src/config.ts @@ -5,6 +5,10 @@ export interface BBConfig { bbSkipCleanup: boolean; numConcurrentIVCVerifiers: number; bbIVCConcurrency: number; + /** Batch size for RPC proof verification (QueuedIVCVerifier concurrency). */ + bbRpcVerifyBatchSize: number; + /** Batch size for P2P peer proof verification (BatchChonkVerifier batch). */ + bbPeerVerifyBatchSize: number; } export interface ACVMConfig { diff --git a/yarn-project/bb-prover/src/verifier/batch_chonk_verifier.ts b/yarn-project/bb-prover/src/verifier/batch_chonk_verifier.ts new file mode 100644 index 000000000000..9d4dc84c5526 --- /dev/null +++ b/yarn-project/bb-prover/src/verifier/batch_chonk_verifier.ts @@ -0,0 +1,346 @@ +import { BackendType, Barretenberg } from '@aztec/bb.js'; +import { createLogger } from '@aztec/foundation/log'; +import { SerialQueue } from '@aztec/foundation/queue'; +import { Timer } from '@aztec/foundation/timer'; +import { ProtocolCircuitVks } from '@aztec/noir-protocol-circuits-types/server/vks'; +import type { ClientProtocolCircuitVerifier, IVCProofVerificationResult } from '@aztec/stdlib/interfaces/server'; +import type { Tx } from '@aztec/stdlib/tx'; +import { + Attributes, + type BatchObservableResult, + type Histogram, + Metrics, + type ObservableGauge, + type TelemetryClient, + type UpDownCounter, + createUpDownCounterWithDefault, + getTelemetryClient, +} from '@aztec/telemetry-client'; + +import { Unpackr } from 'msgpackr'; +import { execSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { createHistogram } from 'node:perf_hooks'; + +import type { BBConfig } from '../config.js'; + +/** Result from the FIFO, matching the C++ VerifyResult struct. */ +interface FifoVerifyResult { + request_id: number; + status: number; + error_message: string; + time_in_queue_ms: number; + time_in_verify_ms: number; + batch_failure_count: number; +} + +/** Maps client protocol artifacts used for chonk verification to VK indices. */ +const CHONK_VK_ARTIFACTS = ['HidingKernelToRollup', 'HidingKernelToPublic'] as const; + +class BatchVerifierMetrics { + private ivcVerificationHistogram: Histogram; + private ivcTotalVerificationHistogram: Histogram; + private ivcFailureCount: UpDownCounter; + private queueDepth: ObservableGauge; + private localHistogramOk = createHistogram({ min: 1, max: 5 * 60 * 1000 }); + private localHistogramFails = createHistogram({ min: 1, max: 5 * 60 * 1000 }); + private aggDurationMetrics: Record<'min' | 'max' | 'p50' | 'p90' | 'avg', ObservableGauge>; + private currentQueueDepth = 0; + + constructor(client: TelemetryClient, name = 'BatchChonkVerifier') { + const meter = client.getMeter(name); + this.ivcVerificationHistogram = meter.createHistogram(Metrics.IVC_VERIFIER_TIME); + this.ivcTotalVerificationHistogram = meter.createHistogram(Metrics.IVC_VERIFIER_TOTAL_TIME); + this.ivcFailureCount = createUpDownCounterWithDefault(meter, Metrics.IVC_VERIFIER_FAILURE_COUNT); + this.queueDepth = meter.createObservableGauge(Metrics.IVC_VERIFIER_QUEUE_DEPTH); + this.queueDepth.addCallback(res => res.observe(this.currentQueueDepth)); + this.aggDurationMetrics = { + avg: meter.createObservableGauge(Metrics.IVC_VERIFIER_AGG_DURATION_AVG), + max: meter.createObservableGauge(Metrics.IVC_VERIFIER_AGG_DURATION_MAX), + min: meter.createObservableGauge(Metrics.IVC_VERIFIER_AGG_DURATION_MIN), + p50: meter.createObservableGauge(Metrics.IVC_VERIFIER_AGG_DURATION_P50), + p90: meter.createObservableGauge(Metrics.IVC_VERIFIER_AGG_DURATION_P90), + }; + meter.addBatchObservableCallback(this.aggregate, Object.values(this.aggDurationMetrics)); + } + + updateQueueDepth(depth: number) { + this.currentQueueDepth = depth; + } + + recordIVCVerification(result: IVCProofVerificationResult) { + this.ivcVerificationHistogram.record(Math.ceil(result.durationMs), { [Attributes.OK]: result.valid }); + this.ivcTotalVerificationHistogram.record(Math.ceil(result.totalDurationMs), { [Attributes.OK]: result.valid }); + if (!result.valid) { + this.ivcFailureCount.add(1); + this.localHistogramFails.record(Math.max(Math.ceil(result.durationMs), 1)); + } else { + this.localHistogramOk.record(Math.max(Math.ceil(result.durationMs), 1)); + } + } + + private aggregate = (res: BatchObservableResult) => { + for (const [histogram, ok] of [ + [this.localHistogramOk, true], + [this.localHistogramFails, false], + ] as const) { + if (histogram.count === 0) { + continue; + } + res.observe(this.aggDurationMetrics.avg, histogram.mean, { [Attributes.OK]: ok }); + res.observe(this.aggDurationMetrics.max, histogram.max, { [Attributes.OK]: ok }); + res.observe(this.aggDurationMetrics.min, histogram.min, { [Attributes.OK]: ok }); + res.observe(this.aggDurationMetrics.p50, histogram.percentile(50), { [Attributes.OK]: ok }); + res.observe(this.aggDurationMetrics.p90, histogram.percentile(90), { [Attributes.OK]: ok }); + } + }; +} + +interface PendingRequest { + resolve: (result: IVCProofVerificationResult) => void; + reject: (error: Error) => void; + totalTimer: Timer; +} + +/** + * Batch verifier for Chonk IVC proofs. Uses the bb batch verifier service + * which batches IPA verification into a single SRS MSM for better throughput. + * + * Architecture: + * - Spawns a persistent `bb msgpack run` process via Barretenberg (native backend) + * - Sends proofs via the msgpack RPC protocol (ChonkBatchVerifierQueue) + * - Receives results via a named FIFO pipe (async, out-of-order) + * - Bisects batch failures to isolate individual bad proofs + */ +export class BatchChonkVerifier implements ClientProtocolCircuitVerifier { + private bb!: Barretenberg; + private fifoPath: string; + private nextRequestId = 0; + private pendingRequests = new Map(); + private sendQueue: SerialQueue; + private fifoStream: fs.ReadStream | null = null; + private fifoReaderRunning = false; + private metrics: BatchVerifierMetrics; + private logger = createLogger('bb-prover:batch_chonk_verifier'); + /** Maps artifact name to VK index in the batch verifier. */ + private vkIndexMap = new Map(); + + private constructor( + private config: BBConfig, + private batchSize: number, + private label: string, + telemetry: TelemetryClient, + ) { + this.fifoPath = path.join(os.tmpdir(), `bb-batch-${label}-${process.pid}-${Date.now()}.fifo`); + this.metrics = new BatchVerifierMetrics(telemetry); + this.sendQueue = new SerialQueue(); + this.sendQueue.start(1); + } + + /** + * Create and start a new BatchChonkVerifier. + * @param config - BB binary configuration. + * @param telemetry - Telemetry client for metrics. + * @param batchSize - Max proofs per batch. + * @param label - Descriptive label for FIFO path and logging (e.g. 'peer', 'rpc'). + */ + static async new( + config: BBConfig, + telemetry: TelemetryClient = getTelemetryClient(), + batchSize = 8, + label = 'verifier', + ): Promise { + const verifier = new BatchChonkVerifier(config, batchSize, label, telemetry); + await verifier.start(); + return verifier; + } + + private async start(): Promise { + this.logger.info('Starting BatchChonkVerifier'); + + // Force native backend -- batch verification is not supported in WASM + this.bb = await Barretenberg.new({ + bbPath: this.config.bbBinaryPath, + backend: BackendType.NativeUnixSocket, + }); + await this.bb.initSRSChonk(); + + // Collect VKs for all chonk-verifiable circuits + const vkBuffers: Uint8Array[] = []; + for (const artifact of CHONK_VK_ARTIFACTS) { + const vk = ProtocolCircuitVks[artifact]; + if (!vk) { + throw new Error(`Missing VK for ${artifact}`); + } + this.vkIndexMap.set(artifact, vkBuffers.length); + vkBuffers.push(vk.keyAsBytes); + } + + // Create FIFO pipe for async result delivery + execSync(`mkfifo ${this.fifoPath}`); + + // Start the batch verifier service in bb + await this.bb.chonkBatchVerifierStart({ + vks: vkBuffers, + numCores: this.config.bbIVCConcurrency || 0, + batchSize: this.batchSize, + fifoPath: this.fifoPath, + }); + + // Start FIFO reader (must happen after service start, since bb opens FIFO for writing) + this.startFifoReader(); + + this.logger.info('BatchChonkVerifier started', { fifoPath: this.fifoPath }); + } + + public verifyProof(tx: Tx): Promise { + const totalTimer = new Timer(); + const requestId = this.nextRequestId++; + const circuit = tx.data.forPublic ? 'HidingKernelToPublic' : 'HidingKernelToRollup'; + const vkIndex = this.vkIndexMap.get(circuit); + if (vkIndex === undefined) { + throw new Error(`No VK index for circuit ${circuit}`); + } + + // Attach public inputs to get the flat proof fields array (C++ splits into ChonkProof segments) + const proofWithPubInputs = tx.chonkProof.attachPublicInputs(tx.data.publicInputs().toFields()); + const proofFields = proofWithPubInputs.fieldsWithPublicInputs.map(f => f.toBuffer()); + + // Create pending promise + const resultPromise = new Promise((resolve, reject) => { + this.pendingRequests.set(requestId, { resolve, reject, totalTimer }); + }); + + // Enqueue via the serial send queue (bb pipe is single-request) + void this.sendQueue.put(async () => { + await this.bb.chonkBatchVerifierQueue({ + requestId, + vkIndex, + proofFields, + }); + }); + + return resultPromise; + } + + public async stop(): Promise { + this.logger.info('Stopping BatchChonkVerifier'); + + // Stop accepting new proofs + await this.sendQueue.end(); + + // Stop the bb service (flushes remaining proofs) + try { + await this.bb.chonkBatchVerifierStop({}); + } catch (err) { + this.logger.warn(`Error stopping batch verifier service: ${err}`); + } + + // Stop FIFO reader + this.fifoReaderRunning = false; + if (this.fifoStream) { + this.fifoStream.destroy(); + this.fifoStream = null; + } + + // Clean up FIFO file + try { + fs.unlinkSync(this.fifoPath); + } catch { + // ignore + } + + // Reject any remaining pending requests + for (const [id, pending] of this.pendingRequests) { + pending.reject(new Error('BatchChonkVerifier stopped')); + this.pendingRequests.delete(id); + } + + // Destroy bb process + await this.bb.destroy(); + + this.logger.info('BatchChonkVerifier stopped'); + } + + private startFifoReader(): void { + this.fifoReaderRunning = true; + const unpackr = new Unpackr({ useRecords: false }); + + const stream = fs.createReadStream(this.fifoPath, { highWaterMark: 64 * 1024 }); + this.fifoStream = stream; + + // State machine for parsing length-delimited msgpack frames + let pendingBuf: Buffer = Buffer.alloc(0); + + stream.on('data', (chunk: Buffer | string) => { + const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + pendingBuf = pendingBuf.length > 0 ? Buffer.concat([pendingBuf, buf]) : buf; + + // Process all complete frames in the buffer + while (pendingBuf.length >= 4) { + const payloadLen = pendingBuf.readUInt32BE(0); + if (payloadLen === 0 || payloadLen > 10 * 1024 * 1024) { + this.logger.warn(`FIFO: invalid payload length ${payloadLen}`); + stream.destroy(); + return; + } + + const frameLen = 4 + payloadLen; + if (pendingBuf.length < frameLen) { + break; // Wait for more data + } + + const payloadBuf = pendingBuf.subarray(4, frameLen); + pendingBuf = pendingBuf.subarray(frameLen); + + try { + const result = unpackr.unpack(payloadBuf) as FifoVerifyResult; + this.handleResult(result); + } catch (err) { + this.logger.error(`FIFO: failed to decode msgpack result: ${err}`); + } + } + }); + + stream.on('error', (err: Error) => { + if (this.fifoReaderRunning) { + this.logger.error(`FIFO reader error: ${err}`); + } + }); + + stream.on('end', () => { + this.logger.debug('FIFO reader: stream ended'); + }); + } + + private handleResult(result: FifoVerifyResult): void { + const pending = this.pendingRequests.get(result.request_id); + if (!pending) { + this.logger.warn(`Received result for unknown request_id=${result.request_id}`); + return; + } + this.pendingRequests.delete(result.request_id); + + const valid = result.status === 0; // VerifyStatus::OK + const durationMs = result.time_in_verify_ms; + const totalDurationMs = pending.totalTimer.ms(); + + const ivcResult: IVCProofVerificationResult = { valid, durationMs, totalDurationMs }; + this.metrics.recordIVCVerification(ivcResult); + this.metrics.updateQueueDepth(this.pendingRequests.size); + + if (!valid) { + this.logger.warn(`Proof verification failed for request_id=${result.request_id}: ${result.error_message}`); + } else { + this.logger.debug(`Proof verified`, { + requestId: result.request_id, + durationMs: Math.ceil(durationMs), + totalDurationMs: Math.ceil(totalDurationMs), + }); + } + + pending.resolve(ivcResult); + } +} diff --git a/yarn-project/bb-prover/src/verifier/index.ts b/yarn-project/bb-prover/src/verifier/index.ts index 1ce2f3a7e41a..229e41274d6e 100644 --- a/yarn-project/bb-prover/src/verifier/index.ts +++ b/yarn-project/bb-prover/src/verifier/index.ts @@ -1,2 +1,3 @@ +export * from './batch_chonk_verifier.js'; export * from './bb_verifier.js'; export * from './queued_chonk_verifier.js'; diff --git a/yarn-project/bb-prover/src/verifier/queued_chonk_verifier.ts b/yarn-project/bb-prover/src/verifier/queued_chonk_verifier.ts index 4eeca0d042e5..dc1faab41cc7 100644 --- a/yarn-project/bb-prover/src/verifier/queued_chonk_verifier.ts +++ b/yarn-project/bb-prover/src/verifier/queued_chonk_verifier.ts @@ -16,8 +16,6 @@ import { import { createHistogram } from 'node:perf_hooks'; -import type { BBConfig } from '../config.js'; - class IVCVerifierMetrics { private ivcVerificationHistogram: Histogram; private ivcTotalVerificationHistogram: Histogram; @@ -86,15 +84,15 @@ export class QueuedIVCVerifier implements ClientProtocolCircuitVerifier { private metrics: IVCVerifierMetrics; public constructor( - config: BBConfig, private verifier: ClientProtocolCircuitVerifier, + concurrency: number, private telemetry: TelemetryClient = getTelemetryClient(), private logger = createLogger('bb-prover:queued_chonk_verifier'), ) { this.metrics = new IVCVerifierMetrics(this.telemetry, 'QueuedIVCVerifier'); this.queue = new SerialQueue(); - this.logger.info(`Starting QueuedIVCVerifier with ${config.numConcurrentIVCVerifiers} concurrent verifiers`); - this.queue.start(config.numConcurrentIVCVerifiers); + this.logger.info(`Starting QueuedIVCVerifier with ${concurrency} concurrent verifiers`); + this.queue.start(concurrency); } public async verifyProof(tx: Tx): Promise { diff --git a/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts b/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts index 59f38bfeedea..b08fabfcb2fd 100644 --- a/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts +++ b/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts @@ -171,7 +171,7 @@ export class FullProverTest { await Barretenberg.initSingleton({ backend: BackendType.NativeUnixSocket }); const verifier = await BBCircuitVerifier.new(bbConfig); - this.circuitProofVerifier = new QueuedIVCVerifier(bbConfig, verifier); + this.circuitProofVerifier = new QueuedIVCVerifier(verifier, bbConfig.bbRpcVerifyBatchSize); this.logger.debug(`Configuring the node for real proofs...`); await this.aztecNodeAdmin.setConfig({ diff --git a/yarn-project/end-to-end/src/fixtures/get_bb_config.ts b/yarn-project/end-to-end/src/fixtures/get_bb_config.ts index 9ede6ef6a1e7..7b17008baca3 100644 --- a/yarn-project/end-to-end/src/fixtures/get_bb_config.ts +++ b/yarn-project/end-to-end/src/fixtures/get_bb_config.ts @@ -15,6 +15,8 @@ const { BB_WORKING_DIRECTORY = '', BB_NUM_IVC_VERIFIERS = '1', BB_IVC_CONCURRENCY = '1', + BB_RPC_VERIFY_BATCH_SIZE, + BB_PEER_VERIFY_BATCH_SIZE, } = process.env; export const getBBConfig = async ( @@ -51,6 +53,8 @@ export const getBBConfig = async ( cleanup, numConcurrentIVCVerifiers: numIvcVerifiers, bbIVCConcurrency: ivcConcurrency, + bbRpcVerifyBatchSize: BB_RPC_VERIFY_BATCH_SIZE ? Number(BB_RPC_VERIFY_BATCH_SIZE) : numIvcVerifiers, + bbPeerVerifyBatchSize: BB_PEER_VERIFY_BATCH_SIZE ? Number(BB_PEER_VERIFY_BATCH_SIZE) : numIvcVerifiers, }; } catch (err) { logger.error(`Native BB not available, error: ${err}`); diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index a547219e839c..2431af691a5e 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -22,6 +22,8 @@ export type EnvVar = | 'BB_SKIP_CLEANUP' | 'BB_WORKING_DIRECTORY' | 'BB_NUM_IVC_VERIFIERS' + | 'BB_RPC_VERIFY_BATCH_SIZE' + | 'BB_PEER_VERIFY_BATCH_SIZE' | 'BB_IVC_CONCURRENCY' | 'BOOTSTRAP_NODES' | 'BLOB_ARCHIVE_API_URL' diff --git a/yarn-project/p2p/src/client/factory.ts b/yarn-project/p2p/src/client/factory.ts index 78878c6caf20..12a3766630b7 100644 --- a/yarn-project/p2p/src/client/factory.ts +++ b/yarn-project/p2p/src/client/factory.ts @@ -44,7 +44,7 @@ export const P2P_ATTESTATION_STORE_NAME = 'p2p-attestation'; export async function createP2PClient( inputConfig: P2PConfig & DataStoreConfig & ChainConfig, archiver: L2BlockSource & ContractDataSource, - proofVerifier: ClientProtocolCircuitVerifier, + proofVerifier: ClientProtocolCircuitVerifier | undefined, worldStateSynchronizer: WorldStateSynchronizer, epochCache: EpochCacheInterface, packageVersion: string, @@ -190,7 +190,7 @@ export async function createP2PClient( async function createP2PService( config: P2PConfig & DataStoreConfig, archiver: L2BlockSource & ContractDataSource, - proofVerifier: ClientProtocolCircuitVerifier, + proofVerifier: ClientProtocolCircuitVerifier | undefined, worldStateSynchronizer: WorldStateSynchronizer, epochCache: EpochCacheInterface, store: AztecAsyncKVStore, diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts index 9acd13d88c3d..71ebc9f17023 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts @@ -178,9 +178,12 @@ export function createFirstStageTxValidationsForGossipedTransactions( * (e.g., duplicates, insufficient balance, pool full). */ export function createSecondStageTxValidationsForGossipedTransactions( - proofVerifier: ClientProtocolCircuitVerifier, + proofVerifier: ClientProtocolCircuitVerifier | undefined, bindings?: LoggerBindings, ): Record { + if (!proofVerifier) { + return {}; + } return { proofValidator: { validator: new TxProofValidator(proofVerifier, bindings), @@ -196,7 +199,7 @@ export function createSecondStageTxValidationsForGossipedTransactions( * caught later by the block building validator. */ function createTxValidatorForMinimumTxIntegrityChecks( - verifier: ClientProtocolCircuitVerifier, + verifier: ClientProtocolCircuitVerifier | undefined, { l1ChainId, rollupVersion, @@ -206,7 +209,7 @@ function createTxValidatorForMinimumTxIntegrityChecks( }, bindings?: LoggerBindings, ): TxValidator { - return new AggregateTxValidator( + const validators: TxValidator[] = [ new MetadataTxValidator( { l1ChainId: new Fr(l1ChainId), @@ -218,8 +221,11 @@ function createTxValidatorForMinimumTxIntegrityChecks( ), new SizeTxValidator(bindings), new DataTxValidator(bindings), - new TxProofValidator(verifier, bindings), - ); + ]; + if (verifier) { + validators.push(new TxProofValidator(verifier, bindings)); + } + return new AggregateTxValidator(...validators); } /** @@ -229,7 +235,7 @@ function createTxValidatorForMinimumTxIntegrityChecks( * enters the pending pool or during block building. */ export function createTxValidatorForReqResponseReceivedTxs( - verifier: ClientProtocolCircuitVerifier, + verifier: ClientProtocolCircuitVerifier | undefined, { l1ChainId, rollupVersion, @@ -248,7 +254,7 @@ export function createTxValidatorForReqResponseReceivedTxs( * re-execution; their validity against state is checked during block building. */ export function createTxValidatorForBlockProposalReceivedTxs( - verifier: ClientProtocolCircuitVerifier, + verifier: ClientProtocolCircuitVerifier | undefined, { l1ChainId, rollupVersion, diff --git a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts index 061263fb20c7..3ef207c00349 100644 --- a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts +++ b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts @@ -191,7 +191,7 @@ export class LibP2PService extends WithTracer implements P2PService { protected mempools: MemPools, protected archiver: L2BlockSource & ContractDataSource, private epochCache: EpochCacheInterface, - private proofVerifier: ClientProtocolCircuitVerifier, + private proofVerifier: ClientProtocolCircuitVerifier | undefined, private worldStateSynchronizer: WorldStateSynchronizer, telemetry: TelemetryClient, logger: Logger = createLogger('p2p:libp2p_service'), @@ -271,7 +271,7 @@ export class LibP2PService extends WithTracer implements P2PService { mempools: MemPools; l2BlockSource: L2BlockSource & ContractDataSource; epochCache: EpochCacheInterface; - proofVerifier: ClientProtocolCircuitVerifier; + proofVerifier: ClientProtocolCircuitVerifier | undefined; worldStateSynchronizer: WorldStateSynchronizer; peerStore: AztecAsyncKVStore; telemetry: TelemetryClient; diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/tx_validator.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/tx_validator.ts index 3f79866a31c9..ae2aa93dd0c8 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/tx_validator.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/tx_validator.ts @@ -6,7 +6,7 @@ import { createTxValidatorForReqResponseReceivedTxs } from '../../../msg_validat export interface BatchRequestTxValidatorConfig { l1ChainId: number; rollupVersion: number; - proofVerifier: ClientProtocolCircuitVerifier; + proofVerifier: ClientProtocolCircuitVerifier | undefined; } export interface IBatchRequestTxValidator { diff --git a/yarn-project/p2p/src/test-helpers/mock-pubsub.ts b/yarn-project/p2p/src/test-helpers/mock-pubsub.ts index cf48654e0aff..b4bd767b0199 100644 --- a/yarn-project/p2p/src/test-helpers/mock-pubsub.ts +++ b/yarn-project/p2p/src/test-helpers/mock-pubsub.ts @@ -52,7 +52,7 @@ export function getMockPubSubP2PServiceFactory( mempools: MemPools; l2BlockSource: L2BlockSource & ContractDataSource; epochCache: EpochCacheInterface; - proofVerifier: ClientProtocolCircuitVerifier; + proofVerifier: ClientProtocolCircuitVerifier | undefined; worldStateSynchronizer: WorldStateSynchronizer; peerStore: AztecAsyncKVStore; telemetry: TelemetryClient; diff --git a/yarn-project/prover-client/src/config.ts b/yarn-project/prover-client/src/config.ts index 658558f9eb44..878e9dd27a13 100644 --- a/yarn-project/prover-client/src/config.ts +++ b/yarn-project/prover-client/src/config.ts @@ -52,6 +52,18 @@ export const bbConfigMappings: ConfigMappingsType = { description: 'Number of threads to use for IVC verification', ...numberConfigHelper(1), }, + bbRpcVerifyBatchSize: { + env: 'BB_RPC_VERIFY_BATCH_SIZE', + description: 'Batch size for RPC proof verification (QueuedIVCVerifier concurrency)', + parseEnv: (val: string) => (val ? Number(val) : Number(process.env.BB_NUM_IVC_VERIFIERS ?? '8')), + defaultValue: Number(process.env.BB_NUM_IVC_VERIFIERS ?? '8'), + }, + bbPeerVerifyBatchSize: { + env: 'BB_PEER_VERIFY_BATCH_SIZE', + description: 'Batch size for P2P peer proof verification (BatchChonkVerifier batch)', + parseEnv: (val: string) => (val ? Number(val) : Number(process.env.BB_NUM_IVC_VERIFIERS ?? '8')), + defaultValue: Number(process.env.BB_NUM_IVC_VERIFIERS ?? '8'), + }, }; export const proverClientConfigMappings: ConfigMappingsType = { diff --git a/yarn-project/prover-client/src/mocks/test_context.ts b/yarn-project/prover-client/src/mocks/test_context.ts index 78f9301c39de..f41d4639dcf5 100644 --- a/yarn-project/prover-client/src/mocks/test_context.ts +++ b/yarn-project/prover-client/src/mocks/test_context.ts @@ -106,6 +106,8 @@ export class TestContext { bbSkipCleanup: config.bbSkipCleanup, numConcurrentIVCVerifiers: 2, bbIVCConcurrency: 1, + bbRpcVerifyBatchSize: 8, + bbPeerVerifyBatchSize: 8, }; localProver = await createProver(bbConfig); } diff --git a/yarn-project/telemetry-client/src/metrics.ts b/yarn-project/telemetry-client/src/metrics.ts index a2985f0c337a..fae743820b57 100644 --- a/yarn-project/telemetry-client/src/metrics.ts +++ b/yarn-project/telemetry-client/src/metrics.ts @@ -1518,3 +1518,10 @@ export const IVC_VERIFIER_AGG_DURATION_AVG: MetricDefinition = { unit: 'ms', valueType: ValueType.DOUBLE, }; + +export const IVC_VERIFIER_QUEUE_DEPTH: MetricDefinition = { + name: 'aztec.ivc_verifier.queue_depth', + description: 'Number of proofs pending verification in the batch verifier', + unit: '{proofs}', + valueType: ValueType.INT, +}; diff --git a/yarn-project/txe/src/state_machine/index.ts b/yarn-project/txe/src/state_machine/index.ts index cf7884c24ff4..ee2b156ba191 100644 --- a/yarn-project/txe/src/state_machine/index.ts +++ b/yarn-project/txe/src/state_machine/index.ts @@ -1,5 +1,4 @@ import { type AztecNodeConfig, AztecNodeService } from '@aztec/aztec-node'; -import { TestCircuitVerifier } from '@aztec/bb-prover/test'; import { CheckpointNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { createLogger } from '@aztec/foundation/log'; @@ -56,7 +55,8 @@ export class TXEStateMachine { new TXEGlobalVariablesBuilder(), new MockEpochCache(), getPackageVersion() ?? '', - new TestCircuitVerifier(), + undefined, + undefined, undefined, log, ); diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 61130cb43aa6..dd5b9f7ada8b 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -956,6 +956,7 @@ __metadata: commander: "npm:^12.1.0" jest: "npm:^30.0.0" jest-mock-extended: "npm:^4.0.0" + msgpackr: "npm:^1.11.2" pako: "npm:^2.1.0" source-map-support: "npm:^0.5.21" ts-node: "npm:^10.9.1" @@ -1671,8 +1672,8 @@ __metadata: version: 0.0.0-use.local resolution: "@aztec/noir-noir_codegen@portal:../noir/packages/noir_codegen::locator=%40aztec%2Faztec3-packages%40workspace%3A." dependencies: - "@aztec/noir-types": "npm:1.0.0-beta.18" - glob: "npm:^13.0.0" + "@aztec/noir-types": "npm:1.0.0-beta.19" + glob: "npm:^13.0.6" ts-command-line-args: "npm:^2.5.1" bin: noir-codegen: lib/main.js @@ -1680,14 +1681,14 @@ __metadata: linkType: soft "@aztec/noir-noir_js@file:../noir/packages/noir_js::locator=%40aztec%2Faztec3-packages%40workspace%3A.": - version: 1.0.0-beta.18 - resolution: "@aztec/noir-noir_js@file:../noir/packages/noir_js#../noir/packages/noir_js::hash=e37c31&locator=%40aztec%2Faztec3-packages%40workspace%3A." + version: 1.0.0-beta.19 + resolution: "@aztec/noir-noir_js@file:../noir/packages/noir_js#../noir/packages/noir_js::hash=c16cdc&locator=%40aztec%2Faztec3-packages%40workspace%3A." dependencies: - "@aztec/noir-acvm_js": "npm:1.0.0-beta.18" - "@aztec/noir-noirc_abi": "npm:1.0.0-beta.18" - "@aztec/noir-types": "npm:1.0.0-beta.18" + "@aztec/noir-acvm_js": "npm:1.0.0-beta.19" + "@aztec/noir-noirc_abi": "npm:1.0.0-beta.19" + "@aztec/noir-types": "npm:1.0.0-beta.19" pako: "npm:^2.1.0" - checksum: 10/77cbcdb9a2513f9a48aef0f017923cc54625062b54b40d7e1947c4be6023a30f9db520926a2e345e865700dac842a47ab2c66b3ffd8eee2f7071300b66fce460 + checksum: 10/68fc1ab58b0a51ae30d56b933d43c6fa3b702e53759e9387ce4e4c63a6e6730ebae9214e79bb3addcca9f512274871998a1777a19c848443474835518f5185b4 languageName: node linkType: hard @@ -1695,7 +1696,7 @@ __metadata: version: 0.0.0-use.local resolution: "@aztec/noir-noirc_abi@portal:../noir/packages/noirc_abi::locator=%40aztec%2Faztec3-packages%40workspace%3A." dependencies: - "@aztec/noir-types": "npm:1.0.0-beta.18" + "@aztec/noir-types": "npm:1.0.0-beta.19" languageName: node linkType: soft @@ -13854,14 +13855,14 @@ __metadata: languageName: node linkType: hard -"glob@npm:^13.0.0": - version: 13.0.0 - resolution: "glob@npm:13.0.0" +"glob@npm:^13.0.6": + version: 13.0.6 + resolution: "glob@npm:13.0.6" dependencies: - minimatch: "npm:^10.1.1" - minipass: "npm:^7.1.2" - path-scurry: "npm:^2.0.0" - checksum: 10/de390721d29ee1c9ea41e40ec2aa0de2cabafa68022e237dc4297665a5e4d650776f2573191984ea1640aba1bf0ea34eddef2d8cbfbfc2ad24b5fb0af41d8846 + minimatch: "npm:^10.2.2" + minipass: "npm:^7.1.3" + path-scurry: "npm:^2.0.2" + checksum: 10/201ad69e5f0aa74e1d8c00a481581f8b8c804b6a4fbfabeeb8541f5d756932800331daeba99b58fb9e4cd67e12ba5a7eba5b82fb476691588418060b84353214 languageName: node linkType: hard @@ -16978,7 +16979,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.1.1, minimatch@npm:^9.0.3 || ^10.0.1": +"minimatch@npm:^10.1.1, minimatch@npm:^10.2.2, minimatch@npm:^9.0.3 || ^10.0.1": version: 10.2.4 resolution: "minimatch@npm:10.2.4" dependencies: @@ -17097,6 +17098,13 @@ __metadata: languageName: node linkType: hard +"minipass@npm:^7.1.3": + version: 7.1.3 + resolution: "minipass@npm:7.1.3" + checksum: 10/175e4d5e20980c3cd316ae82d2c031c42f6c746467d8b1905b51060a0ba4461441a0c25bb67c025fd9617f9a3873e152c7b543c6b5ac83a1846be8ade80dffd6 + languageName: node + linkType: hard + "minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": version: 2.1.2 resolution: "minizlib@npm:2.1.2" @@ -18253,6 +18261,16 @@ __metadata: languageName: node linkType: hard +"path-scurry@npm:^2.0.2": + version: 2.0.2 + resolution: "path-scurry@npm:2.0.2" + dependencies: + lru-cache: "npm:^11.0.0" + minipass: "npm:^7.1.2" + checksum: 10/2b4257422bcb870a4c2d205b3acdbb213a72f5e2250f61c80f79c9d014d010f82bdf8584441612c8e1fa4eb098678f5704a66fa8377d72646bad4be38e57a2c3 + languageName: node + linkType: hard + "path-to-regexp@npm:0.1.12": version: 0.1.12 resolution: "path-to-regexp@npm:0.1.12"