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