Skip to main content

binius_ip_prover/prodcheck/
mod.rs

1// Copyright 2025-2026 The Binius Developers
2
3use std::iter;
4
5use binius_compute::{Allocator, VecLike};
6use binius_field::{Field, PackedField};
7use binius_ip::{mlecheck, prodcheck::MultilinearEvalClaim};
8use binius_math::{
9	FieldBuffer, FieldVec,
10	line::extrapolate_line,
11	multilinear::eq::{eq_ind_partial_eval, eq_one_var},
12};
13use binius_utils::rayon::{
14	prelude::*,
15	task_size::{IndexedParallelIteratorExt, WorkPerItem},
16};
17use itertools::izip;
18
19use crate::{
20	channel::IPProverChannel,
21	sumcheck::{
22		ProveSingleOutput, bivariate_product_mle, common::MleCheckProver, prove_single_mlecheck,
23	},
24};
25
26pub mod one_pad_mle;
27
28use one_pad_mle::OnePadMleCheckProver;
29
30/// Witness-based prover for the product check protocol.
31///
32/// This prover reduces the claim that a multilinear polynomial evaluates to a product over a
33/// Boolean hypercube to a single multilinear evaluation claim.
34pub struct ProdcheckProver<'a, A: Allocator, P: PackedField> {
35	/// Product layers from largest (original witness) to second-smallest.
36	/// `layers[0]` is the original witness. The final products layer is returned
37	/// separately from the constructor.
38	layers: Vec<FieldVec<P, A>>,
39	/// Allocator the product layers are drawn from.
40	alloc: &'a A,
41}
42
43// A manual `Clone` impl (rather than `#[derive(Clone)]`) so the bound lands on the layer buffer
44// `A::Vec<P>` rather than on `A` and `P`: a derive would require `A: Clone` yet still fail to clone
45// the layers unless `A::Vec<P>: Clone`. Holds for the `Vec`-backed `GlobalAllocator`.
46impl<A: Allocator, P: PackedField> Clone for ProdcheckProver<'_, A, P>
47where
48	A::Vec<P>: Clone,
49{
50	fn clone(&self) -> Self {
51		Self {
52			layers: self.layers.clone(),
53			alloc: self.alloc,
54		}
55	}
56}
57
58impl<'a, A, F, P> ProdcheckProver<'a, A, P>
59where
60	A: Allocator,
61	F: Field,
62	P: PackedField<Scalar = F>,
63{
64	/// Creates a new [`ProdcheckProver`].
65	///
66	/// Returns `(prover, products)` where `products` is the final layer containing the
67	/// products over all `k` variables.
68	///
69	/// # Arguments
70	/// * `k` - The number of variables over which the product is taken. Each reduction step reduces
71	///   one variable by computing pairwise products.
72	/// * `witness` - The witness polynomial
73	///
74	/// # Preconditions
75	/// * `witness.log_len() >= k`
76	pub fn new(k: usize, alloc: &'a A, witness: FieldVec<P, A>) -> (Self, FieldVec<P, A>) {
77		assert!(witness.log_len() >= k); // precondition
78
79		let mut layers = Vec::with_capacity(k + 1);
80		layers.push(witness);
81
82		for _ in 0..k {
83			let prev_layer = layers.last().expect("layers is non-empty");
84			let next_log_len = prev_layer.log_len() - 1;
85			let (half_0, half_1) = prev_layer.split_half();
86
87			// A next-layer word is the product of its two sibling words, written into the pool.
88			// Each layer is half the width of the one below it, down to a single word.
89			// One word is one multiply, so a run must be long enough to pay back handing it off.
90			let out_len = half_0.as_ref().len();
91			let mut next_data = alloc.alloc::<P>(out_len);
92			(next_data.spare_capacity_mut(), half_0.as_ref(), half_1.as_ref())
93				.into_par_iter()
94				.with_min_task(WorkPerItem::FieldMuls)
95				.for_each(|(out, &v0, &v1)| {
96					out.write(v0 * v1);
97				});
98			// Invariant: every zip input holds at least `out_len` words.
99			//
100			// A parallel zip yields as many items as its shortest input holds.
101			// A shorter input would leave trailing slots uninitialized.
102			//
103			//     spare capacity:  >= out_len   allocated for at least that many
104			//     sibling halves:  == out_len   halves of one buffer
105			assert!(
106				next_data.capacity() - next_data.len() >= out_len,
107				"the allocated buffer must hold every claimed slot"
108			);
109			assert_eq!(
110				half_1.as_ref().len(),
111				out_len,
112				"the two sibling halves must hold exactly one word per claimed slot"
113			);
114			// Safety: the length claim covers only initialized slots.
115			// - The assertions above bound every zip input below by `out_len`.
116			// - So the loop ran `out_len` items.
117			// - Each item wrote one slot.
118			unsafe {
119				next_data.set_len(out_len);
120			}
121			let next_layer = FieldBuffer::new(next_log_len, next_data);
122
123			layers.push(next_layer);
124		}
125
126		let products = layers.pop().expect("layers has k+1 elements");
127		(Self { layers, alloc }, products)
128	}
129
130	/// Returns the number of remaining layers to prove.
131	pub const fn n_layers(&self) -> usize {
132		self.layers.len()
133	}
134
135	/// Pops the widest remaining layer and returns it.
136	///
137	/// # Preconditions
138	/// * `self.n_layers() >= 1`
139	pub fn pop_layer(&mut self) -> FieldVec<P, A> {
140		self.layers
141			.pop()
142			.expect("precondition: layers is non-empty")
143	}
144
145	/// Pops the last layer and returns an MLE-check prover for it.
146	///
147	/// Returns `(layer_prover, remaining)` where:
148	/// - `layer_prover` is an MLE-check prover for the popped layer
149	/// - `remaining` is `Some(self)` if there are more layers, `None` otherwise
150	pub fn layer_prover(
151		mut self,
152		claim: MultilinearEvalClaim<F>,
153	) -> (impl MleCheckProver<F> + 'a, Option<Self>) {
154		let alloc = self.alloc;
155		let layer = self.layers.pop().expect("layers is non-empty");
156
157		let remaining = if self.layers.is_empty() {
158			None
159		} else {
160			Some(self)
161		};
162
163		// The layer has one more variable than the claim point.
164		// Its low and high halves are the two multilinears whose product this layer reduces.
165		// Sharing the buffer between the halves avoids copying this (largest) layer.
166		let prover = bivariate_product_mle::new_split_half(alloc, layer, claim.point, claim.eval);
167
168		(prover, remaining)
169	}
170
171	/// Runs the product check protocol and returns the final evaluation claim.
172	///
173	/// This consumes the prover and runs sumcheck reductions from the smallest layer back to
174	/// the largest.
175	///
176	/// # Arguments
177	/// * `claim` - The initial multilinear evaluation claim
178	/// * `channel` - The channel for sending prover messages and sampling challenges
179	///
180	/// # Preconditions
181	/// * `claim.point.len() == witness.log_len() - k` (where k is the number of reduction layers)
182	pub fn prove(
183		self,
184		claim: MultilinearEvalClaim<F>,
185		channel: &mut impl IPProverChannel<F>,
186	) -> MultilinearEvalClaim<F> {
187		let mut prover_opt = Some(self);
188		let mut claim = claim;
189
190		while let Some(prover) = prover_opt {
191			let (mle_prover, remaining) = prover.layer_prover(claim.clone());
192			prover_opt = remaining;
193
194			let ProveSingleOutput {
195				multilinear_evals,
196				challenges,
197			} = prove_single_mlecheck(mle_prover, channel);
198
199			let [eval_0, eval_1] = multilinear_evals
200				.try_into()
201				.expect("prover has two multilinears");
202
203			channel.send_many(&[eval_0, eval_1]);
204
205			let r = channel.sample();
206			let next_eval = extrapolate_line(eval_0, eval_1, r);
207
208			let mut next_point = challenges;
209			next_point.reverse();
210			next_point.push(r);
211
212			claim = MultilinearEvalClaim {
213				eval: next_eval,
214				point: next_point,
215			};
216		}
217
218		claim
219	}
220}
221
222/// Output of [`batch_prove`].
223///
224/// After the full `n_layers` reduction, `evals` holds each input prover's reduced evaluation at
225/// `eval_point`. The batched claim the verifier checks is the eq(selector)-weighted combination
226/// of these evaluations.
227pub struct BatchProveOutput<F> {
228	/// The reduced evaluation point (`selector ++ content ++ node`) shared by all input provers.
229	pub eval_point: Vec<F>,
230	/// Each input prover's reduced evaluation at `eval_point`, in input order.
231	pub evals: Vec<F>,
232}
233
234/// Runs a batched product check protocol for multiple independent prodcheck provers.
235///
236/// This combines n provers, each for an $m$-variate multilinear, using multilinear interpolation
237/// over k selector variables (where $n \le 2^k$). The combined claim is the multilinear
238/// extrapolation of the individual claimed products (padded with zeros to $2^k$) evaluated at the
239/// given point.
240///
241/// The claimed products may themselves be evaluations of the $m$-variate product multilinears at a
242/// shared `content_point` (of length equal to each prover's reduced product dimension). When the
243/// products are scalars (each prover reduces over all of its variables), `content_point` is empty.
244///
245/// # Arguments
246/// * `provers` - Vec of n prodcheck provers. All must have the same `n_layers()`, which is $m$.
247/// * `claimed_products` - Vec of n claimed product values, one per prover. Each is the
248///   corresponding prover's product multilinear evaluated at `content_point`.
249/// * `selector_point` - Evaluation point for the selector variables. Length is $k$.
250/// * `content_point` - Shared evaluation point at which the claimed products are taken. Length is
251///   the product-multilinear dimension (i.e. `witness.log_len() - n_layers`). Empty for scalar
252///   products.
253/// * `channel` - The channel for sending prover messages and sampling challenges.
254///
255/// # Preconditions
256/// * `provers` must be non-empty.
257/// * All provers must have the same `n_layers()` value.
258/// * `2^selector_point.len() >= provers.len()`.
259/// * `claimed_products.len() == provers.len()`.
260/// * `content_point.len() == witness.log_len() - n_layers` for each prover.
261///
262/// Returns the reduced per-input-prover evaluations at the reduced evaluation point. The
263/// batched claim is checked by the ordinary `binius_ip::prodcheck::verify` recursion over
264/// `n_layers` layers (the eq(selector)-weighted combination of the returned evaluations), with
265/// the selector coordinates forming the first `k` coordinates of the claim point.
266///
267/// # Returns
268/// A [`BatchProveOutput`] with the reduced `eval_point` and each input prover's reduced eval.
269///
270/// # Mathematical Description
271///
272/// Let $f_i \in K[X_0, \ldots, X_{m-1}]$ be multilinear for all $i \in \{0, \ldots, n - 1\}$. The
273/// $i$'th prover is a prodcheck prover for $f_i$. Let $p_i \in K$ be the claimed hypercube product
274/// of $f_i$.
275///
276/// Let $y \in K^k$ be the evaluation point. The prover is proving a claim that
277///
278/// $$
279/// \sum_{i \in B_k} \textsf{eq}(i; y) \prod_{j \in B_m} f_i(j) = \sum_{i \in B_k} \textsf{eq}(i; y)
280/// p_i, $$
281///
282/// reducing to an evaluation of the interpolated multilinear
283///
284/// $$
285/// \hat{f}(Y_0, \ldots, Y_{k-1}, X_0, \ldots, X_{m-1}) = \sum_{i \in B_k} \textsf{eq}(i; Y) f_i(X).
286/// $$
287pub fn batch_prove<'a, A: Allocator, F: Field, P: PackedField<Scalar = F>>(
288	provers: Vec<ProdcheckProver<'a, A, P>>,
289	claimed_products: Vec<F>,
290	selector_point: Vec<F>,
291	content_point: Vec<F>,
292	channel: &mut impl IPProverChannel<F>,
293) -> BatchProveOutput<F> {
294	assert!(!provers.is_empty()); // precondition
295	assert_eq!(claimed_products.len(), provers.len()); // precondition
296
297	let n = provers.len();
298	let k = selector_point.len();
299	assert!(provers.len() <= (1 << k)); // precondition
300
301	let n_layers = provers[0].n_layers();
302	assert!(n_layers >= 1); // precondition
303	assert!(provers.iter().all(|p| p.n_layers() == n_layers)); // precondition
304
305	// Thread the content point as the initial inner (content) coordinates of the evaluation point.
306	// `batch_prove_layer` splits `eval_point.split_at(k)` into (selector, content); on the first
307	// layer this seeds each layer prover with `claim{point: content_point, eval: claimed_product}`.
308	let eval_point = [selector_point, content_point].concat();
309
310	let (provers, evals, eval_point) = (0..n_layers).fold(
311		(provers, claimed_products, eval_point),
312		|(provers, claimed_products, eval_point), _| {
313			batch_prove_layer(provers, claimed_products, &eval_point, k, channel)
314		},
315	);
316	debug_assert!(provers.is_empty(), "the final layer leaves no provers");
317	debug_assert_eq!(evals.len(), n);
318
319	BatchProveOutput { eval_point, evals }
320}
321
322#[allow(clippy::type_complexity)]
323fn batch_prove_layer<'a, A: Allocator, F: Field, P: PackedField<Scalar = F>>(
324	provers: Vec<ProdcheckProver<'a, A, P>>,
325	claimed_products: Vec<F>,
326	eval_point: &[F],
327	k: usize,
328	channel: &mut impl IPProverChannel<F>,
329) -> (Vec<ProdcheckProver<'a, A, P>>, Vec<F>, Vec<F>) {
330	let alloc = provers[0].alloc;
331	let inner_coords = &eval_point[k..];
332
333	let (layer_provers, next_provers): (Vec<_>, Vec<_>) = iter::zip(provers, claimed_products)
334		.map(|(prover, prod)| {
335			prover.layer_prover(MultilinearEvalClaim {
336				eval: prod,
337				point: inner_coords.to_vec(),
338			})
339		})
340		.unzip();
341
342	let (next_claimed_products, next_point) =
343		prove_layer_rounds::<A, F, P>(layer_provers, eval_point, k, alloc, channel);
344	let next_provers = next_provers.into_iter().flatten().collect();
345
346	(next_provers, next_claimed_products, next_point)
347}
348
349/// Runs one batched layer reduction over already-constructed layer provers.
350///
351/// The per-instance reductions run in lockstep, their round polynomials combined with the
352/// eq(selector) weights, followed by the shared selector rounds over the per-instance child
353/// evaluation pairs. This is the layer loop of both [`batch_prove`], whose instances share a layer
354/// count, and [`batch_prove_unequal_depths`], whose don't.
355///
356/// # Returns
357///
358/// Each instance's claim on the next layer, in input order, and the reduced evaluation point.
359fn prove_layer_rounds<A: Allocator, F: Field, P: PackedField<Scalar = F>>(
360	mut layer_provers: Vec<impl MleCheckProver<F>>,
361	eval_point: &[F],
362	k: usize,
363	alloc: &A,
364	channel: &mut impl IPProverChannel<F>,
365) -> (Vec<F>, Vec<F>) {
366	let n = layer_provers.len();
367	// Split eval_point into outer (selector) and inner (content) coordinates.
368	let (outer_coords, inner_coords) = eval_point.split_at(k);
369
370	// Compute eq weights for batching: eq(i, outer_coords) for all i in B_k.
371	let eq_weights = eq_ind_partial_eval::<F>(outer_coords);
372
373	// Content rounds: individual provers operate independently.
374	let mut challenges = Vec::with_capacity(eval_point.len());
375
376	for _round in 0..inner_coords.len() {
377		// Execute each prover and compute weighted sum of round coefficients.
378		let coeffss = layer_provers
379			.iter_mut()
380			.map(|prover| {
381				let mut round_coeffs_vec = prover.execute();
382				round_coeffs_vec
383					.pop()
384					.expect("prodcheck layer provers have round_coeffs_vec.len() == 1")
385			})
386			.collect::<Vec<_>>();
387
388		let coeffs = iter::zip(coeffss, eq_weights.iter_scalars())
389			.map(|(coeffs, weight)| coeffs * weight)
390			.sum();
391
392		// Send truncated round proof to channel.
393		channel.send_many(mlecheck::RoundProof::truncate(coeffs).coeffs());
394
395		// Sample challenge and fold all provers.
396		let challenge = channel.sample();
397		challenges.push(challenge);
398
399		for prover in layer_provers.iter_mut() {
400			prover.fold(challenge);
401		}
402	}
403
404	// Finish inner provers to get [eval_0, eval_1] pairs.
405	let (mut vals_0, mut vals_1): (Vec<F>, Vec<F>) = layer_provers
406		.into_iter()
407		.map(|prover| {
408			let evals = prover.finish();
409			let [e0, e1]: [F; 2] = evals
410				.try_into()
411				.expect("bivariate product prover has two multilinears");
412			(e0, e1)
413		})
414		.unzip();
415
416	// Pad vals_0 and vals_1 to 2^k with zeros for FieldBuffer::from_values.
417	vals_0.resize(1 << k, F::ZERO);
418	vals_1.resize(1 << k, F::ZERO);
419
420	// Compute eval from buffers: sum_v eq(v, outer_coords) * vals_0[v] * vals_1[v].
421	let eval = izip!(&vals_0, &vals_1, eq_weights.as_ref())
422		.map(|(&v0, &v1, &eq_i)| v0 * v1 * eq_i)
423		.sum();
424
425	// Selector rounds: pack eval pairs straight into allocator buffers and use a single prover.
426	let outer_prover = bivariate_product_mle::new(
427		alloc,
428		[
429			FieldBuffer::<P, _>::from_values_in(alloc, &vals_0),
430			FieldBuffer::<P, _>::from_values_in(alloc, &vals_1),
431		],
432		outer_coords.to_vec(),
433		eval,
434	);
435
436	let ProveSingleOutput {
437		multilinear_evals: outer_evals,
438		challenges: outer_challenges,
439	} = prove_single_mlecheck(outer_prover, channel);
440
441	challenges.extend(outer_challenges);
442
443	let [merged_eval_0, merged_eval_1]: [F; 2] =
444		outer_evals.try_into().expect("prover has two multilinears");
445
446	// Finalize layer: send evals, sample r, compute next claim.
447	channel.send_many(&[merged_eval_0, merged_eval_1]);
448
449	let r = channel.sample();
450
451	let mut next_point = challenges;
452	next_point.reverse();
453	next_point.push(r);
454
455	// Update claimed products for next iteration, dropping the padded (2^k) selector slots.
456	let next_claimed_products = iter::zip(&vals_0[..n], &vals_1[..n])
457		.map(|(e0, e1)| extrapolate_line(*e0, *e1, r))
458		.collect();
459
460	(next_claimed_products, next_point)
461}
462
463/// Output of [`batch_prove_unequal_depths`].
464///
465/// After `n_layers - 1` reduction layers, each tree retains its final (widest) layer, wrapped in
466/// the one-padding prover that corrects it to the batch's depth.
467pub struct BatchProveUnequalDepthsOutput<F, Prover> {
468	/// The reduced evaluation point shared by all remaining provers.
469	pub eval_point: Vec<F>,
470	/// Each input prover's reduced (padded) claim paired with the not-yet-run prover for its final
471	/// layer, in input order.
472	pub provers: Vec<(F, Prover)>,
473}
474
475/// Runs a batched product check for trees of *unequal* depths.
476///
477/// This is [`batch_prove`] without the requirement that every prover have the same layer count.
478/// Each tree shallower than the deepest is proved as a product check over the one-padding of its
479/// witness — the same witness with constant-1 leaves filling the extra depth, which leaves its
480/// product unchanged. The transcript is then exactly that of an equal-depth batch of the maximum
481/// depth: the verifier runs the ordinary [`binius_ip::prodcheck::verify`] over `n_layers` layers
482/// and never learns the individual depths.
483///
484/// Unlike [`batch_prove`], every prover must reduce over *all* of its witness variables, so each
485/// product is a scalar and there is no content point. The one-padding is only worth its
486/// bookkeeping on full trees, and dropping the content dimension keeps that bookkeeping to two
487/// scalars per layer.
488///
489/// The prover does not materialize the padded witnesses. Each layer's per-tree reduction runs
490/// through [`one_pad_mle`], which corrects the unpadded layer's messages in $O(1)$ per round.
491///
492/// The protocol and the round polynomials are specified in the *Batched Product Checks of Unequal
493/// Depths* appendix of the Binius64 whitepaper.
494///
495/// # Arguments
496///
497/// As [`batch_prove`], except that the provers' layer counts may differ and there is no
498/// `content_point`.
499///
500/// # Preconditions
501/// * `provers` must be non-empty.
502/// * Every prover's witness must have exactly `prover.n_layers()` variables, which must be at least
503///   1.
504/// * `2^selector_point.len() >= provers.len()`.
505/// * `claimed_products.len() == provers.len()`.
506///
507/// # Returns
508///
509/// This stops one layer short, returning each tree's reduced claim beside the prover for its
510/// final (widest) layer — the layer whose reduction
511/// dominates the cost, which a caller can therefore batch with other sumchecks. Running those
512/// provers and the selector rounds that follow finishes the check.
513///
514/// The returned claims and, after the final layer, the per-tree leaf evaluations are claims on the
515/// *padded* witnesses. [`unpad_leaf_claim`] reduces one to the claim on the
516/// tree's own witness.
517pub fn batch_prove_unequal_depths<'a, A, F, P, Channel>(
518	mut provers: Vec<ProdcheckProver<'a, A, P>>,
519	claimed_products: Vec<F>,
520	selector_point: Vec<F>,
521	channel: &mut Channel,
522) -> BatchProveUnequalDepthsOutput<F, impl MleCheckProver<F> + use<'a, A, F, P, Channel>>
523where
524	A: Allocator,
525	F: Field,
526	P: PackedField<Scalar = F>,
527	Channel: IPProverChannel<F>,
528{
529	assert!(!provers.is_empty()); // precondition
530	assert_eq!(claimed_products.len(), provers.len()); // precondition
531
532	let k = selector_point.len();
533	assert!(provers.len() <= (1 << k)); // precondition
534	assert!(provers.iter().all(|prover| prover.n_layers() >= 1)); // precondition
535
536	let alloc = provers[0].alloc;
537	let n_layers = provers
538		.iter()
539		.map(ProdcheckProver::n_layers)
540		.max()
541		.expect("provers is non-empty");
542	// How much depth each tree is padded by.
543	let pad_lens = provers
544		.iter()
545		.map(|prover| n_layers - prover.n_layers())
546		.collect::<Vec<_>>();
547
548	// The tree products stay in hand for the padding layers, which are one-paddings of them; the
549	// input vector itself becomes the running per-tree claims.
550	let products = claimed_products.clone();
551	let mut claims = claimed_products;
552	let mut eval_point = selector_point;
553
554	// Reduce down to the final layer. Iteration `node_len` reduces the layer holding `node_len`
555	// node variables, which is the point's suffix past the selector coordinates.
556	for _ in 0..n_layers - 1 {
557		let layer_provers =
558			layer_provers(&mut provers, &pad_lens, &products, &claims, &eval_point[k..]);
559		let (next_claims, next_point) =
560			prove_layer_rounds::<A, F, P>(layer_provers, &eval_point, k, alloc, channel);
561		claims = next_claims;
562		eval_point = next_point;
563	}
564
565	let provers = layer_provers(&mut provers, &pad_lens, &products, &claims, &eval_point[k..]);
566
567	BatchProveUnequalDepthsOutput {
568		eval_point,
569		provers: iter::zip(claims, provers).collect(),
570	}
571}
572
573/// Builds one padded layer prover per tree, for the layer claimed at `node_point`.
574fn layer_provers<'a, A: Allocator, F: Field, P: PackedField<Scalar = F>>(
575	provers: &mut [ProdcheckProver<'a, A, P>],
576	pad_lens: &[usize],
577	products: &[F],
578	claims: &[F],
579	node_point: &[F],
580) -> Vec<OnePadMleCheckProver<F, impl MleCheckProver<F> + use<'a, A, F, P>>> {
581	let node_len = node_point.len();
582
583	izip!(provers, pad_lens, products, claims)
584		.map(|(prover, &pad_len, &product, &claim)| {
585			let alloc = prover.alloc;
586			// While the batch is still above this tree, the layer is a one-padding of the tree's
587			// product, whose two children are that product and the constant one. Only once the
588			// batch reaches the tree does it start consuming its layers.
589			let layer = if node_len < pad_len {
590				FieldBuffer::from_values_in(alloc, &[product, F::ONE])
591			} else {
592				let layer = prover.pop_layer();
593				assert_eq!(
594					layer.log_len(),
595					node_len - pad_len + 1,
596					"precondition: the witness has exactly n_layers variables"
597				);
598				layer
599			};
600			one_pad_mle::new(alloc, layer, pad_len.min(node_len), node_point.to_vec(), claim)
601		})
602		.collect()
603}
604
605/// Reduces a leaf claim on a one-padded witness to the claim on the witness itself.
606///
607/// A batched product check over trees of unequal depths lifts each shallow tree to the batch's
608/// depth by filling `n_pad_vars` extra leaf positions with ones, which leaves its product
609/// unchanged. [`binius_ip::prodcheck::verify`] is oblivious to that, so the claim it outputs for
610/// such a tree is a claim on the padded witness
611///
612/// $$
613/// M'(X_\text{pad}, X_\text{real}) = 1 + \bigl( M(X_\text{real}) - 1 \bigr) \cdot
614/// \text{eq}(0^\nu; X_\text{pad}),
615/// $$
616///
617/// whose padding variables are the lowest ones. This divides out their equality weight and drops
618/// them from the point, leaving the claim on $M$.
619///
620/// # Arguments
621///
622/// * `eval` - The claimed evaluation of the padded witness.
623/// * `point` - The reduced evaluation point, with the batch's selector coordinates already
624///   stripped.
625/// * `n_pad_vars` - How much depth this tree was padded by: the batch's layer count less the tree's
626///   own.
627///
628/// # Preconditions
629/// * `point.len() >= n_pad_vars`
630///
631/// # Panics
632///
633/// Panics if the padding coordinates' equality weight is zero, which requires one of them to equal
634/// one. They are the verifier's own challenges, so no prover can induce this; it happens with
635/// probability at most $\nu / |K|$.
636pub fn unpad_leaf_claim<F: Field>(
637	eval: F,
638	point: &[F],
639	n_pad_vars: usize,
640) -> MultilinearEvalClaim<F> {
641	assert!(point.len() >= n_pad_vars); // precondition
642
643	let pad_eq = point[..n_pad_vars]
644		.iter()
645		.map(|&coord| eq_one_var(F::ZERO, coord))
646		.product::<F>();
647	assert!(pad_eq != F::ZERO, "a padding coordinate equals one");
648
649	MultilinearEvalClaim {
650		eval: F::ONE + (eval - F::ONE) * pad_eq.invert_or_zero(),
651		point: point[n_pad_vars..].to_vec(),
652	}
653}
654
655#[cfg(test)]
656mod tests {
657	use binius_field::{PackedField, field::FieldOps};
658	use binius_ip::prodcheck;
659	use binius_math::{
660		inner_product::inner_product,
661		multilinear::{eq::eq_ind_partial_eval, evaluate::evaluate},
662		test_utils::{Packed128b, random_field_buffer, random_scalars},
663	};
664	use binius_transcript::{ProverTranscript, fiat_shamir::HasherChallenger};
665	use binius_utils::checked_arithmetics::log2_ceil_usize;
666
667	type StdChallenger = HasherChallenger<sha2::Sha256>;
668	use binius_compute::GlobalAllocator;
669	use rand::prelude::*;
670
671	use super::*;
672
673	/// Combines the per-input-prover evals returned by [`batch_prove`] into the single
674	/// [`MultilinearEvalClaim`] the verifier produces: the eq(selector)-weighted sum over the
675	/// first `k` (selector) coordinates of the reduced evaluation point.
676	fn combine_batch_prove<F: Field, P: PackedField<Scalar = F>>(
677		output: BatchProveOutput<F>,
678		k: usize,
679	) -> MultilinearEvalClaim<F> {
680		let BatchProveOutput { eval_point, evals } = output;
681		let eq_weights = eq_ind_partial_eval::<P>(&eval_point[..k]);
682		let final_eval =
683			inner_product(evals.iter().copied(), (0..evals.len()).map(|i| eq_weights.get(i)));
684
685		MultilinearEvalClaim {
686			eval: final_eval,
687			point: eval_point,
688		}
689	}
690
691	fn test_prodcheck_prove_verify_helper<P: PackedField>(n: usize, k: usize) {
692		let mut rng = StdRng::seed_from_u64(0);
693		let alloc = GlobalAllocator;
694
695		// 1. Create random witness with log_len = n + k
696		let witness = random_field_buffer::<P>(&mut rng, n + k);
697
698		// 2. Create prover (computes product layers)
699		let (prover, products) = ProdcheckProver::new(k, &alloc, witness.clone());
700
701		// 3. Generate random n-dimensional challenge point
702		let eval_point = random_scalars::<P::Scalar>(&mut rng, n);
703
704		// 4. Evaluate products layer at challenge point to create claim
705		let products_eval = evaluate(&products, &eval_point);
706		let claim = MultilinearEvalClaim {
707			eval: products_eval,
708			point: eval_point,
709		};
710
711		// 5. Run prover
712		let mut prover_transcript = ProverTranscript::new(StdChallenger::default());
713		let prover_output = prover.prove(claim.clone(), &mut prover_transcript);
714
715		// 6. Run verifier
716		let mut verifier_transcript = prover_transcript.into_verifier();
717		let verifier_output = prodcheck::verify(k, claim, &mut verifier_transcript).unwrap();
718
719		// 7. Check outputs match
720		assert_eq!(prover_output, verifier_output);
721
722		// 8. Verify multilinear evaluation of original witness
723		let expected_eval = evaluate(&witness, &verifier_output.point);
724		assert_eq!(verifier_output.eval, expected_eval);
725	}
726
727	#[test]
728	fn test_prodcheck_prove_verify() {
729		test_prodcheck_prove_verify_helper::<Packed128b>(4, 3);
730	}
731
732	#[test]
733	fn test_prodcheck_full_prove_verify() {
734		test_prodcheck_prove_verify_helper::<Packed128b>(0, 4);
735	}
736
737	fn test_prodcheck_layer_computation_helper<P: PackedField>(n: usize, k: usize) {
738		let mut rng = StdRng::seed_from_u64(0);
739		let alloc = GlobalAllocator;
740
741		// Create random witness with log_len = n + k
742		let witness = random_field_buffer::<P>(&mut rng, n + k);
743
744		// Create prover (computes product layers)
745		let (_prover, products) = ProdcheckProver::new(k, &alloc, witness.clone());
746
747		// For each index i in the products layer, verify it equals the product of witness values
748		// at indices i + z * 2^n for z in 0..2^k (strided access, not contiguous)
749		let stride = 1 << n;
750		let num_terms = 1 << k;
751		for i in 0..(1 << n) {
752			let mut expected_product = P::Scalar::ONE;
753			for z in 0..num_terms {
754				expected_product *= witness.get(i + z * stride);
755			}
756			let actual = products.get(i);
757			assert_eq!(actual, expected_product, "Product mismatch at index {i}");
758		}
759	}
760
761	#[test]
762	fn test_prodcheck_layer_computation() {
763		test_prodcheck_layer_computation_helper::<Packed128b>(4, 3);
764	}
765
766	/// Builds the same product layers serially, through a temporary vector.
767	///
768	/// The layer recurrence is written twice, so it can drift once.
769	fn reference_layers<P: PackedField>(
770		k: usize,
771		alloc: &GlobalAllocator,
772		witness: FieldBuffer<P>,
773	) -> Vec<FieldBuffer<P>> {
774		let mut layers = Vec::with_capacity(k + 1);
775		layers.push(witness);
776
777		for _ in 0..k {
778			let prev_layer = layers.last().expect("layers is non-empty");
779			let next_log_len = prev_layer.log_len() - 1;
780			let (half_0, half_1) = prev_layer.split_half();
781
782			// Pair each word of the low half with the word of the high half above it.
783			let evals = half_0
784				.as_ref()
785				.iter()
786				.zip(half_1.as_ref())
787				.map(|(v0, v1)| *v0 * *v1)
788				.collect::<Vec<P>>();
789
790			let mut next_data = alloc.alloc::<P>(evals.len());
791			next_data.extend_from_slice(&evals);
792			layers.push(FieldBuffer::new(next_log_len, next_data));
793		}
794
795		layers
796	}
797
798	#[test]
799	fn layers_match_the_reference_word_for_word() {
800		let mut rng = StdRng::seed_from_u64(7);
801		let alloc = GlobalAllocator;
802
803		// Invariant: the layers are compared as packed words, not as scalars.
804		// So the lanes past a short layer's length are compared too.
805		//
806		// Fixture state: four scalars per packed word.
807		//
808		//     log_len:   6   5   4   3   2   1   0
809		//     words:    16   8   4   2   1   1   1
810		//
811		// From log_len 2 down a layer is one word whose trailing lanes are not elements.
812		// Sweeping every depth at every witness size covers both sides of that line.
813		for log_len in 1..=6 {
814			for k in 1..=log_len {
815				let witness = random_field_buffer::<Packed128b>(&mut rng, log_len);
816
817				let (prover, products) = ProdcheckProver::new(k, &alloc, witness.clone());
818				let reference = reference_layers(k, &alloc, witness);
819
820				// The constructor keeps the first k layers and hands back the last one.
821				let built = prover.layers.iter().chain(iter::once(&products));
822
823				for (depth, (built, reference)) in built.zip(&reference).enumerate() {
824					assert_eq!(built.log_len(), reference.log_len(), "log_len at depth {depth}");
825					assert_eq!(
826						built.as_ref(),
827						reference.as_ref(),
828						"layer words at depth {depth}, witness log_len {log_len}, k {k}"
829					);
830				}
831			}
832		}
833	}
834
835	// ==================== batch_prove tests ====================
836
837	/// Helper function for testing batch_prove with ProdcheckProvers.
838	///
839	/// Each witness has exactly `n_layers` variables so that the products are scalars (0-variate).
840	///
841	/// # Arguments
842	/// * `n_layers` - Number of product reduction layers (= variables per witness)
843	/// * `n_provers` - Number of provers to batch
844	fn test_batch_prove_verify_helper<P: PackedField>(n_layers: usize, n_provers: usize) {
845		let mut rng = StdRng::seed_from_u64(42);
846		let alloc = GlobalAllocator;
847
848		let log_n_provers = log2_ceil_usize(n_provers);
849
850		// Each witness has exactly n_layers variables; products are scalars
851		let witnesses: Vec<FieldBuffer<P>> = (0..n_provers)
852			.map(|_| random_field_buffer::<P>(&mut rng, n_layers))
853			.collect();
854
855		// One prover per witness, each paired with the products of its own layers.
856		let (provers, individual_products): (Vec<_>, Vec<_>) = witnesses
857			.iter()
858			.map(|witness| ProdcheckProver::new(n_layers, &alloc, witness.clone()))
859			.unzip();
860
861		// Products are 0-variate (scalars): just get the single value
862		let claimed_products: Vec<P::Scalar> = individual_products
863			.iter()
864			.map(|products| {
865				assert_eq!(products.log_len(), 0);
866				products.get(0)
867			})
868			.collect();
869
870		// Generate random selector challenge point (length = log_n_provers)
871		let selector_challenge = random_scalars::<P::Scalar>(&mut rng, log_n_provers);
872
873		// Compute combined claim using eq weights
874		let eq_weights = eq_ind_partial_eval::<P>(&selector_challenge);
875		let combined_eval = inner_product(
876			claimed_products.iter().copied(),
877			(0..n_provers).map(|i| eq_weights.get(i)),
878		);
879
880		// The verifier claim has point = selector_challenge (length log_n_provers)
881		let claim = MultilinearEvalClaim {
882			eval: combined_eval,
883			point: selector_challenge.clone(),
884		};
885
886		// Run batch_prove (scalar products: empty content point), then finish the final layer.
887		let mut prover_transcript = ProverTranscript::new(StdChallenger::default());
888		let batch_output = batch_prove(
889			provers,
890			claimed_products,
891			selector_challenge,
892			Vec::new(),
893			&mut prover_transcript,
894		);
895		// Each remaining prover has exactly one layer.
896		assert_eq!(batch_output.evals.len(), n_provers);
897		let prover_output = combine_batch_prove::<_, P>(batch_output, log_n_provers);
898
899		// Run verifier with n_layers layers
900		let mut verifier_transcript = prover_transcript.into_verifier();
901		let verifier_output = prodcheck::verify(n_layers, claim, &mut verifier_transcript).unwrap();
902
903		// Check prover and verifier outputs match
904		assert_eq!(prover_output, verifier_output);
905
906		// Verify final evaluation against multilinear extrapolation of input witnesses
907		let final_point = &verifier_output.point;
908		assert_eq!(final_point.len(), log_n_provers + n_layers);
909
910		let selector_challenges = &final_point[..log_n_provers];
911		let content_challenges = &final_point[log_n_provers..];
912
913		let selector_weights = eq_ind_partial_eval::<P>(selector_challenges);
914
915		let expected_eval: P::Scalar = inner_product(
916			(0..n_provers).map(|i| evaluate(&witnesses[i], content_challenges)),
917			(0..n_provers).map(|i| selector_weights.get(i)),
918		);
919
920		assert_eq!(
921			verifier_output.eval, expected_eval,
922			"Final evaluation should match batch witness interpolation"
923		);
924	}
925
926	#[test]
927	fn test_batch_prove_power_of_two_provers() {
928		// 4 provers, 3 layers
929		test_batch_prove_verify_helper::<Packed128b>(3, 4);
930	}
931
932	#[test]
933	fn test_batch_prove_non_power_of_two_provers() {
934		// 3 provers (non-power of 2, requires padding), 4 layers
935		test_batch_prove_verify_helper::<Packed128b>(4, 3);
936	}
937
938	#[test]
939	fn test_batch_prove_single_prover() {
940		// 1 prover (edge case), 5 layers
941		test_batch_prove_verify_helper::<Packed128b>(5, 1);
942	}
943
944	#[test]
945	fn test_batch_prove_single_layer() {
946		// n_layers=1 edge case (the minimum): batch_prove runs 0 reductions and retains the single
947		// (final) layer, which the test then finishes.
948		test_batch_prove_verify_helper::<Packed128b>(1, 4);
949	}
950
951	/// Helper for testing batch_prove where the claimed products are non-scalar: each prover's
952	/// product multilinear is `content_len`-variate, claimed at a shared random content point.
953	///
954	/// # Arguments
955	/// * `n_layers` - Number of product reduction layers (= selector-reduced variables per witness)
956	/// * `n_provers` - Number of provers to batch
957	/// * `content_len` - Number of variables of the product multilinear (witness has log_len =
958	///   content_len + n_layers)
959	fn test_batch_prove_with_content_helper<P: PackedField>(
960		n_layers: usize,
961		n_provers: usize,
962		content_len: usize,
963	) {
964		let mut rng = StdRng::seed_from_u64(7);
965		let alloc = GlobalAllocator;
966
967		let log_n_provers = log2_ceil_usize(n_provers);
968
969		// Each witness has log_len = content_len + n_layers; products are content_len-variate.
970		let witnesses: Vec<FieldBuffer<P>> = (0..n_provers)
971			.map(|_| random_field_buffer::<P>(&mut rng, content_len + n_layers))
972			.collect();
973
974		let (provers, individual_products): (Vec<_>, Vec<_>) = witnesses
975			.iter()
976			.map(|witness| ProdcheckProver::new(n_layers, &alloc, witness.clone()))
977			.unzip();
978
979		// Shared content point; each claimed product is its multilinear evaluated there.
980		let content_point = random_scalars::<P::Scalar>(&mut rng, content_len);
981		let claimed_products: Vec<P::Scalar> = individual_products
982			.iter()
983			.map(|products| {
984				assert_eq!(products.log_len(), content_len);
985				evaluate(products, &content_point)
986			})
987			.collect();
988
989		// Combined verifier claim: eq(selector)-weighted sum of the claimed products, at point
990		// selector ++ content.
991		let selector_challenge = random_scalars::<P::Scalar>(&mut rng, log_n_provers);
992		let eq_weights = eq_ind_partial_eval::<P>(&selector_challenge);
993		let combined_eval = inner_product(
994			claimed_products.iter().copied(),
995			(0..n_provers).map(|i| eq_weights.get(i)),
996		);
997
998		let claim = MultilinearEvalClaim {
999			eval: combined_eval,
1000			point: [selector_challenge.clone(), content_point.clone()].concat(),
1001		};
1002
1003		// Run batch_prove with non-empty content point, then finish the final layer.
1004		let mut prover_transcript = ProverTranscript::new(StdChallenger::default());
1005		let batch_output = batch_prove(
1006			provers,
1007			claimed_products,
1008			selector_challenge,
1009			content_point,
1010			&mut prover_transcript,
1011		);
1012		assert_eq!(batch_output.evals.len(), n_provers);
1013		let prover_output = combine_batch_prove::<_, P>(batch_output, log_n_provers);
1014
1015		// Run verifier with n_layers layers.
1016		let mut verifier_transcript = prover_transcript.into_verifier();
1017		let verifier_output = prodcheck::verify(n_layers, claim, &mut verifier_transcript).unwrap();
1018
1019		assert_eq!(prover_output, verifier_output);
1020
1021		// The reduced point is [selector (log_n_provers), witness vars (content_len + n_layers)],
1022		// where the witness-var coordinates are in each witness's own variable order — so
1023		// `witness_i.evaluate(witness_challenges)` is the per-prover content evaluation that the
1024		// selector-weighted sum recombines.
1025		let final_point = &verifier_output.point;
1026		assert_eq!(final_point.len(), log_n_provers + n_layers + content_len);
1027
1028		let selector_challenges = &final_point[..log_n_provers];
1029		let witness_challenges = &final_point[log_n_provers..];
1030
1031		let selector_weights = eq_ind_partial_eval::<P>(selector_challenges);
1032
1033		let expected_eval: P::Scalar = inner_product(
1034			(0..n_provers).map(|i| evaluate(&witnesses[i], witness_challenges)),
1035			(0..n_provers).map(|i| selector_weights.get(i)),
1036		);
1037
1038		assert_eq!(
1039			verifier_output.eval, expected_eval,
1040			"Final evaluation should match batch witness interpolation"
1041		);
1042	}
1043
1044	#[test]
1045	fn test_batch_prove_with_content() {
1046		// 3 provers (non power of 2), 4 layers, content_len = 2.
1047		test_batch_prove_with_content_helper::<Packed128b>(4, 3, 2);
1048	}
1049
1050	// ==================== batch_prove_unequal_depths tests ====================
1051
1052	/// One prover per entry of `depths`, each reducing over all of its witness variables.
1053	#[allow(clippy::type_complexity)]
1054	fn unequal_depth_provers<'a, P: PackedField>(
1055		rng: &mut impl Rng,
1056		alloc: &'a GlobalAllocator,
1057		depths: &[usize],
1058	) -> (Vec<FieldBuffer<P>>, Vec<ProdcheckProver<'a, GlobalAllocator, P>>, Vec<P::Scalar>) {
1059		itertools::multiunzip(depths.iter().map(|&depth| {
1060			let witness = random_field_buffer::<P>(&mut *rng, depth);
1061			let (prover, products) = ProdcheckProver::new(depth, alloc, witness.clone());
1062			assert_eq!(products.log_len(), 0);
1063			(witness, prover, products.get(0))
1064		}))
1065	}
1066
1067	/// The eq(selector)-weighted combination of per-tree claims, as the verifier forms it.
1068	fn combine_claims<P: PackedField>(
1069		claims: &[P::Scalar],
1070		selector_point: &[P::Scalar],
1071	) -> P::Scalar {
1072		let eq_weights = eq_ind_partial_eval::<P>(selector_point);
1073		inner_product(claims.iter().copied(), (0..claims.len()).map(|i| eq_weights.get(i)))
1074	}
1075
1076	/// Proves a batch of unequal-depth trees against the depth-oblivious verifier, then unpads each
1077	/// tree's leaf claim and checks it against that tree's own witness.
1078	fn test_unequal_depths_helper<P: PackedField>(depths: &[usize]) {
1079		let mut rng = StdRng::seed_from_u64(11);
1080		let alloc = GlobalAllocator;
1081
1082		let k = log2_ceil_usize(depths.len());
1083		let n_layers = *depths.iter().max().expect("depths is non-empty");
1084
1085		let (witnesses, provers, claimed_products) =
1086			unequal_depth_provers::<P>(&mut rng, &alloc, depths);
1087
1088		// The verifier's input claim is the eq(selector)-weighted combination of the products.
1089		let selector_point = random_scalars::<P::Scalar>(&mut rng, k);
1090		let claim = MultilinearEvalClaim {
1091			eval: combine_claims::<P>(&claimed_products, &selector_point),
1092			point: selector_point.clone(),
1093		};
1094
1095		let mut prover_transcript = ProverTranscript::new(StdChallenger::default());
1096		let BatchProveUnequalDepthsOutput {
1097			eval_point,
1098			provers,
1099		} = batch_prove_unequal_depths(
1100			provers,
1101			claimed_products,
1102			selector_point,
1103			&mut prover_transcript,
1104		);
1105
1106		// Finish the retained final layer: run it exactly as an interior reduction layer does.
1107		let (_claims, provers): (Vec<_>, Vec<_>) = provers.into_iter().unzip();
1108		let (evals, eval_point) =
1109			prove_layer_rounds::<_, _, P>(provers, &eval_point, k, &alloc, &mut prover_transcript);
1110		assert_eq!(evals.len(), depths.len());
1111
1112		// The verifier's control flow depends only on the maximum depth.
1113		let mut verifier_transcript = prover_transcript.into_verifier();
1114		let verifier_output = prodcheck::verify(n_layers, claim, &mut verifier_transcript).unwrap();
1115
1116		assert_eq!(verifier_output.point, eval_point);
1117		assert_eq!(verifier_output.eval, combine_claims::<P>(&evals, &eval_point[..k]));
1118
1119		// Each tree's reduced claim is on its *padded* witness; unpadding it yields a claim on the
1120		// witness itself, at a suffix of the shared node point.
1121		for (i, (&depth, witness)) in iter::zip(depths, &witnesses).enumerate() {
1122			let leaf = unpad_leaf_claim(evals[i], &eval_point[k..], n_layers - depth);
1123			assert_eq!(leaf.point.len(), depth);
1124			assert_eq!(leaf.eval, evaluate(witness, &leaf.point), "tree {i}");
1125		}
1126	}
1127
1128	#[test]
1129	fn test_unequal_depths_mixed() {
1130		test_unequal_depths_helper::<Packed128b>(&[2, 4, 5]);
1131	}
1132
1133	#[test]
1134	fn test_unequal_depths_single_prover() {
1135		test_unequal_depths_helper::<Packed128b>(&[3]);
1136	}
1137
1138	#[test]
1139	fn test_unequal_depths_power_of_two_provers() {
1140		// The shallowest tree is padded by more than one layer, the deepest not at all.
1141		test_unequal_depths_helper::<Packed128b>(&[1, 2, 5, 5]);
1142	}
1143
1144	#[test]
1145	fn test_unequal_depths_all_minimal() {
1146		// Depth 1 throughout: every tree retains its final layer immediately.
1147		test_unequal_depths_helper::<Packed128b>(&[1, 1, 1]);
1148	}
1149
1150	#[test]
1151	fn test_unequal_depths_maximal_padding() {
1152		// A single-layer tree beside a deep one: all but its last reduction is padding.
1153		test_unequal_depths_helper::<Packed128b>(&[1, 6]);
1154	}
1155
1156	/// At equal depths every tree is padded by nothing, so the unequal-depth driver must emit
1157	/// byte-for-byte the transcript that [`batch_prove`] does.
1158	#[test]
1159	fn test_unequal_depths_matches_batch_prove_at_equal_depths() {
1160		type P = Packed128b;
1161		type F = <P as FieldOps>::Scalar;
1162
1163		let depths = [4; 3];
1164		let k = log2_ceil_usize(depths.len());
1165		let alloc = GlobalAllocator;
1166
1167		let mut rng = StdRng::seed_from_u64(23);
1168		let selector_point = random_scalars::<F>(&mut rng, k);
1169		// Both drivers see the same trees, so both rebuild them from the same seed.
1170		let prover_seed = 24;
1171
1172		let unequal_proof = {
1173			let mut rng = StdRng::seed_from_u64(prover_seed);
1174			let (_, provers, claimed_products) =
1175				unequal_depth_provers::<P>(&mut rng, &alloc, &depths);
1176
1177			let mut transcript = ProverTranscript::new(StdChallenger::default());
1178			let BatchProveUnequalDepthsOutput {
1179				eval_point,
1180				provers,
1181			} = batch_prove_unequal_depths(
1182				provers,
1183				claimed_products,
1184				selector_point.clone(),
1185				&mut transcript,
1186			);
1187			let (_claims, provers): (Vec<_>, Vec<_>) = provers.into_iter().unzip();
1188			prove_layer_rounds::<_, _, P>(provers, &eval_point, k, &alloc, &mut transcript);
1189			transcript.finalize()
1190		};
1191
1192		let equal_proof = {
1193			let mut rng = StdRng::seed_from_u64(prover_seed);
1194			let (_, provers, claimed_products) =
1195				unequal_depth_provers::<P>(&mut rng, &alloc, &depths);
1196
1197			let mut transcript = ProverTranscript::new(StdChallenger::default());
1198			batch_prove(provers, claimed_products, selector_point, Vec::new(), &mut transcript);
1199			transcript.finalize()
1200		};
1201
1202		assert_eq!(unequal_proof, equal_proof);
1203	}
1204}