binius_core/constraint_system/
verify.rs

1// Copyright 2024-2025 Irreducible Inc.
2
3use std::{cmp::Reverse, iter};
4
5use binius_field::{BinaryField, PackedField, TowerField};
6use binius_hash::PseudoCompressionFunction;
7use binius_math::{ArithExpr, CompositionPoly, EvaluationOrder};
8use binius_utils::{bail, checked_arithmetics::log2_ceil_usize};
9use digest::{core_api::BlockSizeUser, Digest, Output};
10use itertools::{izip, multiunzip, Itertools};
11use tracing::instrument;
12
13use super::{
14	channel::{Boundary, OracleOrConst},
15	error::{Error, VerificationError},
16	exp, ConstraintSystem, Proof,
17};
18use crate::{
19	composition::IndexComposition,
20	constraint_system::{
21		channel::{Flush, FlushDirection},
22		common::{FDomain, FEncode, FExt},
23	},
24	fiat_shamir::{CanSample, Challenger},
25	merkle_tree::BinaryMerkleTreeScheme,
26	oracle::{MultilinearOracleSet, OracleId},
27	piop,
28	polynomial::MultivariatePoly,
29	protocols::{
30		evalcheck::EvalcheckMultilinearClaim,
31		gkr_exp,
32		gkr_gpa::{self, LayerClaim},
33		greedy_evalcheck,
34		sumcheck::{
35			self, constraint_set_zerocheck_claim, zerocheck, BatchSumcheckOutput,
36			CompositeSumClaim, EqIndSumcheckClaim, ZerocheckClaim,
37		},
38	},
39	ring_switch,
40	tower::{PackedTop, TowerFamily, TowerUnderlier},
41	transcript::VerifierTranscript,
42	transparent::eq_ind::EqIndPartialEval,
43};
44
45/// Verifies a proof against a constraint system.
46#[instrument("constraint_system::verify", skip_all, level = "debug")]
47pub fn verify<U, Tower, Hash, Compress, Challenger_>(
48	constraint_system: &ConstraintSystem<FExt<Tower>>,
49	log_inv_rate: usize,
50	security_bits: usize,
51	boundaries: &[Boundary<FExt<Tower>>],
52	proof: Proof,
53) -> Result<(), Error>
54where
55	U: TowerUnderlier<Tower>,
56	Tower: TowerFamily,
57	Tower::B128: PackedTop<Tower>,
58	Hash: Digest + BlockSizeUser,
59	Compress: PseudoCompressionFunction<Output<Hash>, 2> + Default + Sync,
60	Challenger_: Challenger + Default,
61{
62	let ConstraintSystem {
63		mut oracles,
64		mut table_constraints,
65		mut flushes,
66		non_zero_oracle_ids,
67		max_channel_id,
68		mut exponents,
69		..
70	} = constraint_system.clone();
71
72	// Stable sort constraint sets in descending order by number of variables.
73	table_constraints.sort_by_key(|constraint_set| Reverse(constraint_set.n_vars));
74
75	let Proof { transcript } = proof;
76
77	let mut transcript = VerifierTranscript::<Challenger_>::new(transcript);
78	transcript.observe().write_slice(boundaries);
79
80	let merkle_scheme = BinaryMerkleTreeScheme::<_, Hash, _>::new(Compress::default());
81	let (commit_meta, oracle_to_commit_index) = piop::make_oracle_commit_meta(&oracles)?;
82	let fri_params = piop::make_commit_params_with_optimal_arity::<_, FEncode<Tower>, _>(
83		&commit_meta,
84		&merkle_scheme,
85		security_bits,
86		log_inv_rate,
87	)?;
88
89	// Read polynomial commitment polynomials
90	let mut reader = transcript.message();
91	let commitment = reader.read::<Output<Hash>>()?;
92
93	// GKR exp multiplication
94	exponents.sort_by_key(|b| std::cmp::Reverse(b.n_vars(&oracles)));
95
96	let exp_challenge = transcript.sample_vec(exp::max_n_vars(&exponents, &oracles));
97
98	let mut reader = transcript.message();
99	let exp_evals = reader.read_scalar_slice(exponents.len())?;
100
101	let exp_claims = exp::make_claims(&exponents, &oracles, &exp_challenge, &exp_evals)?
102		.into_iter()
103		.collect::<Vec<_>>();
104
105	let base_exp_output =
106		gkr_exp::batch_verify(EvaluationOrder::HighToLow, &exp_claims, &mut transcript)?;
107
108	let exp_eval_claims = exp::make_eval_claims(&exponents, base_exp_output)?;
109
110	// Grand product arguments
111	// Grand products for non-zero checks
112	let mut reader = transcript.message();
113	let non_zero_products = reader.read_scalar_slice(non_zero_oracle_ids.len())?;
114	if non_zero_products
115		.iter()
116		.any(|count| *count == Tower::B128::zero())
117	{
118		bail!(Error::Zeros);
119	}
120
121	let non_zero_prodcheck_claims = gkr_gpa::construct_grand_product_claims(
122		&non_zero_oracle_ids,
123		&oracles,
124		&non_zero_products,
125	)?;
126
127	// Grand products for flushing
128	let mixing_challenge = transcript.sample();
129	// TODO(cryptographers): Find a way to sample less randomness
130	let permutation_challenges = transcript.sample_vec(max_channel_id + 1);
131
132	flushes.sort_by_key(|flush| flush.channel_id);
133	let flush_oracle_ids =
134		make_flush_oracles(&mut oracles, &flushes, mixing_challenge, &permutation_challenges)?;
135	let flush_selectors = flushes
136		.iter()
137		.map(|flush| flush.selector)
138		.collect::<Vec<_>>();
139
140	let flush_products = transcript
141		.message()
142		.read_scalar_slice(flush_oracle_ids.len())?;
143	verify_channels_balance(
144		&flushes,
145		&flush_products,
146		boundaries,
147		mixing_challenge,
148		&permutation_challenges,
149	)?;
150
151	let flush_prodcheck_claims =
152		gkr_gpa::construct_grand_product_claims(&flush_oracle_ids, &oracles, &flush_products)?;
153
154	// Verify grand products
155	let mut final_layer_claims = gkr_gpa::batch_verify(
156		EvaluationOrder::LowToHigh,
157		[flush_prodcheck_claims, non_zero_prodcheck_claims].concat(),
158		&mut transcript,
159	)?;
160
161	let non_zero_final_layer_claims = final_layer_claims.split_off(flush_oracle_ids.len());
162	let flush_final_layer_claims = final_layer_claims;
163
164	// Reduce non_zero_final_layer_claims to evalcheck claims
165	let non_zero_prodcheck_eval_claims =
166		gkr_gpa::make_eval_claims(non_zero_oracle_ids, non_zero_final_layer_claims)?;
167
168	// Reduce flush_final_layer_claims to sumcheck claims then evalcheck claims
169	let (flush_oracle_ids, flush_selectors, flush_final_layer_claims) =
170		reorder_for_flushing_by_n_vars(
171			&oracles,
172			&flush_oracle_ids,
173			flush_selectors,
174			flush_final_layer_claims,
175		);
176
177	let flush_sumcheck_metas = get_flush_dedup_sumcheck_metas(
178		&oracles,
179		&flush_oracle_ids,
180		&flush_selectors,
181		&flush_final_layer_claims,
182	)?;
183
184	let DedupEqIndSumcheckClaims {
185		eq_ind_sumcheck_claims,
186		gkr_eval_points,
187		flush_selectors_unique_by_claim,
188		flush_oracle_ids_by_claim,
189	} = get_flush_dedup_eq_ind_sumcheck_claims(flush_sumcheck_metas)?;
190
191	let regular_sumcheck_claims =
192		sumcheck::eq_ind::reduce_to_regular_sumchecks(&eq_ind_sumcheck_claims)?;
193
194	let flush_sumcheck_output = sumcheck::batch_verify(
195		EvaluationOrder::LowToHigh,
196		&regular_sumcheck_claims,
197		&mut transcript,
198	)?;
199
200	let flush_eval_claims = get_post_flush_sumcheck_eval_claims_without_eq(
201		&oracles,
202		&flush_selectors_unique_by_claim,
203		&flush_oracle_ids_by_claim,
204		&flush_sumcheck_output,
205	)?;
206
207	// Check the eval claim on the transparent eq polynomial
208	for (gkr_eval_point, evals) in izip!(gkr_eval_points, flush_sumcheck_output.multilinear_evals) {
209		let gkr_eval_point_len = gkr_eval_point.len();
210		let eq_ind = EqIndPartialEval::new(gkr_eval_point);
211
212		let sumcheck_challenges_len = flush_sumcheck_output.challenges.len();
213		let expected_eval = eq_ind.evaluate(
214			&flush_sumcheck_output.challenges[(sumcheck_challenges_len - gkr_eval_point_len)..],
215		)?;
216
217		let &actual_eval = evals
218			.last()
219			.expect("Flush sumcheck composition non-empty by construction");
220
221		if expected_eval != actual_eval {
222			return Err(Error::FalseEqEvaluationClaim);
223		}
224	}
225
226	// Zerocheck
227	let (zerocheck_claims, zerocheck_oracle_metas) = table_constraints
228		.iter()
229		.cloned()
230		.map(constraint_set_zerocheck_claim)
231		.collect::<Result<Vec<_>, _>>()?
232		.into_iter()
233		.unzip::<_, _, Vec<_>, Vec<_>>();
234
235	let (max_n_vars, skip_rounds) =
236		max_n_vars_and_skip_rounds(&zerocheck_claims, <FDomain<Tower>>::N_BITS);
237
238	let zerocheck_challenges = transcript.sample_vec(max_n_vars - skip_rounds);
239
240	let univariate_cnt = zerocheck_claims
241		.partition_point(|zerocheck_claim| zerocheck_claim.n_vars() > max_n_vars - skip_rounds);
242
243	let univariate_output = sumcheck::batch_verify_zerocheck_univariate_round(
244		&zerocheck_claims[..univariate_cnt],
245		skip_rounds,
246		&mut transcript,
247	)?;
248
249	let univariate_challenge = univariate_output.univariate_challenge;
250
251	let eq_ind_sumcheck_claims = zerocheck::reduce_to_eq_ind_sumchecks(&zerocheck_claims)?;
252	let regular_sumcheck_claims =
253		sumcheck::eq_ind::reduce_to_regular_sumchecks(&eq_ind_sumcheck_claims)?;
254
255	let sumcheck_output = sumcheck::batch_verify_with_start(
256		EvaluationOrder::LowToHigh,
257		univariate_output.batch_verify_start,
258		&regular_sumcheck_claims,
259		&mut transcript,
260	)?;
261
262	let zerocheck_output = sumcheck::eq_ind::verify_sumcheck_outputs(
263		&eq_ind_sumcheck_claims,
264		&zerocheck_challenges,
265		sumcheck_output,
266	)?;
267
268	let univariate_cnt =
269		zerocheck_claims.partition_point(|claim| claim.n_vars() > max_n_vars - skip_rounds);
270
271	let mut reduction_claims = Vec::with_capacity(univariate_cnt);
272	for (claim, univariatized_multilinear_evals) in
273		iter::zip(&zerocheck_claims, &zerocheck_output.multilinear_evals)
274	{
275		let claim_skip_rounds = claim.n_vars().saturating_sub(max_n_vars - skip_rounds);
276
277		let reduction_claim = sumcheck::univariate::univariatizing_reduction_claim(
278			claim_skip_rounds,
279			univariatized_multilinear_evals,
280		)?;
281
282		reduction_claims.push(reduction_claim);
283	}
284
285	let univariatizing_output =
286		sumcheck::batch_verify(EvaluationOrder::LowToHigh, &reduction_claims, &mut transcript)?;
287
288	let multilinear_zerocheck_output = sumcheck::univariate::verify_sumcheck_outputs(
289		&reduction_claims,
290		univariate_challenge,
291		&zerocheck_output.challenges,
292		univariatizing_output,
293	)?;
294
295	let zerocheck_eval_claims = sumcheck::make_eval_claims(
296		EvaluationOrder::LowToHigh,
297		zerocheck_oracle_metas,
298		multilinear_zerocheck_output,
299	)?;
300
301	// Evalcheck
302	let eval_claims = greedy_evalcheck::verify(
303		&mut oracles,
304		[non_zero_prodcheck_eval_claims, flush_eval_claims]
305			.concat()
306			.into_iter()
307			.chain(zerocheck_eval_claims)
308			.chain(exp_eval_claims),
309		&mut transcript,
310	)?;
311
312	// Reduce committed evaluation claims to PIOP sumcheck claims
313	let system = ring_switch::EvalClaimSystem::new(
314		&oracles,
315		&commit_meta,
316		&oracle_to_commit_index,
317		&eval_claims,
318	)?;
319
320	let ring_switch::ReducedClaim {
321		transparents,
322		sumcheck_claims: piop_sumcheck_claims,
323	} = ring_switch::verify::<_, Tower, _>(&system, &mut transcript)?;
324
325	// Prove evaluation claims using PIOP compiler
326	piop::verify(
327		&commit_meta,
328		&merkle_scheme,
329		&fri_params,
330		&commitment,
331		&transparents,
332		&piop_sumcheck_claims,
333		&mut transcript,
334	)?;
335
336	transcript.finalize()?;
337
338	Ok(())
339}
340
341pub fn max_n_vars_and_skip_rounds<F, Composition>(
342	zerocheck_claims: &[ZerocheckClaim<F, Composition>],
343	domain_bits: usize,
344) -> (usize, usize)
345where
346	F: TowerField,
347	Composition: CompositionPoly<F>,
348{
349	let max_n_vars = max_n_vars(zerocheck_claims);
350
351	// Univariate skip zerocheck domain size is degree * 2^skip_rounds, which
352	// limits skip_rounds to ceil(log2(degree)) less than domain field bits.
353	// We also do back-loaded batching and need to align last skip rounds
354	// according to individual claim initial rounds.
355	let domain_max_skip_rounds = zerocheck_claims
356		.iter()
357		.map(|claim| {
358			let log_degree = log2_ceil_usize(claim.max_individual_degree());
359			max_n_vars - claim.n_vars() + domain_bits.saturating_sub(log_degree)
360		})
361		.min()
362		.unwrap_or(0);
363
364	let max_skip_rounds = domain_max_skip_rounds.min(max_n_vars);
365	(max_n_vars, max_skip_rounds)
366}
367
368fn max_n_vars<F, Composition>(zerocheck_claims: &[ZerocheckClaim<F, Composition>]) -> usize
369where
370	F: TowerField,
371	Composition: CompositionPoly<F>,
372{
373	zerocheck_claims
374		.iter()
375		.map(|claim| claim.n_vars())
376		.max()
377		.unwrap_or(0)
378}
379
380fn verify_channels_balance<F: TowerField>(
381	flushes: &[Flush<F>],
382	flush_products: &[F],
383	boundaries: &[Boundary<F>],
384	mixing_challenge: F,
385	permutation_challenges: &[F],
386) -> Result<(), Error> {
387	if flush_products.len() != flushes.len() {
388		return Err(VerificationError::IncorrectNumberOfFlushProducts.into());
389	}
390
391	let mut flush_iter = flushes
392		.iter()
393		.zip(flush_products.iter().copied())
394		.peekable();
395	while let Some((flush, _)) = flush_iter.peek() {
396		let channel_id = flush.channel_id;
397
398		let boundary_products =
399			boundaries
400				.iter()
401				.fold((F::ONE, F::ONE), |(pull_product, push_product), boundary| {
402					let Boundary {
403						channel_id: boundary_channel_id,
404						direction,
405						multiplicity,
406						values,
407						..
408					} = boundary;
409
410					if *boundary_channel_id == channel_id {
411						let (mixed_values, _) = values.iter().fold(
412							(permutation_challenges[channel_id], F::ONE),
413							|(sum, mixing), values| {
414								(sum + mixing * values, mixing * mixing_challenge)
415							},
416						);
417
418						let mixed_values_with_multiplicity =
419							mixed_values.pow_vartime([*multiplicity]);
420
421						return match direction {
422							FlushDirection::Pull => {
423								(pull_product * mixed_values_with_multiplicity, push_product)
424							}
425							FlushDirection::Push => {
426								(pull_product, push_product * mixed_values_with_multiplicity)
427							}
428						};
429					}
430
431					(pull_product, push_product)
432				});
433
434		let (pull_product, push_product) = flush_iter
435			.peeking_take_while(|(flush, _)| flush.channel_id == channel_id)
436			.fold(boundary_products, |(pull_product, push_product), (flush, flush_product)| {
437				let flush_product_with_multiplicity =
438					flush_product.pow_vartime([flush.multiplicity]);
439				match flush.direction {
440					FlushDirection::Pull => {
441						(pull_product * flush_product_with_multiplicity, push_product)
442					}
443					FlushDirection::Push => {
444						(pull_product, push_product * flush_product_with_multiplicity)
445					}
446				}
447			});
448		if pull_product != push_product {
449			return Err(VerificationError::ChannelUnbalanced { id: channel_id }.into());
450		}
451	}
452
453	Ok(())
454}
455
456pub fn make_flush_oracles<F: TowerField>(
457	oracles: &mut MultilinearOracleSet<F>,
458	flushes: &[Flush<F>],
459	mixing_challenge: F,
460	permutation_challenges: &[F],
461) -> Result<Vec<OracleId>, Error> {
462	let mut mixing_powers = vec![F::ONE];
463	let mut flush_iter = flushes.iter();
464
465	permutation_challenges
466		.iter()
467		.enumerate()
468		.flat_map(|(channel_id, permutation_challenge)| {
469			flush_iter
470				.peeking_take_while(|flush| flush.channel_id == channel_id)
471				.map(|flush| {
472					// Check that all flushed oracles have the same number of variables
473					let mut non_const_oracles =
474						flush.oracles.iter().copied().filter_map(|id| match id {
475							OracleOrConst::Oracle(oracle_id) => Some(oracle_id),
476							_ => None,
477						});
478
479					let first_oracle = non_const_oracles.next().ok_or(Error::EmptyFlushOracles)?;
480					let n_vars = oracles.n_vars(first_oracle);
481
482					for oracle_id in non_const_oracles {
483						let oracle_n_vars = oracles.n_vars(oracle_id);
484						if oracle_n_vars != n_vars {
485							return Err(Error::ChannelFlushNvarsMismatch {
486								expected: n_vars,
487								got: oracle_n_vars,
488							});
489						}
490					}
491
492					// Compute powers of the mixing challenge
493					while mixing_powers.len() < flush.oracles.len() {
494						let last_power = *mixing_powers.last().expect(
495							"mixing_powers is initialized with one element; \
496								mixing_powers never shrinks; \
497								thus, it must not be empty",
498						);
499						mixing_powers.push(last_power * mixing_challenge);
500					}
501
502					let const_linear_combination = flush
503						.oracles
504						.iter()
505						.copied()
506						.zip(mixing_powers.iter())
507						.filter_map(|(id, coeff)| match id {
508							OracleOrConst::Const { base, .. } => Some(base * coeff),
509							_ => None,
510						})
511						.sum::<F>();
512
513					//To store a linear combination with constants and actual oracles, we add in the factor corresponding to the constant values into the offset.
514					let id = oracles
515						.add_named(format!("flush channel_id={channel_id}"))
516						.linear_combination_with_offset(
517							n_vars,
518							*permutation_challenge + const_linear_combination,
519							flush
520								.oracles
521								.iter()
522								.zip(mixing_powers.iter().copied())
523								.filter_map(|(id, coeff)| match id {
524									OracleOrConst::Oracle(oracle_id) => Some((*oracle_id, coeff)),
525									_ => None,
526								}),
527						)?;
528					Ok(id)
529				})
530				.collect::<Vec<_>>()
531		})
532		.collect()
533}
534
535#[derive(Debug)]
536pub struct FlushSumcheckMeta<F: TowerField> {
537	pub composite_sum_claims:
538		Vec<CompositeSumClaim<F, IndexComposition<FlushSumcheckComposition, 2>>>,
539	pub flush_selectors_unique: Vec<OracleId>,
540	pub flush_oracle_ids: Vec<OracleId>,
541	pub eval_point: Vec<F>,
542}
543
544pub fn get_flush_dedup_sumcheck_metas<F: TowerField>(
545	oracles: &MultilinearOracleSet<F>,
546	flush_oracle_ids: &[OracleId],
547	flush_selectors: &[OracleId],
548	final_layer_claims: &[LayerClaim<F>],
549) -> Result<Vec<FlushSumcheckMeta<F>>, Error> {
550	let total_flushes = flush_oracle_ids.len();
551
552	debug_assert_eq!(total_flushes, flush_selectors.len());
553	debug_assert_eq!(total_flushes, final_layer_claims.len());
554
555	let mut flush_sumcheck_metas = Vec::new();
556
557	let mut begin = 0;
558	for end in 1..=total_flushes {
559		if end < total_flushes
560			&& final_layer_claims[end].eval_point == final_layer_claims[end - 1].eval_point
561		{
562			continue;
563		}
564
565		let eval_point = final_layer_claims[begin].eval_point.clone();
566		let n_vars = eval_point.len();
567
568		// deduplicate selector oracles
569		let mut flush_selectors_unique = flush_selectors[begin..end].to_vec();
570		flush_selectors_unique.sort();
571		flush_selectors_unique.dedup();
572
573		let n_selectors = flush_selectors_unique.len();
574		let n_multilinears = n_selectors + end - begin;
575
576		let mut composite_sum_claims = Vec::with_capacity(end - begin);
577
578		for (i, (&oracle_id, &flush_selector, layer_claim)) in izip!(
579			&flush_oracle_ids[begin..end],
580			&flush_selectors[begin..end],
581			&final_layer_claims[begin..end]
582		)
583		.enumerate()
584		{
585			debug_assert_eq!(n_vars, oracles.n_vars(oracle_id));
586
587			let Some(selector_index) = flush_selectors_unique
588				.iter()
589				.position(|&flush_selectors_dedup| flush_selectors_dedup == flush_selector)
590			else {
591				panic!(
592					"flush_selectors_unique is a set of unique flush selectors \
593                     from a given slice of oracles, search must succeed by construction."
594				);
595			};
596
597			let composite_sum_claim = CompositeSumClaim {
598				composition: IndexComposition::new(
599					n_multilinears,
600					[i + n_selectors, selector_index],
601					FlushSumcheckComposition,
602				)?,
603				sum: layer_claim.eval,
604			};
605
606			composite_sum_claims.push(composite_sum_claim);
607		}
608
609		let flush_sumcheck_meta = FlushSumcheckMeta {
610			composite_sum_claims,
611			flush_selectors_unique,
612			flush_oracle_ids: flush_oracle_ids[begin..end].to_vec(),
613			eval_point,
614		};
615
616		flush_sumcheck_metas.push(flush_sumcheck_meta);
617
618		begin = end;
619	}
620
621	Ok(flush_sumcheck_metas)
622}
623
624#[derive(Debug)]
625pub struct FlushSumcheckComposition;
626
627impl<P: PackedField> CompositionPoly<P> for FlushSumcheckComposition {
628	fn n_vars(&self) -> usize {
629		2
630	}
631
632	fn degree(&self) -> usize {
633		2
634	}
635
636	fn expression(&self) -> ArithExpr<P::Scalar> {
637		ArithExpr::Var(0) * ArithExpr::Var(1) + ArithExpr::one() - ArithExpr::Var(1)
638	}
639
640	fn binary_tower_level(&self) -> usize {
641		0
642	}
643
644	fn evaluate(&self, query: &[P]) -> Result<P, binius_math::Error> {
645		let unmasked_flush = query[0];
646		let selector = query[1];
647
648		Ok(selector * unmasked_flush + (P::one() - selector))
649	}
650}
651
652pub fn get_post_flush_sumcheck_eval_claims_without_eq<F: TowerField>(
653	oracles: &MultilinearOracleSet<F>,
654	flush_selectors_unique_by_claim: &[Vec<OracleId>],
655	flush_oracle_ids_by_claim: &[Vec<OracleId>],
656	sumcheck_output: &BatchSumcheckOutput<F>,
657) -> Result<Vec<EvalcheckMultilinearClaim<F>>, Error> {
658	let n_claims = sumcheck_output.multilinear_evals.len();
659	debug_assert_eq!(n_claims, flush_oracle_ids_by_claim.len());
660	debug_assert_eq!(n_claims, flush_selectors_unique_by_claim.len());
661
662	let max_n_vars = sumcheck_output.challenges.len();
663
664	let mut evalcheck_claims = Vec::new();
665	for (flush_oracle_ids, evals, flush_selectors_unique) in izip!(
666		flush_oracle_ids_by_claim,
667		&sumcheck_output.multilinear_evals,
668		flush_selectors_unique_by_claim
669	) {
670		let n_selectors = flush_selectors_unique.len();
671		debug_assert_eq!(evals.len(), n_selectors + flush_oracle_ids.len() + 1);
672
673		for (&flush_selector, &eval) in izip!(flush_selectors_unique, evals) {
674			let n_vars = oracles.n_vars(flush_selector);
675			let eval_point = sumcheck_output.challenges[max_n_vars - n_vars..].into();
676
677			evalcheck_claims.push(EvalcheckMultilinearClaim {
678				id: flush_selector,
679				eval_point,
680				eval,
681			});
682		}
683
684		for (&flush_oracle, &eval) in izip!(flush_oracle_ids, &evals[n_selectors..]) {
685			let n_vars = oracles.n_vars(flush_oracle);
686			let eval_point = sumcheck_output.challenges[max_n_vars - n_vars..].into();
687
688			evalcheck_claims.push(EvalcheckMultilinearClaim {
689				id: flush_oracle,
690				eval_point,
691				eval,
692			});
693		}
694	}
695
696	Ok(evalcheck_claims)
697}
698
699pub struct DedupEqIndSumcheckClaims<F: TowerField, Composition: CompositionPoly<F>> {
700	eq_ind_sumcheck_claims: Vec<EqIndSumcheckClaim<F, Composition>>,
701	gkr_eval_points: Vec<Vec<F>>,
702	flush_selectors_unique_by_claim: Vec<Vec<OracleId>>,
703	flush_oracle_ids_by_claim: Vec<Vec<OracleId>>,
704}
705
706#[allow(clippy::type_complexity)]
707pub fn get_flush_dedup_eq_ind_sumcheck_claims<F: TowerField>(
708	flush_sumcheck_metas: Vec<FlushSumcheckMeta<F>>,
709) -> Result<DedupEqIndSumcheckClaims<F, impl CompositionPoly<F>>, Error> {
710	let n_claims = flush_sumcheck_metas.len();
711	let mut eq_ind_sumcheck_claims = Vec::with_capacity(n_claims);
712	let mut gkr_eval_points = Vec::with_capacity(n_claims);
713	let mut flush_oracle_ids_by_claim = Vec::with_capacity(n_claims);
714	let mut flush_selectors_unique_by_claim = Vec::with_capacity(n_claims);
715	for flush_sumcheck_meta in flush_sumcheck_metas {
716		let FlushSumcheckMeta {
717			composite_sum_claims,
718			flush_selectors_unique,
719			flush_oracle_ids,
720			eval_point,
721		} = flush_sumcheck_meta;
722
723		let n_vars = eval_point.len();
724		let n_multilinears = flush_selectors_unique.len() + flush_oracle_ids.len();
725		let eq_ind_sumcheck_claim =
726			EqIndSumcheckClaim::new(n_vars, n_multilinears, composite_sum_claims)?;
727
728		eq_ind_sumcheck_claims.push(eq_ind_sumcheck_claim);
729		gkr_eval_points.push(eval_point);
730		flush_selectors_unique_by_claim.push(flush_selectors_unique);
731		flush_oracle_ids_by_claim.push(flush_oracle_ids);
732	}
733
734	Ok(DedupEqIndSumcheckClaims {
735		eq_ind_sumcheck_claims,
736		gkr_eval_points,
737		flush_selectors_unique_by_claim,
738		flush_oracle_ids_by_claim,
739	})
740}
741
742pub fn reorder_for_flushing_by_n_vars<F: TowerField>(
743	oracles: &MultilinearOracleSet<F>,
744	flush_oracle_ids: &[OracleId],
745	flush_selectors: Vec<OracleId>,
746	flush_final_layer_claims: Vec<LayerClaim<F>>,
747) -> (Vec<OracleId>, Vec<usize>, Vec<LayerClaim<F>>) {
748	let mut zipped: Vec<_> =
749		izip!(flush_oracle_ids.iter().copied(), flush_selectors, flush_final_layer_claims)
750			.collect();
751	zipped.sort_by_key(|&(id, flush_selector, _)| Reverse((oracles.n_vars(id), flush_selector)));
752	multiunzip(zipped)
753}