Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
batch_merge_verifier.cpp
Go to the documentation of this file.
1// === AUDIT STATUS ===
2// internal: { status: not started, auditors: [], commit: }
3// external_1: { status: not started, auditors: [], commit: }
4// external_2: { status: not started, auditors: [], commit: }
5// =====================
6
11
12namespace bb {
13
14template <typename Curve, size_t MaxMergeSize>
16 reduce_to_pairing_check(const Proof& proof, const FF hash)
17{
18 BB_BENCH_NAME("BatchMergeVerifier::reduce_to_pairing_check");
19
20 transcript->load_proof(proof);
21
22 // Get the lowest 127 bits of the hash
23 // We compare the calculated hashes against this value so that we can reuse the transcript hash calculations
24 // A collision happens with probability 2^{-127}
25 const FF binding_hash = std::get<0>(Transcript::Codec::split_challenge(hash));
26
27 // -------------------------------------------------------------------------
28 // Step 1: Receive commitments to columns to be merged
29 // -------------------------------------------------------------------------
30 std::vector<std::vector<Commitment>> subtable_cols(MAX_MERGE_SIZE, std::vector<Commitment>(NUM_WIRES));
31 std::vector<FF> calculated_hashes;
32 for (size_t idx = 0; idx < MAX_MERGE_SIZE; ++idx) {
33 for (size_t col = 0; col < NUM_WIRES; ++col) {
34 subtable_cols[idx][col] = transcript->template receive_from_prover<Commitment>(
35 "COLUMN_" + std::to_string(col) + "_" + std::to_string(idx));
36 }
37 calculated_hashes.push_back(transcript->template get_challenge<FF>("HASH_" + std::to_string(idx)));
38 }
39
40 // -------------------------------------------------------------------------
41 // Step 1.b: Receive commitments to the masking table
42 // -------------------------------------------------------------------------
43 std::array<Commitment, NUM_WIRES> zk_columns;
44 for (size_t col = 0; col < NUM_WIRES; ++col) {
45 zk_columns[col] = transcript->template receive_from_prover<Commitment>("ZK_COLUMN_" + std::to_string(col));
46 }
47
48 // -------------------------------------------------------------------------
49 // Step 1.c: Flatten the columns for easier utilization
50 // -------------------------------------------------------------------------
51 std::vector<Commitment> flattened_cols;
52 flattened_cols.reserve(NUM_EVALS_FROM_COLUMNS);
53 for (size_t col = 0; col < NUM_WIRES; ++col) {
54 flattened_cols.push_back(std::move(zk_columns[col]));
55 }
56 for (auto& subtable_col : subtable_cols) {
57 for (size_t col = 0; col < NUM_WIRES; col++) {
58 flattened_cols.push_back(std::move(subtable_col[col]));
59 }
60 }
61
62 // -------------------------------------------------------------------------
63 // Step 2: Receive N and shift sizes from the proof
64 // -------------------------------------------------------------------------
65 const FF N = transcript->template receive_from_prover<FF>("NUM_SUBTABLES");
66
67 // -------------------------------------------------------------------------
68 // Step 2.a: Enforce 1 <= N <= MAX_MERGE_SIZE
69 // -------------------------------------------------------------------------
70 FF running_product = FF(1);
71 for (size_t idx = 0; idx < MAX_MERGE_SIZE; idx++) {
72 running_product *= (N - FF(idx + 1));
73 }
74
75 bool is_valid_num_subtables = true;
76 if constexpr (IsRecursive) {
77 is_valid_num_subtables = running_product.get_value().is_zero();
78 running_product.assert_equal(FF(0));
79 } else {
80 is_valid_num_subtables = running_product.is_zero();
81 }
82
83 std::vector<FF> shift_sizes;
84 shift_sizes.reserve(NUM_COLUMN_TABLES);
85 shift_sizes.push_back(FF(UltraEccOpsTable::ZK_ULTRA_OPS));
86 // Array s.t. indicator_array[i] = (i < N)
87 std::vector<FF> indicator_array = compute_indicator_array(N);
88
89 for (size_t i = 0; i < MAX_MERGE_SIZE; ++i) {
90 size_t idx = 1 + i;
91 shift_sizes.push_back(transcript->template receive_from_prover<FF>("SHIFT_SIZE_" + std::to_string(i)));
92 shift_sizes[idx] = shift_sizes[idx] * indicator_array[i]; // zero out shift sizes for unused subtables
93 }
94
95 // -------------------------------------------------------------------------
96 // Step 3: Receive [T] commitments from proof
97 // -------------------------------------------------------------------------
98 TableCommitments merged_commitments;
99 for (size_t col = 0; col < NUM_WIRES; ++col) {
100 merged_commitments[col] =
101 transcript->template receive_from_prover<Commitment>("MERGED_COLUMN_" + std::to_string(col));
102 }
103
104 // -------------------------------------------------------------------------
105 // Step 4: Compute degree check challenges 1, α, α^2, .., α^{(M + 1) * NUM_WIRES-1}
106 // -------------------------------------------------------------------------
107 std::vector<FF> degree_check_challenges;
108 degree_check_challenges.reserve(NUM_EVALS_FROM_COLUMNS);
109 const FF degree_check_challenge = transcript->template get_challenge<FF>("DEGREE_CHECK_CHALLENGE");
110 degree_check_challenges = { FF(1), degree_check_challenge };
111 for (size_t idx = 2; idx < NUM_EVALS_FROM_COLUMNS; idx++) {
112 degree_check_challenges.push_back(degree_check_challenges.back() * degree_check_challenge);
113 }
114
115 // -------------------------------------------------------------------------
116 // Step 5: Receive [G] commitments from proof
117 // -------------------------------------------------------------------------
118 Commitment degree_check_commitment = transcript->template receive_from_prover<Commitment>("DEGREE_CHECK_POLY");
119
120 // -------------------------------------------------------------------------
121 // Step 6: Compute evaluation challenge κ, powers of kappa and their inverses
122 // -------------------------------------------------------------------------
123 const FF kappa = transcript->template get_challenge<FF>("KAPPA");
124 const FF kappa_inv = kappa.invert();
125
126 std::vector<FF> powers_of_kappa;
127 powers_of_kappa.reserve(shift_sizes.size());
128 for (const FF& shift_size : shift_sizes) {
129 if constexpr (IsRecursive) {
130 // Shift sizes are at most 2^CONST_OP_QUEUE_LOG_SIZE so the implicit range constraint enforced by pow is
131 // always satisfied
132 powers_of_kappa.push_back(kappa.template pow<CONST_OP_QUEUE_LOG_SIZE + 1>(shift_size));
133 } else {
135 static_cast<uint32_t>(shift_size), 1UL << (CONST_OP_QUEUE_LOG_SIZE + 1), "Shift size is too large");
136 powers_of_kappa.push_back(kappa.pow(shift_size));
137 }
138 }
139
140 std::vector<FF> powers_of_kappa_inv;
141 powers_of_kappa_inv.reserve(powers_of_kappa.size());
142 if constexpr (IsRecursive) {
143 for (const FF& kappa_pow : powers_of_kappa) {
144 powers_of_kappa_inv.push_back(kappa_pow.invert());
145 }
146 } else {
147 powers_of_kappa_inv = powers_of_kappa;
148 FF::batch_invert(powers_of_kappa_inv);
149 }
150
151 // -------------------------------------------------------------------------
152 // Step 7: Receive evaluations
153 // -------------------------------------------------------------------------
154 // C_i_col(κ)
155 std::vector<FF> evals;
156 evals.reserve(NUM_EVALS);
157 for (size_t i = 0; i < NUM_EVALS_FROM_COLUMNS; ++i) {
158 const FF received_eval = transcript->template receive_from_prover<FF>("C_EVAL_" + std::to_string(i));
159 evals.push_back(received_eval);
160 }
161
162 // T_col(κ)
163 for (size_t col = 0; col < NUM_WIRES; ++col) {
164 evals.push_back(transcript->template receive_from_prover<FF>("MERGED_EVAL_" + std::to_string(col)));
165 }
166
167 // G_col(κ^{-1})
168 evals.push_back(transcript->template receive_from_prover<FF>("DEGREE_CHECK_EVAL"));
169
170 // -------------------------------------------------------------------------
171 // Step 9: Verify concatenation identity, degree identity, and hash consistency
172 // -------------------------------------------------------------------------
173
174 std::vector<OriginTag> origin_tags;
175 if constexpr (IsRecursive) {
176 // To prevent an OriginTag false positive, we re-tag the powers of kappa with the round
177 // provenance of evals
178 for (FF& kappa_pow : powers_of_kappa) {
179 origin_tags.push_back(kappa_pow.get_origin_tag());
180 kappa_pow.set_origin_tag(evals[0].get_origin_tag());
181 }
182 for (FF& kappa_pow : powers_of_kappa_inv) {
183 kappa_pow.set_origin_tag(evals[0].get_origin_tag());
184 }
185 }
186
187 const bool concatenation_verified = check_concatenation_identity(evals, powers_of_kappa);
188 const bool degree_check_verified =
189 check_degree_identity(evals, powers_of_kappa_inv, kappa, degree_check_challenges);
190 const bool hash_verified = check_hash_consistency(binding_hash, calculated_hashes, indicator_array);
191
192 // Reset origin tags
193 if constexpr (IsRecursive) {
194 for (auto [kappa_pow, origin_tag] : zip_view(powers_of_kappa, origin_tags)) {
195 kappa_pow.set_origin_tag(origin_tag);
196 }
197 for (auto [kappa_pow, origin_tag] : zip_view(powers_of_kappa_inv, origin_tags)) {
198 kappa_pow.set_origin_tag(origin_tag);
199 }
200 }
201
202 // -------------------------------------------------------------------------
203 // Run Shplonk and reduce to KZG pairing check
204 // -------------------------------------------------------------------------
205 std::vector<OpeningClaim<Curve>> opening_claims;
206 opening_claims.reserve(NUM_OPENING_CLAIMS);
207 for (size_t idx = 0; idx < NUM_EVALS_FROM_COLUMNS; ++idx) {
208 opening_claims.push_back(OpeningClaim<Curve>{ { kappa, evals[idx] }, flattened_cols[idx] });
209 }
210 for (size_t idx = 0; idx < NUM_WIRES; ++idx) {
211 opening_claims.push_back(
212 OpeningClaim<Curve>{ { kappa, evals[NUM_EVALS_FROM_COLUMNS + idx] }, merged_commitments[idx] });
213 }
214 opening_claims.push_back(OpeningClaim<Curve>{ { kappa_inv, evals.back() }, degree_check_commitment });
215
216 ShplonkVerifier shplonk_verifier = ShplonkVerifier::reduce_verification_no_finalize(opening_claims, transcript);
217
218 Commitment g1_identity;
219 if constexpr (IsRecursive) {
220 g1_identity = Commitment::one(kappa.get_context());
221 } else {
222 g1_identity = Commitment::one();
223 }
224 BatchOpeningClaim<Curve> batch_claim = shplonk_verifier.export_batch_opening_claim(g1_identity);
225
226 BB_ASSERT(batch_claim.commitments.size() == MERGE_BATCHED_CLAIM_SIZE);
227 BB_ASSERT(batch_claim.scalars.size() == MERGE_BATCHED_CLAIM_SIZE);
228
229 PairingPoints pairing_points = PCS::reduce_verify_batch_opening_claim(std::move(batch_claim), transcript);
230
231 vinfo("BatchMergeVerifier: concatenation check passed: ", concatenation_verified ? "true" : "false");
232 vinfo("BatchMergeVerifier: degree check passed: ", degree_check_verified ? "true" : "false");
233 vinfo("BatchMergeVerifier: hash check passed: ", hash_verified ? "true" : "false");
234 vinfo("BatchMergeVerifier: is N in [1, MAX_MERGE_SIZE]: ", is_valid_num_subtables ? "true" : "false");
235
236 return { pairing_points,
237 merged_commitments,
238 degree_check_verified && concatenation_verified && hash_verified && is_valid_num_subtables };
239}
240
241template <typename Curve, size_t MaxMergeSize>
243 compute_indicator_array(const FF& N) const
244{
245 // Array s.t. indicator_array[i] = (i < N)
246 std::vector<FF> indicator_array;
247 if constexpr (IsRecursive) {
248 BB_ASSERT_GT(N.get_value(), 0U);
249
250 // Create the array
251 // Note that N is automatically range constrainted because we assert that 1 <= N <= MAX_MERGE_SIZE
252 for (size_t idx = 0; idx < MAX_MERGE_SIZE; idx++) {
253 const FF idx_wit = FF(idx);
254 indicator_array.push_back(idx_wit.template ranged_less_than<LOG_MAX_MERGE_SIZE + 1>(N));
255 }
256 } else {
257 BB_ASSERT_GT(static_cast<uint32_t>(N), 0U);
258 for (size_t idx = 0; idx < MAX_MERGE_SIZE; idx++) {
259 indicator_array.push_back(idx < static_cast<uint32_t>(N) ? FF(1) : FF(0));
260 }
261 }
262
263 return indicator_array;
264}
265
266template <typename Curve, size_t MaxMergeSize>
268 compute_dirac_array(const std::vector<FF>& indicator_array) const
269{
270 // Shift to the left the indicator array (i < N) to get shifted_indicator_array[i] = (i < N - 1)
271 std::vector<FF> shifted_indicator_array;
272 shifted_indicator_array.reserve(MAX_MERGE_SIZE);
273 for (size_t i = 0; i < MAX_MERGE_SIZE - 1; ++i) {
274 shifted_indicator_array.push_back(indicator_array[i + 1]);
275 }
276 shifted_indicator_array.push_back(FF(0));
277
278 // Construct array s.t. dirac_array[i] = (i == N - 1)
279 std::vector<FF> dirac_array;
280 dirac_array.reserve(MAX_MERGE_SIZE);
281 for (size_t i = 0; i < MAX_MERGE_SIZE; ++i) {
282 dirac_array.push_back(indicator_array[i] - shifted_indicator_array[i]);
283 }
284
285 return dirac_array;
286}
287
288template <typename Curve, size_t MaxMergeSize>
290 std::vector<FF>& evals, const std::vector<FF>& pow_kappa_subtable_size) const
291{
292 bool concatenation_verified = true;
293 for (size_t j = 0; j < NUM_WIRES; ++j) {
294 FF concatenation_diff = evals[((NUM_COLUMN_TABLES - 1) * NUM_WIRES) + j];
295 // Horner: i from N-1 down to 0 — accum ← accum · κ^{size_i} + T_{i,j}(κ).
296 for (size_t i_rev = 1; i_rev < NUM_COLUMN_TABLES; ++i_rev) {
297 const size_t i = NUM_COLUMN_TABLES - 1 - i_rev;
298 concatenation_diff *= pow_kappa_subtable_size[i];
299 concatenation_diff += evals[(i * NUM_WIRES) + j];
300 }
301 concatenation_diff -= evals[NUM_EVALS_FROM_COLUMNS + j];
302
303 if constexpr (IsRecursive) {
304 concatenation_verified &= concatenation_diff.get_value() == 0;
305 concatenation_diff.assert_equal(FF(0),
306 "assert_equal: merge concatenation identity failed in Merge Verifier");
307 } else {
308 concatenation_verified &= concatenation_diff == 0;
309 }
310 }
311 return concatenation_verified;
312}
313
314template <typename Curve, size_t MaxMergeSize>
316 std::vector<FF>& evals,
317 const std::vector<FF>& powers_of_kappa_inv,
318 const FF& kappa,
319 const std::vector<FF>& degree_check_challenges) const
320{
321 FF degree_check_diff(0);
322 for (size_t i = 0; i < powers_of_kappa_inv.size(); ++i) {
323 for (size_t j = 0; j < NUM_WIRES; ++j) {
324 degree_check_diff +=
325 degree_check_challenges[(i * NUM_WIRES) + j] * powers_of_kappa_inv[i] * evals[(i * NUM_WIRES) + j];
326 }
327 }
328 degree_check_diff *= kappa;
329 degree_check_diff -= evals.back();
330
331 bool degree_check_verified = true;
332 if constexpr (IsRecursive) {
333 degree_check_verified &= degree_check_diff.get_value() == 0;
334 degree_check_diff.assert_equal(FF(0), "assert_equal: merge degree identity failed in Merge Verifier");
335 } else {
336 degree_check_verified &= degree_check_diff == 0;
337 }
338
339 return degree_check_verified;
340}
341
342template <typename Curve, size_t MaxMergeSize>
344 const std::vector<Commitment>& col_commitments, const std::optional<FF>& prev_hash)
345{
346 std::vector<FF> hash_inputs;
347 if (prev_hash.has_value()) {
348 if constexpr (IsRecursive) {
349 const FF& h = prev_hash.value();
350 h.set_origin_tag(OriginTag::constant());
351 hash_inputs.push_back(h);
352 } else {
353 hash_inputs.push_back(prev_hash.value());
354 }
355 }
356 for (const auto& com : col_commitments) {
357 auto com_serialized = Transcript::Codec::serialize_to_fields(com);
358 if constexpr (IsRecursive) {
359 for (auto& el : com_serialized) {
360 el.set_origin_tag(OriginTag::constant());
361 }
362 }
363 hash_inputs.insert(hash_inputs.end(), com_serialized.begin(), com_serialized.end());
364 }
365 if constexpr (IsRecursive) {
366 FF hash_result = stdlib::poseidon2<typename Curve::Builder>::hash(hash_inputs);
367 hash_result.unset_free_witness_tag();
368 hash_result.set_origin_tag(OriginTag::constant());
369 return hash_result;
370 } else {
372 }
373}
374
375template <typename Curve, size_t MaxMergeSize>
377 const std::vector<FF>& calculated_hashes,
378 const std::vector<FF>& indicator_array) const
379{
380 // Construct array s.t. dirac_array[i] = (i == N - 1)
381 std::vector<FF> dirac_array = compute_dirac_array(indicator_array);
382
383 // Compute element-wise product of extended_hash and dirac_array
384 FF expected_hash = dirac_array[0] * calculated_hashes[0];
385 for (size_t i = 1; i < MAX_MERGE_SIZE; ++i) {
386 expected_hash += calculated_hashes[i] * dirac_array[i];
387 }
388
389 FF hash_diff = expected_hash - hash;
390 bool verified = true;
391 if constexpr (IsRecursive) {
392 verified = hash_diff.get_value() == 0;
393 hash_diff.assert_equal(FF(0), "BatchMergeVerifier: column commitments hash mismatch");
394 } else {
395 verified = hash_diff == FF(0);
396 }
397
398 return verified;
399}
400
401// Explicit template instantiations
403template class BatchMergeVerifier_<stdlib::bn254<MegaCircuitBuilder>, CHONK_MAX_NUM_CIRCUITS>;
404
405// For testing
408
409} // namespace bb
#define BB_ASSERT(expression,...)
Definition assert.hpp:70
#define BB_ASSERT_GT(left, right,...)
Definition assert.hpp:113
#define BB_ASSERT_LT(left, right,...)
Definition assert.hpp:143
bb::field< bb::Bn254FrParams > FF
Definition field.cpp:24
#define BB_BENCH_NAME(name)
Definition bb_bench.hpp:264
Unified batch verifier for the batch Goblin ECC op queue merge protocol.
bool check_degree_identity(std::vector< FF > &evals, const std::vector< FF > &powers_of_kappa_inv, const FF &kappa, const std::vector< FF > &degree_check_challenges) const
Verify the degree identity G(κ⁻¹) = Σ_{i,col} α_{i,col} · C_i_col(κ) · κ^{1 − shift_sizes[j]}.
typename Curve::ScalarField FF
ReductionResult reduce_to_pairing_check(const Proof &proof, const FF hash)
Reduce the batch merge proof to a pairing check.
bool check_concatenation_identity(std::vector< FF > &evals, const std::vector< FF > &pow_kappa_subtable_size) const
Verify the concatenation identity T(κ) = Σ_i C_i(κ) · κ^{offset_i} for every column.
static FF ecc_op_hash_step(const std::vector< Commitment > &col_commitments, const std::optional< FF > &prev_hash=std::nullopt)
Compute one step of the ECC op running hash.
typename Curve::AffineElement Commitment
std::vector< FF > compute_indicator_array(const FF &N) const
Compute array of length M := MaxMergeSize s.t. indicator_array[i] = (i < N).
std::array< Commitment, NUM_WIRES > TableCommitments
std::conditional_t< Curve::is_stdlib_type, stdlib::recursion::PairingPoints< Curve >, bb::PairingPoints< Curve > > PairingPoints
std::vector< FF > compute_dirac_array(const std::vector< FF > &indicator_array) const
Compute array of length M := MaxMergeSize s.t. dirac_array[i] = (i == N - 1)
bool check_hash_consistency(const FF &hash, const std::vector< FF > &calculated_hashes, const std::vector< FF > &indicator_array) const
Verify that the column commitments in the proof match the running hash from accumulation.
Unverified claim (C,r,v) for some witness polynomial p(X) such that.
Definition claim.hpp:55
Shplonk Verifier.
Definition shplonk.hpp:331
BatchOpeningClaim< Curve > export_batch_opening_claim(const Commitment &g1_identity)
Export a BatchOpeningClaim instead of performing final batch_mul.
Definition shplonk.hpp:417
static constexpr size_t ZK_ULTRA_OPS
static FF hash(const std::vector< FF > &input)
Hashes a vector of field elements.
#define vinfo(...)
Definition log.hpp:94
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
std::string to_string(bb::avm2::ValueTag tag)
Result of batch merge verification.
An accumulator consisting of the Shplonk evaluation challenge and vectors of commitments and scalars.
Definition claim.hpp:155
std::vector< Commitment > commitments
Definition claim.hpp:160
std::vector< Scalar > scalars
Definition claim.hpp:161
static OriginTag constant()
static void batch_invert(C &coeffs) noexcept
Batch invert a collection of field elements using Montgomery's trick.