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