Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
chonk_batch_verifier.test.cpp
Go to the documentation of this file.
1#ifndef __wasm__
7
8#include <algorithm>
9#include <atomic>
10#include <condition_variable>
11#include <mutex>
12#include <numeric>
13#include <random>
14#include <set>
15#include <stdexcept>
16#include <thread>
17
18using namespace bb;
19
20static constexpr size_t SMALL_LOG_2_NUM_GATES = 5;
21
22class ChonkBatchVerifierTests : public ::testing::Test {
23 protected:
25
26 using CircuitProducer = PrivateFunctionExecutionMockCircuitProducer;
27
29 size_t num_app_circuits = 1)
30 {
31 CircuitProducer circuit_producer(num_app_circuits);
32 const size_t num_circuits = circuit_producer.total_num_circuits;
33 Chonk ivc{ num_circuits };
34 TestSettings settings{ .log2_num_gates = SMALL_LOG_2_NUM_GATES };
35 for (size_t j = 0; j < num_circuits; ++j) {
36 circuit_producer.construct_and_accumulate_next_circuit(ivc, settings);
37 }
38 return { ivc.prove(), ivc.get_hiding_kernel_vk_and_hash() };
39 }
40
41 static void tamper_ipa_g_zero(ChonkProof& proof)
42 {
43 using IpaCommitment = curve::Grumpkin::AffineElement;
44 using IpaScalar = curve::Grumpkin::ScalarField;
45
46 constexpr size_t commitment_size = FrCodec::template calc_num_fields<IpaCommitment>();
47 constexpr size_t scalar_size = FrCodec::template calc_num_fields<IpaScalar>();
48 constexpr size_t g_zero_offset = 2 * CONST_ECCVM_LOG_N * commitment_size;
49 static_assert(g_zero_offset + commitment_size + scalar_size == IPA_PROOF_LENGTH);
50 ASSERT_LE(g_zero_offset + commitment_size, proof.ipa_proof.size());
51
52 IpaCommitment wrong_g_zero = IpaCommitment::one() * IpaScalar(7);
53 auto wrong_g_zero_fields = FrCodec::serialize_to_fields<IpaCommitment>(wrong_g_zero);
54 std::copy(wrong_g_zero_fields.begin(),
55 wrong_g_zero_fields.end(),
56 proof.ipa_proof.begin() + static_cast<std::ptrdiff_t>(g_zero_offset));
57 }
58
63 std::mutex mutex;
66 size_t expected = 0;
67
69 {
70 std::lock_guard lock(mutex);
71 results.push_back(std::move(r));
72 cv.notify_one();
73 }
74
75 void wait_for(size_t count, std::chrono::seconds timeout = std::chrono::seconds(120))
76 {
77 expected = count;
78 std::unique_lock lock(mutex);
79 ASSERT_TRUE(cv.wait_for(lock, timeout, [&] { return results.size() >= expected; }))
80 << "Timed out waiting for " << expected << " results, got " << results.size();
81 }
82 };
83};
84
85TEST_F(ChonkBatchVerifierTests, BatchOfTwoValidProofs)
86{
87 auto [proof1, vk1] = generate_chonk_proof();
88 auto [proof2, vk2] = generate_chonk_proof();
89
90 ResultCollector collector;
91 ChonkBatchVerifier verifier;
92
93 // Both proofs use VK index 0 (same VK for simplicity)
94 verifier.start(
95 { vk1 }, /*num_cores=*/2, /*batch_size=*/2, [&](VerifyResult r) { collector.on_result(std::move(r)); });
96
97 verifier.enqueue(VerifyRequest{ .request_id = 1, .vk_index = 0, .proof = std::move(proof1) });
98 verifier.enqueue(VerifyRequest{ .request_id = 2, .vk_index = 0, .proof = std::move(proof2) });
99
100 collector.wait_for(2);
101 verifier.stop();
102
103 ASSERT_EQ(collector.results.size(), 2);
104 for (auto& r : collector.results) {
105 EXPECT_TRUE(r.verified()) << "request_id=" << r.request_id << " error=" << r.error_message;
106 EXPECT_GT(r.time_in_verify_ms, 0);
107 }
108}
109
111{
112 // Enqueue 1 proof with batch_size=4, then stop. The proof should be flushed.
113 auto [proof, vk] = generate_chonk_proof();
114
115 ResultCollector collector;
116 ChonkBatchVerifier verifier;
117
118 verifier.start(
119 { vk }, /*num_cores=*/1, /*batch_size=*/4, [&](VerifyResult r) { collector.on_result(std::move(r)); });
120 verifier.enqueue(VerifyRequest{ .request_id = 42, .vk_index = 0, .proof = std::move(proof) });
121
122 // Stop triggers flush of remaining items
123 verifier.stop();
124
125 ASSERT_EQ(collector.results.size(), 1);
126 EXPECT_TRUE(collector.results[0].verified());
127 EXPECT_EQ(collector.results[0].request_id, 42);
128}
129
130TEST_F(ChonkBatchVerifierTests, TamperedProofBisected)
131{
133
134 auto [good_proof, vk1] = generate_chonk_proof();
135 auto [bad_proof, vk2] = generate_chonk_proof();
136
137 // Corrupt the IPA proof portion
138 ASSERT_FALSE(bad_proof.ipa_proof.empty());
139 bad_proof.ipa_proof[0] = bad_proof.ipa_proof[0] + bb::fr(1);
140
141 ResultCollector collector;
142 ChonkBatchVerifier verifier;
143
144 verifier.start(
145 { vk1 }, /*num_cores=*/2, /*batch_size=*/2, [&](VerifyResult r) { collector.on_result(std::move(r)); });
146
147 verifier.enqueue(VerifyRequest{ .request_id = 1, .vk_index = 0, .proof = std::move(good_proof) });
148 verifier.enqueue(VerifyRequest{ .request_id = 2, .vk_index = 0, .proof = std::move(bad_proof) });
149
150 collector.wait_for(2);
151 verifier.stop();
152
153 ASSERT_EQ(collector.results.size(), 2);
154
155 // Find good and bad results by request_id
156 const VerifyResult* good = nullptr;
157 const VerifyResult* bad = nullptr;
158 for (auto& r : collector.results) {
159 if (r.request_id == 1) {
160 good = &r;
161 }
162 if (r.request_id == 2) {
163 bad = &r;
164 }
165 }
166
167 ASSERT_NE(good, nullptr);
168 ASSERT_NE(bad, nullptr);
169 EXPECT_TRUE(good->verified()) << "good proof should verify, error=" << good->error_message;
170 EXPECT_FALSE(bad->verified()) << "bad proof should fail";
171 EXPECT_GT(bad->batch_failure_count, 0u) << "bisection should have occurred";
172}
173
174TEST_F(ChonkBatchVerifierTests, TamperedIpaGZeroRejected)
175{
177
178 auto [good_proof, vk] = generate_chonk_proof();
179 auto bad_proof = good_proof;
180 tamper_ipa_g_zero(bad_proof);
181
182 ChonkNativeVerifier direct_verifier(vk);
183 EXPECT_FALSE(direct_verifier.verify(bad_proof));
184
185 ResultCollector collector;
186 ChonkBatchVerifier verifier;
187 verifier.start(
188 { vk }, /*num_cores=*/2, /*batch_size=*/2, [&](VerifyResult r) { collector.on_result(std::move(r)); });
189
190 verifier.enqueue(VerifyRequest{ .request_id = 1, .vk_index = 0, .proof = std::move(good_proof) });
191 verifier.enqueue(VerifyRequest{ .request_id = 2, .vk_index = 0, .proof = std::move(bad_proof) });
192
193 collector.wait_for(2);
194 verifier.stop();
195
196 std::sort(collector.results.begin(), collector.results.end(), [](auto& a, auto& b) {
197 return a.request_id < b.request_id;
198 });
199 ASSERT_EQ(collector.results.size(), 2);
200 EXPECT_TRUE(collector.results[0].verified()) << collector.results[0].error_message;
201 EXPECT_FALSE(collector.results[1].verified());
202}
203
204TEST_F(ChonkBatchVerifierTests, RejectsDuplicateRequestId)
205{
206 auto [proof, vk] = generate_chonk_proof();
207
208 ResultCollector collector;
209 ChonkBatchVerifier verifier;
210 verifier.start(
211 { vk }, /*num_cores=*/1, /*batch_size=*/4, [&](VerifyResult r) { collector.on_result(std::move(r)); });
212
213 verifier.enqueue(VerifyRequest{ .request_id = 9, .vk_index = 0, .proof = proof });
214 EXPECT_THROW_OR_ABORT(verifier.enqueue(VerifyRequest{ .request_id = 9, .vk_index = 0, .proof = proof }),
215 ".*duplicate request_id.*");
216
217 collector.wait_for(1);
218 verifier.stop();
219}
220
221TEST_F(ChonkBatchVerifierTests, RejectsEnqueueAfterStop)
222{
223 auto [proof, vk] = generate_chonk_proof();
224
225 ResultCollector collector;
226 ChonkBatchVerifier verifier;
227 verifier.start(
228 { vk }, /*num_cores=*/1, /*batch_size=*/1, [&](VerifyResult r) { collector.on_result(std::move(r)); });
229 verifier.stop();
230
231 EXPECT_THROW_OR_ABORT(verifier.enqueue(VerifyRequest{ .request_id = 1, .vk_index = 0, .proof = std::move(proof) }),
232 ".*not running.*");
233}
234
235TEST_F(ChonkBatchVerifierTests, ConcurrentStopIsIdempotent)
236{
237 auto [proof, vk] = generate_chonk_proof();
238
239 ResultCollector collector;
240 ChonkBatchVerifier verifier;
241 verifier.start(
242 { vk }, /*num_cores=*/1, /*batch_size=*/4, [&](VerifyResult r) { collector.on_result(std::move(r)); });
243 verifier.enqueue(VerifyRequest{ .request_id = 1, .vk_index = 0, .proof = std::move(proof) });
244
245 std::thread stop_a([&] { verifier.stop(); });
246 std::thread stop_b([&] { verifier.stop(); });
247 stop_a.join();
248 stop_b.join();
249
250 ASSERT_EQ(collector.results.size(), 1);
251 EXPECT_TRUE(collector.results[0].verified());
252}
253
254TEST_F(ChonkBatchVerifierTests, RejectsDoubleStart)
255{
256 auto [proof, vk] = generate_chonk_proof();
257 (void)proof;
258
259 ResultCollector collector;
260 ChonkBatchVerifier verifier;
261 verifier.start(
262 { vk }, /*num_cores=*/1, /*batch_size=*/1, [&](VerifyResult r) { collector.on_result(std::move(r)); });
263 EXPECT_THROW_OR_ABORT(verifier.start({ vk }, /*num_cores=*/1, /*batch_size=*/1, [](VerifyResult) {}),
264 ".*already started.*");
265 verifier.stop();
266}
267
268TEST_F(ChonkBatchVerifierTests, WrongProofSizeReturnsFailedResult)
269{
270 auto [proof, vk] = generate_chonk_proof();
271 proof.joint_proof.push_back(bb::fr(1));
272
273 ResultCollector collector;
274 ChonkBatchVerifier verifier;
275 verifier.start(
276 { vk }, /*num_cores=*/1, /*batch_size=*/1, [&](VerifyResult r) { collector.on_result(std::move(r)); });
277
278 verifier.enqueue(VerifyRequest{ .request_id = 12, .vk_index = 0, .proof = std::move(proof) });
279 collector.wait_for(1);
280 verifier.stop();
281
282 ASSERT_EQ(collector.results.size(), 1);
283 EXPECT_FALSE(collector.results[0].verified());
284 EXPECT_NE(collector.results[0].error_message.find("wrong size"), std::string::npos);
285}
286
287TEST_F(ChonkBatchVerifierTests, CallbackExceptionDoesNotKillVerifier)
288{
289 auto [proof, vk] = generate_chonk_proof();
290
291 std::mutex mutex;
293 size_t callback_count = 0;
294
295 ChonkBatchVerifier verifier;
296 verifier.start({ vk }, /*num_cores=*/1, /*batch_size=*/1, [&](VerifyResult) {
297 {
298 std::lock_guard lock(mutex);
299 callback_count++;
300 }
301 cv.notify_one();
302 throw std::runtime_error("callback failed");
303 });
304
305 verifier.enqueue(VerifyRequest{ .request_id = 1, .vk_index = 0, .proof = std::move(proof) });
306
307 {
308 std::unique_lock lock(mutex);
309 ASSERT_TRUE(cv.wait_for(lock, std::chrono::seconds(120), [&] { return callback_count == 1; }));
310 }
311 verifier.stop();
312}
313
320TEST_F(ChonkBatchVerifierTests, RandomMixedBatches)
321{
323 auto [good_proof_template, vk] = generate_chonk_proof();
324
325 // { seed, total, num_bad, batch_size, num_cores }
326 struct TestCase {
327 uint32_t seed;
328 size_t total;
329 size_t num_bad;
330 uint32_t batch_size;
331 uint32_t num_cores;
332 };
333 const std::vector<TestCase> cases = {
334 { 42, 16, 0, 16, 4 }, // all valid
335 { 100, 8, 8, 8, 4 }, // all invalid
336 { 8080, 30, 1, 30, 4 }, // needle in haystack
337 { 2025, 30, 10, 30, 4 }, // ~1/3 bad
338 { 6174, 30, 15, 30, 4 }, // half bad
339 { 9999, 30, 29, 30, 4 }, // inverted needle
340 { 1337, 30, 7, 8, 4 }, // failures across multiple batches
341 { 314, 12, 3, 12, 1 }, // single core
342 { 555, 8, 3, 1, 4 }, // degenerate batch_size=1
343 };
344
345 for (const auto& [seed, total, num_bad, batch_size, num_cores] : cases) {
346 SCOPED_TRACE("seed=" + std::to_string(seed) + " total=" + std::to_string(total) +
347 " num_bad=" + std::to_string(num_bad) + " batch_size=" + std::to_string(batch_size));
348
349 // Pick bad indices via seeded Fisher-Yates shuffle
350 std::vector<size_t> indices(total);
351 std::iota(indices.begin(), indices.end(), 0);
352 std::mt19937 rng(seed);
353 for (size_t i = total - 1; i > 0; --i) {
355 std::swap(indices[i], indices[dist(rng)]);
356 }
357 std::set<size_t> bad_indices(indices.begin(), indices.begin() + static_cast<ptrdiff_t>(num_bad));
358
359 // Build proofs, corrupting IPA for bad ones
361 proofs.reserve(total);
362 for (size_t i = 0; i < total; ++i) {
363 proofs.push_back(good_proof_template);
364 if (bad_indices.count(i)) {
365 proofs.back().ipa_proof[0] = proofs.back().ipa_proof[0] + bb::fr(1);
366 }
367 }
368
369 ResultCollector collector;
370 ChonkBatchVerifier verifier;
371 verifier.start({ vk }, num_cores, batch_size, [&](VerifyResult r) { collector.on_result(std::move(r)); });
372
373 for (size_t i = 0; i < total; ++i) {
374 verifier.enqueue(
375 VerifyRequest{ .request_id = static_cast<uint64_t>(i), .vk_index = 0, .proof = std::move(proofs[i]) });
376 }
377
378 collector.wait_for(total, std::chrono::seconds(300));
379 verifier.stop();
380
381 ASSERT_EQ(collector.results.size(), total);
382 std::sort(collector.results.begin(), collector.results.end(), [](auto& a, auto& b) {
383 return a.request_id < b.request_id;
384 });
385 for (size_t i = 0; i < total; ++i) {
386 EXPECT_EQ(collector.results[i].request_id, i);
387 if (bad_indices.count(i)) {
388 EXPECT_FALSE(collector.results[i].verified()) << "proof " << i << " should fail";
389 } else {
390 EXPECT_TRUE(collector.results[i].verified()) << "proof " << i << " should pass";
391 }
392 }
393 }
394}
395
397{
398 auto [proof, vk] = generate_chonk_proof();
399
400 ResultCollector collector;
401 ChonkBatchVerifier verifier;
402
403 verifier.start(
404 { vk }, /*num_cores=*/1, /*batch_size=*/1, [&](VerifyResult r) { collector.on_result(std::move(r)); });
405
406 // vk_index=99 is out of range
407 verifier.enqueue(VerifyRequest{ .request_id = 7, .vk_index = 99, .proof = std::move(proof) });
408
409 collector.wait_for(1);
410 verifier.stop();
411
412 ASSERT_EQ(collector.results.size(), 1);
413 EXPECT_FALSE(collector.results[0].verified());
414 EXPECT_EQ(collector.results[0].request_id, 7);
415 EXPECT_NE(collector.results[0].error_message.find("invalid vk_index"), std::string::npos);
416}
417
418#endif // __wasm__
#define EXPECT_THROW_OR_ABORT(statement, matcher)
Definition assert.hpp:192
#define BB_DISABLE_ASSERTS()
Definition assert.hpp:33
TEST_F(ChonkBatchVerifierTests, BatchOfTwoValidProofs)
PrivateFunctionExecutionMockCircuitProducer CircuitProducer
static std::pair< ChonkProof, std::shared_ptr< MegaZKFlavor::VKAndHash > > generate_chonk_proof(size_t num_app_circuits=1)
static void tamper_ipa_g_zero(ChonkProof &proof)
Asynchronous batch verifier for Chonk IVC proofs.
void enqueue(VerifyRequest request)
Enqueue a proof for verification.
void stop()
Stop the processor, flushing remaining proofs.
void start(std::vector< std::shared_ptr< MegaZKFlavor::VKAndHash > > vks, uint32_t num_cores, uint32_t batch_size, ResultCallback on_result)
Start the coordinator thread.
The IVC scheme used by the aztec client for private function execution.
Definition chonk.hpp:39
Verifier for Chonk IVC proofs (both native and recursive).
Output verify(const Proof &proof)
Verify a Chonk proof.
typename Group::affine_element AffineElement
Definition grumpkin.hpp:64
FF a
FF b
std::filesystem::path bb_crs_path()
void init_file_crs_factory(const std::filesystem::path &path)
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
::testing::Types< BN254Settings, GrumpkinSettings > TestSettings
field< Bn254FrParams > fr
Definition fr.hpp:155
VerifierCommitmentKey< Curve > vk
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
std::string to_string(bb::avm2::ValueTag tag)
Helper: collect results from the processor via callback.
void wait_for(size_t count, std::chrono::seconds timeout=std::chrono::seconds(120))
HonkProof ipa_proof
A request to verify a single Chonk proof.
Result of verifying a single proof within a batch.