Repository: IrreducibleOSS/binius
Pull request #61: New MultilinearPolyVariant for multilinear extensions of composites
Author: login: TomWambsgans
Created: 2025-03-01T19:58:44Z | Merged: 2025-03-07T15:54:56Z

Attempt to fix #40 

Linked issue #40: New MultilinearPolyVariant for multilinear extensions of composites
`binius_core::oracle::MultilinearPolyVariant` defines the different types of virtual multilinear oracles. One of them, for example, is `LinearCombination`, which is defined by a vector of pairs of other columns and associated scalar coefficients. Since it is a linear combination of multilinear polynomials, the derived polynomial is also multilinear and can be easily evaluated by a verifier, given oracle access to the underlying oracles.

We can define a multivariate polynomial as a composite of other multilinear oracles with a higher-degree composition. For example, given two n-variate oracles, $p_0$ and $p_1$, we can define the composite $f = p_0 * p_1$. Unfortunately, this would not be multilinear and thus could not be a `MultilinearPolyVariant`. On the other hand, the multilinear extension of a composite is well-defined. In fact, this satisfies our definition of a virtual polynomial oracle because there is a succinct reduction for evaluation: the sumcheck protocol. The verifier can check the evaluation of the MLE of a composite polynomial at a given point by a sumcheck reduction of the composite polynomial multiplied by a partially evaluated eq indicator.

This task then requires a few items:

1. Define the new `MultilinearPolyVariant`
2. Handle the evaluation of the new oracle type in the `binius_core::protocols::evalcheck` protocol.
3. Expose the new oracle variant through the `binius_circuits::ConstraintSystemBuilder` interface.

This feature would enable the functionality of data-parallel GKR, programmable with the standard Binius constraint system interface.

This task is moderately difficult, but possible for external contributors.

Diff:
diff --git a/crates/circuits/src/builder/constraint_system.rs b/crates/circuits/src/builder/constraint_system.rs
index 0645a2d22..78bc53c8d 100644
--- a/crates/circuits/src/builder/constraint_system.rs
+++ b/crates/circuits/src/builder/constraint_system.rs
@@ -253,6 +253,19 @@ impl<'arena> ConstraintSystemBuilder<'arena> {
 			.linear_combination_with_offset(n_vars, offset, inner)
 	}
 
+	pub fn add_composite_mle(
+		&mut self,
+		name: impl ToString,
+		n_vars: usize,
+		inner: impl IntoIterator<Item = OracleId>,
+		comp: ArithExpr<F>,
+	) -> Result<OracleId, OracleError> {
+		self.oracles
+			.borrow_mut()
+			.add_named(self.scoped_name(name))
+			.composite_mle(n_vars, inner, comp)
+	}
+
 	pub fn add_packed(
 		&mut self,
 		name: impl ToString,
diff --git a/crates/circuits/src/lib.rs b/crates/circuits/src/lib.rs
index 59ae8195d..f6b0af9bf 100644
--- a/crates/circuits/src/lib.rs
+++ b/crates/circuits/src/lib.rs
@@ -29,19 +29,28 @@ mod tests {
 	use binius_core::{
 		constraint_system::{
 			self,
-			channel::{Boundary, FlushDirection},
+			channel::{validate_witness, Boundary, FlushDirection},
 		},
 		fiat_shamir::HasherChallenger,
+		oracle::ShiftVariant,
+		polynomial::ArithCircuitPoly,
 		tower::CanonicalTowerFamily,
 	};
 	use binius_field::{
-		as_packed_field::PackedType, underlier::WithUnderlier, BinaryField8b, Field,
+		arch::OptimalUnderlier, as_packed_field::PackedType, underlier::WithUnderlier,
+		BinaryField128b, BinaryField64b, BinaryField8b, Field,
 	};
 	use binius_hal::make_portable_backend;
 	use binius_hash::compress::Groestl256ByteCompression;
-	use binius_math::DefaultEvaluationDomainFactory;
+	use binius_macros::arith_expr;
+	use binius_math::{
+		CompositionPoly, DefaultEvaluationDomainFactory, IsomorphicEvaluationDomainFactory,
+	};
 	use groestl_crypto::Groestl256;
 
+	type B128 = BinaryField128b;
+	type B64 = BinaryField64b;
+
 	use crate::builder::{
 		types::{F, U},
 		ConstraintSystemBuilder,
@@ -161,4 +170,230 @@ mod tests {
 		>(&constraint_system, 1, 10, &boundaries, proof)
 		.unwrap();
 	}
+
+	#[test]
+	#[ignore]
+	fn test_composite_circuit() {
+		let backend = make_portable_backend();
+		let allocator = bumpalo::Bump::new();
+		let mut builder = ConstraintSystemBuilder::new_with_witness(&allocator);
+		let n_vars = 8;
+		let log_inv_rate = 1;
+		let security_bits = 30;
+		let comp_1 = arith_expr!(B128[x, y] = x*y*y*0x85 +x*x*y*0x9 + y + 0x123);
+		let comp_2 =
+			arith_expr!(B128[x, y, z] = x*z*y*0x81115 +x*y*0x98888 + y*z + z*z*z*z*z*z + 0x155523);
+		let comp_3 = arith_expr!(B128[a, b, c, d, e, f] = e*f*f + a*b*c*2 + d*0x999 + 0x123);
+		let comp_4 = arith_expr!(B128[a, b] = a*(b+a));
+
+		let column_x = builder.add_committed("x", n_vars, 7);
+		let column_y = builder.add_committed("y", n_vars, 7);
+		let column_comp_1 = builder
+			.add_composite_mle("comp1", n_vars, [column_x, column_y], comp_1.clone())
+			.unwrap();
+
+		let column_shift = builder
+			.add_shifted(
+				"shift",
+				column_comp_1,
+				(1 << n_vars) - 1,
+				n_vars,
+				ShiftVariant::CircularLeft,
+			)
+			.unwrap();
+
+		let column_comp_2 = builder
+			.add_composite_mle(
+				"comp2",
+				n_vars,
+				[column_y, column_comp_1, column_shift],
+				comp_2.clone(),
+			)
+			.unwrap();
+
+		let column_z = builder.add_committed("z", n_vars + 1, 6);
+		let column_packed = builder.add_packed("packed", column_z, 1).unwrap();
+
+		let column_comp_3 = builder
+			.add_composite_mle(
+				"comp3",
+				n_vars,
+				[
+					column_x,
+					column_x,
+					column_comp_1,
+					column_shift,
+					column_comp_2,
+					column_packed,
+				],
+				comp_3.clone(),
+			)
+			.unwrap();
+
+		let column_comp_4 = builder
+			.add_composite_mle(
+				"comp4",
+				n_vars,
+				[
+					column_comp_2,
+					column_comp_3,
+					column_x,
+					column_shift,
+					column_y,
+				],
+				comp_4.clone(),
+			)
+			.unwrap();
+
+		// dummy channel
+		let channel = builder.add_channel();
+		builder
+			.send(
+				channel,
+				1 << n_vars,
+				vec![
+					column_x,
+					column_y,
+					column_comp_1,
+					column_shift,
+					column_comp_2,
+					column_packed,
+					column_comp_3,
+				],
+			)
+			.unwrap();
+		builder
+			.receive(
+				channel,
+				1 << n_vars,
+				vec![
+					column_x,
+					column_y,
+					column_comp_1,
+					column_shift,
+					column_comp_2,
+					column_packed,
+					column_comp_3,
+				],
+			)
+			.unwrap();
+
+		let values_x = (0..(1 << n_vars))
+			.map(|i| B128::from(i as u128))
+			.collect::<Vec<_>>();
+		let values_y = (0..(1 << n_vars))
+			.map(|i| B128::from(i * i))
+			.collect::<Vec<_>>();
+
+		let arith_poly_1 = ArithCircuitPoly::new(comp_1);
+		let values_comp_1 = (0..(1 << n_vars))
+			.map(|i| arith_poly_1.evaluate(&[values_x[i], values_y[i]]).unwrap())
+			.collect::<Vec<_>>();
+
+		let mut values_shift = values_comp_1.clone();
+		let first = values_shift.remove(0);
+		values_shift.push(first);
+
+		let arith_poly_2 = ArithCircuitPoly::new(comp_2);
+		let values_comp_2 = (0..(1 << n_vars))
+			.map(|i| {
+				arith_poly_2
+					.evaluate(&[values_y[i], values_comp_1[i], values_shift[i]])
+					.unwrap()
+			})
+			.collect::<Vec<_>>();
+
+		let values_z = (0..(1 << (n_vars + 1)))
+			.map(|i| B64::from(i * i / 8 + i % 10_u64))
+			.collect::<Vec<_>>();
+		let values_packed = (0..(1 << n_vars))
+			.map(|i| {
+				B128::from(
+					((values_z[2 * i + 1].val() as u128) << 64) + values_z[2 * i].val() as u128,
+				)
+			})
+			.collect::<Vec<_>>();
+
+		let arith_poly_3 = ArithCircuitPoly::new(comp_3);
+		let values_comp_3 = (0..(1 << n_vars))
+			.map(|i| {
+				arith_poly_3
+					.evaluate(&[
+						values_x[i],
+						values_x[i],
+						values_comp_1[i],
+						values_shift[i],
+						values_comp_2[i],
+						values_packed[i],
+					])
+					.unwrap()
+			})
+			.collect::<Vec<_>>();
+
+		let arith_poly_4 = ArithCircuitPoly::new(comp_4);
+		let values_comp_4 = (0..(1 << n_vars))
+			.map(|i| {
+				arith_poly_4
+					.evaluate(&[values_comp_2[i], values_comp_3[i]])
+					.unwrap()
+			})
+			.collect::<Vec<_>>();
+
+		let mut add_witness_col_b128 = |oracle_id: usize, values: &[B128]| {
+			builder
+				.witness()
+				.unwrap()
+				.new_column::<B128>(oracle_id)
+				.as_mut_slice()
+				.copy_from_slice(values);
+		};
+		add_witness_col_b128(column_x, &values_x);
+		add_witness_col_b128(column_y, &values_y);
+		add_witness_col_b128(column_comp_1, &values_comp_1);
+		add_witness_col_b128(column_shift, &values_shift);
+		add_witness_col_b128(column_comp_2, &values_comp_2);
+		add_witness_col_b128(column_packed, &values_packed);
+		add_witness_col_b128(column_comp_3, &values_comp_3);
+		add_witness_col_b128(column_comp_4, &values_comp_4);
+		builder
+			.witness()
+			.unwrap()
+			.new_column::<B64>(column_z)
+			.as_mut_slice()
+			.copy_from_slice(&values_z);
+
+		let witness = builder.take_witness().unwrap();
+		let constraint_system = builder.build().unwrap();
+
+		validate_witness(&witness, &[], &[], 1).unwrap();
+
+		let domain_factory = IsomorphicEvaluationDomainFactory::<BinaryField8b>::default();
+		let proof = binius_core::constraint_system::prove::<
+			OptimalUnderlier,
+			CanonicalTowerFamily,
+			_,
+			groestl_crypto::Groestl256,
+			Groestl256ByteCompression,
+			HasherChallenger<groestl_crypto::Groestl256>,
+			_,
+		>(
+			&constraint_system,
+			log_inv_rate,
+			security_bits,
+			&[],
+			witness,
+			&domain_factory,
+			&backend,
+		)
+		.unwrap();
+
+		binius_core::constraint_system::verify::<
+			OptimalUnderlier,
+			CanonicalTowerFamily,
+			groestl_crypto::Groestl256,
+			Groestl256ByteCompression,
+			HasherChallenger<groestl_crypto::Groestl256>,
+		>(&constraint_system, log_inv_rate, security_bits, &[], proof)
+		.unwrap();
+	}
 }
diff --git a/crates/core/src/constraint_system/validate.rs b/crates/core/src/constraint_system/validate.rs
index daddb2c39..da732aa92 100644
--- a/crates/core/src/constraint_system/validate.rs
+++ b/crates/core/src/constraint_system/validate.rs
@@ -17,7 +17,9 @@ use crate::{
 		ConstraintPredicate, MultilinearOracleSet, MultilinearPolyOracle, MultilinearPolyVariant,
 		ProjectionVariant, ShiftVariant,
 	},
-	polynomial::{test_utils::decompose_index_to_hypercube_point, ArithCircuitPoly},
+	polynomial::{
+		test_utils::decompose_index_to_hypercube_point, ArithCircuitPoly, MultilinearComposite,
+	},
 	protocols::sumcheck::prove::zerocheck,
 	witness::MultilinearExtensionIndex,
 };
@@ -244,6 +246,18 @@ where
 				});
 			}
 		}
+		MultilinearPolyVariant::Composite(composite_mle) => {
+			let inner_polys = composite_mle
+				.polys()
+				.map(|id| witness.get_multilin_poly(id))
+				.collect::<Result<Vec<_>, _>>()?;
+			let composite = MultilinearComposite::new(n_vars, composite_mle.c(), inner_polys)?;
+			for i in 0..1 << n_vars {
+				let got = poly.evaluate_on_hypercube(i)?;
+				let expected = composite.evaluate_on_hypercube(i)?;
+				check_eval(oracle_label, i, expected, got)?;
+			}
+		}
 	}
 	Ok(())
 }
diff --git a/crates/core/src/oracle/multilinear.rs b/crates/core/src/oracle/multilinear.rs
index 1aa590f73..7d139c8d0 100644
--- a/crates/core/src/oracle/multilinear.rs
+++ b/crates/core/src/oracle/multilinear.rs
@@ -4,13 +4,16 @@ use std::{array, fmt::Debug, sync::Arc};
 
 use binius_field::{BinaryField128b, Field, TowerField};
 use binius_macros::{DeserializeBytes, SerializeBytes};
+use binius_math::ArithExpr;
 use binius_utils::{bail, DeserializeBytes, SerializationError, SerializationMode, SerializeBytes};
 use bytes::Buf;
 use getset::{CopyGetters, Getters};
 
 use crate::{
 	oracle::{CompositePolyOracle, Error},
-	polynomial::{Error as PolynomialError, IdentityCompositionPoly, MultivariatePoly},
+	polynomial::{
+		ArithCircuitPoly, Error as PolynomialError, IdentityCompositionPoly, MultivariatePoly,
+	},
 };
 
 /// Identifier for a multilinear oracle in a [`MultilinearOracleSet`].
@@ -232,6 +235,44 @@ impl<F: TowerField> MultilinearOracleSetAddition<'_, F> {
 		Ok(self.mut_ref.add_to_set(oracle))
 	}
 
+	pub fn composite_mle(
+		self,
+		n_vars: usize,
+		inner: impl IntoIterator<Item = OracleId>,
+		comp: ArithExpr<F>,
+	) -> Result<OracleId, Error> {
+		let inner = inner
+			.into_iter()
+			.map(|inner_id| {
+				if inner_id >= self.mut_ref.oracles.len() {
+					return Err(Error::InvalidOracleId(inner_id));
+				}
+				if self.mut_ref.n_vars(inner_id) != n_vars {
+					return Err(Error::IncorrectNumberOfVariables { expected: n_vars });
+				}
+				Ok(self.mut_ref.get_from_set(inner_id))
+			})
+			.collect::<Result<Vec<_>, _>>()?;
+
+		let tower_level = inner
+			.iter()
+			.map(|oracle| oracle.binary_tower_level())
+			.max()
+			.unwrap_or(0);
+
+		let composite_mle = CompositeMLE::new(n_vars, inner, comp)?;
+
+		let oracle = |id: OracleId| MultilinearPolyOracle {
+			id,
+			n_vars,
+			tower_level,
+			name: self.name,
+			variant: MultilinearPolyVariant::Composite(composite_mle),
+		};
+
+		Ok(self.mut_ref.add_to_set(oracle))
+	}
+
 	pub fn zero_padded(self, inner_id: OracleId, n_vars: usize) -> Result<OracleId, Error> {
 		if inner_id >= self.mut_ref.oracles.len() {
 			bail!(Error::InvalidOracleId(inner_id));
@@ -412,6 +453,15 @@ impl<F: TowerField> MultilinearOracleSet<F> {
 		self.add().zero_padded(id, n_vars)
 	}
 
+	pub fn add_composite_mle(
+		&mut self,
+		n_vars: usize,
+		inner: impl IntoIterator<Item = OracleId>,
+		comp: ArithExpr<F>,
+	) -> Result<OracleId, Error> {
+		self.add().composite_mle(n_vars, inner, comp)
+	}
+
 	pub fn oracle(&self, id: OracleId) -> MultilinearPolyOracle<F> {
 		self.oracles[id].clone()
 	}
@@ -487,6 +537,7 @@ pub enum MultilinearPolyVariant<F: TowerField> {
 	Packed(Packed),
 	LinearCombination(LinearCombination<F>),
 	ZeroPadded(OracleId),
+	Composite(CompositeMLE<F>),
 }
 
 impl DeserializeBytes for MultilinearPolyVariant<BinaryField128b> {
@@ -721,6 +772,56 @@ impl<F: TowerField> LinearCombination<F> {
 	}
 }
 
+/// MLE of a multivariate polynomial evaluated on multilinear oracles.
+///
+/// i.e. the MLE of the evaluations of $C(M_1, M_2, \ldots, M_n)$ on $\{0, 1\}^\mu$ where:
+/// - $C$ is a arbitrary polynomial in $n$ variables
+/// - $M_1, M_2, \ldots, M_n$ are multilinear oracles in μ variables
+///
+/// ($C$ should be sufficiently lightweight to be evaluated by the verifier)
+#[derive(Debug, Clone, PartialEq, Eq, Getters, CopyGetters, SerializeBytes)]
+pub struct CompositeMLE<F: TowerField> {
+	/// $\mu$
+	#[get_copy = "pub"]
+	n_vars: usize,
+	/// $M_1, M_2, \ldots, M_n$
+	#[getset(get = "pub")]
+	inner: Vec<OracleId>,
+	/// $C$
+	#[getset(get = "pub")]
+	c: ArithCircuitPoly<F>,
+}
+
+impl<F: TowerField> CompositeMLE<F> {
+	pub fn new(
+		n_vars: usize,
+		inner: impl IntoIterator<Item = MultilinearPolyOracle<F>>,
+		c: ArithExpr<F>,
+	) -> Result<Self, Error> {
+		let inner = inner
+			.into_iter()
+			.map(|oracle| {
+				if oracle.n_vars() == n_vars {
+					Ok(oracle.id())
+				} else {
+					Err(Error::IncorrectNumberOfVariables { expected: n_vars })
+				}
+			})
+			.collect::<Result<Vec<_>, _>>()?;
+		let c = ArithCircuitPoly::with_n_vars(inner.len(), c)
+			.map_err(|_| Error::CompositionMismatch)?; // occurs if `c` has more variables than `inner.len()`
+		Ok(Self { n_vars, inner, c })
+	}
+
+	pub fn polys(&self) -> impl Iterator<Item = OracleId> + '_ {
+		self.inner.iter().copied()
+	}
+
+	pub fn n_polys(&self) -> usize {
+		self.inner.len()
+	}
+}
+
 impl<F: TowerField> MultilinearPolyOracle<F> {
 	pub const fn id(&self) -> OracleId {
 		self.id
@@ -747,6 +848,7 @@ impl<F: TowerField> MultilinearPolyOracle<F> {
 			MultilinearPolyVariant::Packed(_) => "Packed",
 			MultilinearPolyVariant::LinearCombination(_) => "LinearCombination",
 			MultilinearPolyVariant::ZeroPadded(_) => "ZeroPadded",
+			MultilinearPolyVariant::Composite(_) => "CompositeMLE",
 		}
 	}
 
diff --git a/crates/core/src/polynomial/arith_circuit.rs b/crates/core/src/polynomial/arith_circuit.rs
index 2bec2030e..c0fb7d97d 100644
--- a/crates/core/src/polynomial/arith_circuit.rs
+++ b/crates/core/src/polynomial/arith_circuit.rs
@@ -4,6 +4,7 @@ use std::{fmt::Debug, mem::MaybeUninit, sync::Arc};
 
 use binius_field::{ExtensionField, Field, PackedField, TowerField};
 use binius_math::{ArithExpr, CompositionPoly, Error};
+use binius_utils::{DeserializeBytes, SerializationError, SerializationMode, SerializeBytes};
 use stackalloc::{
 	helpers::{slice_assume_init, slice_assume_init_mut},
 	stackalloc_uninit,
@@ -132,6 +133,39 @@ pub struct ArithCircuitPoly<F: Field> {
 	tower_level: usize,
 }
 
+impl<F: Field> PartialEq for ArithCircuitPoly<F> {
+	fn eq(&self, other: &Self) -> bool {
+		self.n_vars == other.n_vars && self.expr == other.expr
+	}
+}
+
+impl<F: Field> Eq for ArithCircuitPoly<F> {}
+
+impl<F: TowerField> SerializeBytes for ArithCircuitPoly<F> {
+	fn serialize(
+		&self,
+		mut write_buf: impl bytes::BufMut,
+		mode: SerializationMode,
+	) -> Result<(), SerializationError> {
+		(&self.expr, self.n_vars).serialize(&mut write_buf, mode)
+	}
+}
+
+impl<F: TowerField> DeserializeBytes for ArithCircuitPoly<F> {
+	fn deserialize(
+		read_buf: impl bytes::Buf,
+		mode: SerializationMode,
+	) -> Result<Self, SerializationError>
+	where
+		Self: Sized,
+	{
+		let (expr, n_vars) = <(ArithExpr<F>, usize)>::deserialize(read_buf, mode)?;
+		Self::with_n_vars(n_vars, expr).map_err(|_| SerializationError::InvalidConstruction {
+			name: "ArithCircuitPoly",
+		})
+	}
+}
+
 impl<F: TowerField> ArithCircuitPoly<F> {
 	pub fn new(expr: ArithExpr<F>) -> Self {
 		let degree = expr.degree();
diff --git a/crates/core/src/protocols/evalcheck/evalcheck.rs b/crates/core/src/protocols/evalcheck/evalcheck.rs
index eeb015464..a71284043 100644
--- a/crates/core/src/protocols/evalcheck/evalcheck.rs
+++ b/crates/core/src/protocols/evalcheck/evalcheck.rs
@@ -35,6 +35,7 @@ enum EvalcheckNumerics {
 	Repeating,
 	LinearCombination,
 	ZeroPadded,
+	CompositeMLE,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -48,6 +49,7 @@ pub enum EvalcheckProof<F: Field> {
 		subproofs: Vec<(F, EvalcheckProof<F>)>,
 	},
 	ZeroPadded(F, Box<EvalcheckProof<F>>),
+	CompositeMLE,
 }
 
 impl<F: Field> EvalcheckProof<F> {
@@ -62,11 +64,12 @@ impl<F: Field> EvalcheckProof<F> {
 				subproofs: subproofs
 					.into_iter()
 					.map(|(eval, proof)| (eval.into(), proof.isomorphic()))
-					.collect::<Vec<_>>(),
+					.collect(),
 			},
 			Self::ZeroPadded(eval, proof) => {
 				EvalcheckProof::ZeroPadded(eval.into(), Box::new(proof.isomorphic()))
 			}
+			Self::CompositeMLE => EvalcheckProof::CompositeMLE,
 		}
 	}
 }
@@ -81,6 +84,7 @@ impl EvalcheckNumerics {
 			5 => Ok(Self::Repeating),
 			6 => Ok(Self::LinearCombination),
 			7 => Ok(Self::ZeroPadded),
+			8 => Ok(Self::CompositeMLE),
 			_ => Err(Error::EvalcheckSerializationError),
 		}
 	}
@@ -122,6 +126,9 @@ pub fn serialize_evalcheck_proof<B: BufMut, F: TowerField>(
 			transcript.write_scalar(*val);
 			serialize_evalcheck_proof(transcript, subproof);
 		}
+		EvalcheckProof::CompositeMLE => {
+			transcript.write_bytes(&[EvalcheckNumerics::CompositeMLE as u8]);
+		}
 	}
 }
 
@@ -132,6 +139,7 @@ pub fn deserialize_evalcheck_proof<B: Buf, F: TowerField>(
 	let mut ty = 0;
 	transcript.read_bytes(slice::from_mut(&mut ty))?;
 	let as_enum = EvalcheckNumerics::from(ty)?;
+
 	match as_enum {
 		EvalcheckNumerics::Transparent => Ok(EvalcheckProof::Transparent),
 		EvalcheckNumerics::Committed => Ok(EvalcheckProof::Committed),
@@ -158,6 +166,7 @@ pub fn deserialize_evalcheck_proof<B: Buf, F: TowerField>(
 			let subproof = deserialize_evalcheck_proof(transcript)?;
 			Ok(EvalcheckProof::ZeroPadded(scalar, Box::new(subproof)))
 		}
+		EvalcheckNumerics::CompositeMLE => Ok(EvalcheckProof::CompositeMLE),
 	}
 }
 
diff --git a/crates/core/src/protocols/evalcheck/prove.rs b/crates/core/src/protocols/evalcheck/prove.rs
index fc1f3873a..011b1b7db 100644
--- a/crates/core/src/protocols/evalcheck/prove.rs
+++ b/crates/core/src/protocols/evalcheck/prove.rs
@@ -15,7 +15,10 @@ use tracing::instrument;
 use super::{
 	error::Error,
 	evalcheck::{EvalcheckMultilinearClaim, EvalcheckProof},
-	subclaims::{calculate_projected_mles, MemoizedQueries, ProjectedBivariateMeta},
+	subclaims::{
+		add_composite_sumcheck_to_constraints, calculate_projected_mles, composite_sumcheck_meta,
+		fill_eq_witness_for_composites, MemoizedQueries, ProjectedBivariateMeta,
+	},
 	EvalPoint, EvalPointOracleIdMap,
 };
 use crate::{
@@ -167,7 +170,7 @@ where
 				.collect::<Vec<_>>();
 
 			self.memoized_queries
-				.memoize_query_par(deduplicated_eval_points, self.backend)?;
+				.memoize_query_par(&deduplicated_eval_points, self.backend)?;
 
 			// Make new evaluation claims in parallel.
 			let subclaims = deduplicated_claims_without_evals
@@ -217,7 +220,15 @@ where
 			.map(|claim| Self::projected_bivariate_meta(self.oracles, claim))
 			.collect::<Result<Vec<_>, Error>>()?;
 
-		let projected_mle = calculate_projected_mles(
+		let projected_mles = calculate_projected_mles(
+			&projected_bivariate_metas,
+			&mut self.memoized_queries,
+			&self.projected_bivariate_claims,
+			self.witness_index,
+			self.backend,
+		)?;
+
+		fill_eq_witness_for_composites(
 			&projected_bivariate_metas,
 			&mut self.memoized_queries,
 			&self.projected_bivariate_claims,
@@ -228,7 +239,7 @@ where
 		for (claim, meta, projected) in izip!(
 			std::mem::take(&mut self.projected_bivariate_claims),
 			projected_bivariate_metas,
-			projected_mle
+			projected_mles
 		) {
 			self.process_sumcheck(claim, meta, projected)?;
 		}
@@ -322,6 +333,14 @@ where
 				);
 			}
 
+			MultilinearPolyVariant::Composite(_) => {
+				self.finalized_proofs.insert(
+					multilinear_id,
+					eval_point,
+					(eval, EvalcheckProof::CompositeMLE),
+				);
+			}
+
 			MultilinearPolyVariant::Projected(projected) => {
 				let (id, values) = (projected.id(), projected.values());
 				let new_eval_point = match projected.projection_variant() {
@@ -460,6 +479,7 @@ where
 						);
 					})
 			}
+
 			_ => unreachable!(),
 		};
 		res.is_some()
@@ -514,7 +534,9 @@ where
 				};
 				self.collect_projected_committed(subclaim);
 			}
-			MultilinearPolyVariant::Shifted { .. } | MultilinearPolyVariant::Packed { .. } => {
+			MultilinearPolyVariant::Shifted { .. }
+			| MultilinearPolyVariant::Packed { .. }
+			| MultilinearPolyVariant::Composite { .. } => {
 				self.projected_bivariate_claims.push(evalcheck_claim)
 			}
 			MultilinearPolyVariant::LinearCombination(linear_combination) => {
@@ -564,6 +586,7 @@ where
 			MultilinearPolyVariant::Packed(packed) => {
 				packed_sumcheck_meta(oracles, packed, eval_point)
 			}
+			MultilinearPolyVariant::Composite(_) => composite_sumcheck_meta(oracles, eval_point),
 			_ => unreachable!(),
 		}
 	}
@@ -572,7 +595,7 @@ where
 		&mut self,
 		evalcheck_claim: EvalcheckMultilinearClaim<F>,
 		meta: ProjectedBivariateMeta,
-		projected: MultilinearExtension<PackedType<U, F>>,
+		projected: Option<MultilinearExtension<PackedType<U, F>>>,
 	) -> Result<(), Error> {
 		let EvalcheckMultilinearClaim {
 			id,
@@ -588,8 +611,8 @@ where
 				eval,
 				self.witness_index,
 				&mut self.new_sumchecks_constraints,
-				projected,
-			)?,
+				projected.expect("projected is required by shifted oracle"),
+			),
 
 			MultilinearPolyVariant::Packed(packed) => process_packed_sumcheck(
 				self.oracles,
@@ -599,11 +622,21 @@ where
 				eval,
 				self.witness_index,
 				&mut self.new_sumchecks_constraints,
-				projected,
-			)?,
+				projected.expect("projected is required by packed oracle"),
+			),
+
+			MultilinearPolyVariant::Composite(composite) => {
+				// witness for eq MLE has been previously filled in `fill_eq_witness_for_composites`
+				add_composite_sumcheck_to_constraints(
+					meta,
+					&mut self.new_sumchecks_constraints,
+					&composite,
+					eval,
+				);
+				Ok(())
+			}
 			_ => unreachable!(),
-		};
-		Ok(())
+		}
 	}
 
 	fn make_new_eval_claim(
diff --git a/crates/core/src/protocols/evalcheck/subclaims.rs b/crates/core/src/protocols/evalcheck/subclaims.rs
index fdc6ddcb2..356572e9f 100644
--- a/crates/core/src/protocols/evalcheck/subclaims.rs
+++ b/crates/core/src/protocols/evalcheck/subclaims.rs
@@ -9,7 +9,7 @@
 //!  * one multilin (the multiplier) is transparent (`shift_ind`, `eq_ind`, or tower basis)
 //!  * other multilin is a projection of one of the evalcheck claim multilins to its first variables
 
-use std::collections::HashSet;
+use std::collections::{HashMap, HashSet};
 
 use binius_field::{
 	as_packed_field::{PackScalar, PackedType},
@@ -18,7 +18,8 @@ use binius_field::{
 };
 use binius_hal::{ComputationBackend, ComputationBackendExt};
 use binius_math::{
-	ArithExpr, EvaluationDomainFactory, MLEDirectAdapter, MultilinearExtension, MultilinearQuery,
+	ArithExpr, CompositionPoly, EvaluationDomainFactory, MLEDirectAdapter, MultilinearExtension,
+	MultilinearQuery,
 };
 use binius_maybe_rayon::prelude::*;
 use binius_utils::bail;
@@ -27,8 +28,8 @@ use super::{error::Error, evalcheck::EvalcheckMultilinearClaim};
 use crate::{
 	fiat_shamir::Challenger,
 	oracle::{
-		ConstraintSet, ConstraintSetBuilder, Error as OracleError, MultilinearOracleSet, OracleId,
-		Packed, ProjectionVariant, Shifted,
+		CompositeMLE, ConstraintSet, ConstraintSetBuilder, Error as OracleError,
+		MultilinearOracleSet, OracleId, Packed, ProjectionVariant, Shifted,
 	},
 	polynomial::MultivariatePoly,
 	protocols::sumcheck::{
@@ -37,7 +38,9 @@ use crate::{
 		Error as SumcheckError,
 	},
 	transcript::ProverTranscript,
-	transparent::{shift_ind::ShiftIndPartialEval, tower_basis::TowerBasis},
+	transparent::{
+		eq_ind::EqIndPartialEval, shift_ind::ShiftIndPartialEval, tower_basis::TowerBasis,
+	},
 	witness::{MultilinearExtensionIndex, MultilinearWitness},
 };
 
@@ -126,7 +129,19 @@ pub fn packed_sumcheck_meta<F: TowerField>(
 	})
 }
 
-pub fn add_bivariate_sumcheck_to_constraints<F: Field>(
+pub fn composite_sumcheck_meta<F: TowerField>(
+	oracles: &mut MultilinearOracleSet<F>,
+	eval_point: &[F],
+) -> Result<ProjectedBivariateMeta, Error> {
+	Ok(ProjectedBivariateMeta {
+		multiplier_id: oracles.add_transparent(EqIndPartialEval::new(eval_point.to_vec()))?,
+		inner_id: None,
+		projected_id: None,
+		projected_n_vars: eval_point.len(),
+	})
+}
+
+pub fn add_bivariate_sumcheck_to_constraints<F: TowerField>(
 	meta: ProjectedBivariateMeta,
 	constraint_builders: &mut Vec<ConstraintSetBuilder<F>>,
 	n_vars: usize,
@@ -135,17 +150,26 @@ pub fn add_bivariate_sumcheck_to_constraints<F: Field>(
 	if n_vars > constraint_builders.len() {
 		constraint_builders.resize_with(n_vars, || ConstraintSetBuilder::new());
 	}
-
-	add_bivariate_sumcheck_to_constraint_builder(meta, &mut constraint_builders[n_vars - 1], eval);
+	let bivariate_product = ArithExpr::Var(0) * ArithExpr::Var(1);
+	constraint_builders[n_vars - 1].add_sumcheck(meta.oracle_ids(), bivariate_product, eval);
 }
 
-fn add_bivariate_sumcheck_to_constraint_builder<F: Field>(
+pub fn add_composite_sumcheck_to_constraints<F: TowerField>(
 	meta: ProjectedBivariateMeta,
-	constraint_builder: &mut ConstraintSetBuilder<F>,
+	constraint_builders: &mut Vec<ConstraintSetBuilder<F>>,
+	comp: &CompositeMLE<F>,
 	eval: F,
 ) {
-	let bivariate_product = ArithExpr::Var(0) * ArithExpr::Var(1);
-	constraint_builder.add_sumcheck(meta.oracle_ids(), bivariate_product, eval);
+	let n_vars = comp.n_vars();
+	let mut oracle_ids = comp.inner().clone();
+	oracle_ids.push(meta.multiplier_id); // eq
+
+	// Var(comp.n_polys()) corresponds to the eq MLE (meta.multiplier_id)
+	let expr = <_ as CompositionPoly<F>>::expression(comp.c()) * ArithExpr::Var(comp.n_polys());
+	if n_vars > constraint_builders.len() {
+		constraint_builders.resize_with(n_vars, || ConstraintSetBuilder::new());
+	}
+	constraint_builders[n_vars - 1].add_sumcheck(oracle_ids, expr, eval);
 }
 
 /// Creates bivariate witness and adds them to the witness index, and add bivariate sumcheck constraint to the [`ConstraintSetBuilder`]
@@ -185,7 +209,8 @@ where
 
 #[derive(Clone, Copy)]
 pub struct ProjectedBivariateMeta {
-	inner_id: OracleId,
+	/// `Some` if shifted / packed, `None` if composite
+	inner_id: Option<OracleId>,
 	projected_id: Option<OracleId>,
 	multiplier_id: OracleId,
 	projected_n_vars: usize,
@@ -194,7 +219,10 @@ pub struct ProjectedBivariateMeta {
 impl ProjectedBivariateMeta {
 	pub fn oracle_ids(&self) -> [OracleId; 2] {
 		[
-			self.projected_id.unwrap_or(self.inner_id),
+			self.projected_id.unwrap_or_else(|| {
+				self.inner_id
+					.expect("oracle_ids() is only defined for shifted / packed")
+			}),
 			self.multiplier_id,
 		]
 	}
@@ -227,7 +255,7 @@ fn projected_bivariate_meta<F: TowerField, T: MultivariatePoly<F> + 'static>(
 		oracles.add_transparent(multiplier_transparent_ctr(projected_eval_point)?)?;
 
 	let meta = ProjectedBivariateMeta {
-		inner_id,
+		inner_id: Some(inner_id),
 		projected_id,
 		multiplier_id,
 		projected_n_vars,
@@ -273,48 +301,107 @@ where
 	Ok(())
 }
 
+/// shifted / packed oracle -> compute the projected MLE (i.e. the inner oracle evaluated on the projected eval_point)
+/// composite oracle -> None
+#[allow(clippy::type_complexity)]
 pub fn calculate_projected_mles<U, F, Backend>(
 	metas: &[ProjectedBivariateMeta],
 	memoized_queries: &mut MemoizedQueries<PackedType<U, F>, Backend>,
 	projected_bivariate_claims: &[EvalcheckMultilinearClaim<F>],
 	witness_index: &MultilinearExtensionIndex<U, F>,
 	backend: &Backend,
-) -> Result<Vec<MultilinearExtension<PackedType<U, F>>>, Error>
+) -> Result<Vec<Option<MultilinearExtension<PackedType<U, F>>>>, Error>
 where
 	U: UnderlierType + PackScalar<F>,
 	F: TowerField,
 	Backend: ComputationBackend,
 {
-	let inner_multilins = metas
-		.iter()
-		.map(|meta| {
-			witness_index
-				.get_multilin_poly(meta.inner_id)
-				.map_err(Error::from)
-		})
-		.collect::<Result<Vec<_>, Error>>()?;
-
-	// Memoize queries for calculate_projected_mle
+	let mut queries_to_memoize = Vec::new();
 	for (meta, claim) in metas.iter().zip(projected_bivariate_claims) {
-		let eval_point = &claim.eval_point[meta.projected_n_vars..];
-		memoized_queries.full_query(eval_point, backend)?;
+		if meta.inner_id.is_some() {
+			// packed / shifted
+			queries_to_memoize.push(&claim.eval_point[meta.projected_n_vars..]);
+		}
 	}
+	memoized_queries.memoize_query_par(&queries_to_memoize, backend)?;
 
-	inner_multilins
+	projected_bivariate_claims
 		.par_iter()
-		.zip(projected_bivariate_claims)
 		.zip(metas)
-		.map(|((inner_multilin, claim), meta)| {
-			let eval_point = &claim.eval_point[meta.projected_n_vars..];
-			let query = memoized_queries
-				.full_query_readonly(eval_point)
-				.ok_or(Error::MissingQuery)?;
-
-			backend
-				.evaluate_partial_high(&inner_multilin, query.to_ref())
-				.map_err(Error::from)
+		.map(|(claim, meta)| {
+			match meta.inner_id {
+				Some(inner_id) => {
+					{
+						// packed / shifted
+						let inner_multilin = witness_index.get_multilin_poly(inner_id)?;
+						let eval_point = &claim.eval_point[meta.projected_n_vars..];
+						let query = memoized_queries
+							.full_query_readonly(eval_point)
+							.ok_or(Error::MissingQuery)?;
+						Ok(Some(
+							backend
+								.evaluate_partial_high(&inner_multilin, query.to_ref())
+								.map_err(Error::from)?,
+						))
+					}
+				}
+				None => Ok(None), // composite
+			}
+		})
+		.collect::<Result<Vec<Option<_>>, Error>>()
+}
+
+/// Each composite oracle induces a new eq oracle, for which we need to fill the witness
+pub fn fill_eq_witness_for_composites<U, F, Backend>(
+	metas: &[ProjectedBivariateMeta],
+	memoized_queries: &mut MemoizedQueries<PackedType<U, F>, Backend>,
+	projected_bivariate_claims: &[EvalcheckMultilinearClaim<F>],
+	witness_index: &mut MultilinearExtensionIndex<U, F>,
+	backend: &Backend,
+) -> Result<(), Error>
+where
+	U: UnderlierType + PackScalar<F>,
+	F: TowerField,
+	Backend: ComputationBackend,
+{
+	let dedup_eval_points = metas
+		.iter()
+		.zip(projected_bivariate_claims)
+		.filter(|(meta, _)| meta.inner_id.is_none())
+		.map(|(_, claim)| claim.eval_point.as_ref())
+		.collect::<HashSet<_>>();
+
+	memoized_queries
+		.memoize_query_par(&dedup_eval_points.iter().copied().collect::<Vec<_>>(), backend)?;
+
+	let eq_indicators = dedup_eval_points
+		.into_iter()
+		.map(|eval_point| {
+			let mle = MLEDirectAdapter::from(MultilinearExtension::from_values(
+				memoized_queries
+					.full_query_readonly(eval_point)
+					.expect("computed above")
+					.expansion()
+					.to_vec(),
+			)?)
+			.upcast_arc_dyn();
+			Ok((eval_point, mle))
 		})
-		.collect::<Result<Vec<_>, Error>>()
+		.collect::<Result<HashMap<_, _>, Error>>()?;
+
+	for (meta, claim) in metas
+		.iter()
+		.zip(projected_bivariate_claims)
+		.filter(|(meta, _)| meta.inner_id.is_none())
+	{
+		let eq_ind = eq_indicators
+			.get(claim.eval_point.as_ref())
+			.expect("was added above");
+
+		witness_index.update_multilin_poly(vec![(meta.multiplier_id, eq_ind.clone())])?;
+	}
+
+	Ok(())
 }
 
 #[allow(clippy::type_complexity)]
@@ -375,10 +462,10 @@ impl<P: PackedField, Backend: ComputationBackend> MemoizedQueries<P, Backend> {
 
 	pub fn memoize_query_par(
 		&mut self,
-		eval_points: Vec<&[P::Scalar]>,
+		eval_points: &[&[P::Scalar]],
 		backend: &Backend,
 	) -> Result<(), Error> {
-		let deduplicated_eval_points = eval_points.into_iter().collect::<HashSet<_>>();
+		let deduplicated_eval_points = eval_points.iter().collect::<HashSet<_>>();
 
 		let new_queries = deduplicated_eval_points
 			.into_par_iter()
diff --git a/crates/core/src/protocols/evalcheck/tests.rs b/crates/core/src/protocols/evalcheck/tests.rs
index 9fdb5c1cf..0ef16ad72 100644
--- a/crates/core/src/protocols/evalcheck/tests.rs
+++ b/crates/core/src/protocols/evalcheck/tests.rs
@@ -10,14 +10,17 @@ use binius_field::{
 	PackedBinaryField1x128b, PackedField, TowerField,
 };
 use binius_hal::{make_portable_backend, ComputationBackendExt};
-use binius_math::{extrapolate_line, MultilinearExtension, MultilinearPoly, MultilinearQuery};
+use binius_macros::arith_expr;
+use binius_math::{
+	extrapolate_line, CompositionPoly, MultilinearExtension, MultilinearPoly, MultilinearQuery,
+};
 use bytemuck::cast_slice_mut;
 use itertools::Either;
 use rand::{rngs::StdRng, thread_rng, SeedableRng};
 
 use crate::{
 	oracle::{MultilinearOracleSet, ShiftVariant},
-	polynomial::MultivariatePoly,
+	polynomial::{ArithCircuitPoly, MultivariatePoly},
 	protocols::evalcheck::{
 		deserialize_evalcheck_proof, serialize_evalcheck_proof, EvalcheckMultilinearClaim,
 		EvalcheckProof, EvalcheckProver, EvalcheckVerifier,
@@ -321,6 +324,94 @@ fn test_evalcheck_linear_combination_size_one() {
 	verifier_state.verify(vec![claim], proof).unwrap();
 }
 
+#[test]
+fn test_evalcheck_composite() {
+	let n_vars = 8;
+
+	let select_row1 = SelectRow::new(n_vars, 0).unwrap();
+	let select_row2 = SelectRow::new(n_vars, 5).unwrap();
+	let select_row3 = SelectRow::new(n_vars, 10).unwrap();
+
+	let mut oracles = MultilinearOracleSet::new();
+
+	let select_row1_oracle_id = oracles.add_transparent(select_row1.clone()).unwrap();
+	let select_row2_oracle_id = oracles.add_transparent(select_row2.clone()).unwrap();
+	let select_row3_oracle_id = oracles.add_transparent(select_row3.clone()).unwrap();
+
+	let comp = arith_expr!(BinaryField128b[x, y, z] = x * y * 15 + z * y * 8 + z * 2 + 77);
+
+	let composite_id = oracles
+		.add_composite_mle(
+			n_vars,
+			[
+				select_row1_oracle_id,
+				select_row2_oracle_id,
+				select_row3_oracle_id,
+			],
+			comp.clone(),
+		)
+		.unwrap();
+
+	let mut rng = StdRng::seed_from_u64(0);
+	let eval_point = repeat_with(|| <FExtension as Field>::random(&mut rng))
+		.take(n_vars)
+		.collect::<Vec<_>>();
+
+	let eval = ArithCircuitPoly::new(comp)
+		.evaluate(&[
+			select_row1.evaluate(&eval_point).unwrap(),
+			select_row2.evaluate(&eval_point).unwrap(),
+			select_row3.evaluate(&eval_point).unwrap(),
+		])
+		.unwrap();
+
+	let select_row1_witness = select_row1
+		.multilinear_extension::<PackedBinaryField128x1b>()
+		.unwrap();
+	let select_row2_witness = select_row2
+		.multilinear_extension::<PackedBinaryField128x1b>()
+		.unwrap();
+	let select_row3_witness = select_row3
+		.multilinear_extension::<PackedBinaryField128x1b>()
+		.unwrap();
+
+	let lin_com_values = (0..1 << n_vars)
+		.map(|i| {
+			select_row1_witness.evaluate_on_hypercube(i).unwrap() * FExtension::new(2)
+				+ select_row2_witness.evaluate_on_hypercube(i).unwrap() * FExtension::new(3)
+				+ select_row3_witness.evaluate_on_hypercube(i).unwrap() * FExtension::new(4)
+				+ FExtension::new(1)
+		})
+		.map(PackedBinaryField1x128b::set_single)
+		.collect();
+	let lin_com_witness = MultilinearExtension::from_values(lin_com_values).unwrap();
+
+	// Make the claim a composite oracle over a linear combination, in order to test the case
+	// of requiring nested composite evalcheck proofs.
+	let claim = EvalcheckMultilinearClaim {
+		id: composite_id,
+		eval_point: eval_point.into(),
+		eval,
+	};
+
+	let mut witness_index = MultilinearExtensionIndex::<U, FExtension>::new();
+	witness_index
+		.update_multilin_poly(vec![
+			(select_row1_oracle_id, select_row1_witness.specialize_arc_dyn()),
+			(select_row2_oracle_id, select_row2_witness.specialize_arc_dyn()),
+			(select_row3_oracle_id, select_row3_witness.specialize_arc_dyn()),
+			(composite_id, lin_com_witness.specialize_arc_dyn()),
+		])
+		.unwrap();
+
+	let backend = make_portable_backend();
+	let mut prover_state = EvalcheckProver::new(&mut oracles, &mut witness_index, &backend);
+	let proof = prover_state.prove(vec![claim.clone()]).unwrap();
+
+	let mut verifier_state = EvalcheckVerifier::<FExtension>::new(&mut oracles);
+	verifier_state.verify(vec![claim], proof).unwrap();
+}
+
 #[test]
 fn test_evalcheck_repeating() {
 	let n_vars = 7;
diff --git a/crates/core/src/protocols/evalcheck/verify.rs b/crates/core/src/protocols/evalcheck/verify.rs
index de865b24d..7df9549a4 100644
--- a/crates/core/src/protocols/evalcheck/verify.rs
+++ b/crates/core/src/protocols/evalcheck/verify.rs
@@ -11,7 +11,8 @@ use super::{
 	error::{Error, VerificationError},
 	evalcheck::{EvalcheckMultilinearClaim, EvalcheckProof},
 	subclaims::{
-		add_bivariate_sumcheck_to_constraints, packed_sumcheck_meta, shifted_sumcheck_meta,
+		add_bivariate_sumcheck_to_constraints, add_composite_sumcheck_to_constraints,
+		composite_sumcheck_meta, packed_sumcheck_meta, shifted_sumcheck_meta,
 	},
 };
 use crate::oracle::{
@@ -92,10 +93,9 @@ impl<'a, F: TowerField> EvalcheckVerifier<'a, F> {
 		let multilinear = self.oracles.oracle(id);
 		match multilinear.variant.clone() {
 			MultilinearPolyVariant::Transparent(inner) => {
-				match evalcheck_proof {
-					EvalcheckProof::Transparent => {}
-					_ => return Err(VerificationError::SubproofMismatch.into()),
-				};
+				if evalcheck_proof != EvalcheckProof::Transparent {
+					return Err(VerificationError::SubproofMismatch.into());
+				}
 
 				let actual_eval = inner.poly().evaluate(&eval_point)?;
 				if actual_eval != eval {
@@ -107,9 +107,8 @@ impl<'a, F: TowerField> EvalcheckVerifier<'a, F> {
 			}
 
 			MultilinearPolyVariant::Committed => {
-				match evalcheck_proof {
-					EvalcheckProof::Committed => {}
-					_ => return Err(VerificationError::SubproofMismatch.into()),
+				if evalcheck_proof != EvalcheckProof::Committed {
+					return Err(VerificationError::SubproofMismatch.into());
 				}
 
 				let claim = EvalcheckMultilinearClaim {
@@ -159,10 +158,9 @@ impl<'a, F: TowerField> EvalcheckVerifier<'a, F> {
 			}
 
 			MultilinearPolyVariant::Shifted(shifted) => {
-				match evalcheck_proof {
-					EvalcheckProof::Shifted => {}
-					_ => return Err(VerificationError::SubproofMismatch.into()),
-				};
+				if evalcheck_proof != EvalcheckProof::Shifted {
+					return Err(VerificationError::SubproofMismatch.into());
+				}
 
 				let meta = shifted_sumcheck_meta(self.oracles, &shifted, &eval_point)?;
 				add_bivariate_sumcheck_to_constraints(
@@ -174,10 +172,9 @@ impl<'a, F: TowerField> EvalcheckVerifier<'a, F> {
 			}
 
 			MultilinearPolyVariant::Packed(packed) => {
-				match evalcheck_proof {
-					EvalcheckProof::Packed => {}
-					_ => return Err(VerificationError::SubproofMismatch.into()),
-				};
+				if evalcheck_proof != EvalcheckProof::Packed {
+					return Err(VerificationError::SubproofMismatch.into());
+				}
 
 				let meta = packed_sumcheck_meta(self.oracles, &packed, &eval_point)?;
 				add_bivariate_sumcheck_to_constraints(
@@ -244,6 +241,19 @@ impl<'a, F: TowerField> EvalcheckVerifier<'a, F> {
 					subclaim_eval_point,
 				)?;
 			}
+			MultilinearPolyVariant::Composite(composition) => {
+				if evalcheck_proof != EvalcheckProof::CompositeMLE {
+					return Err(VerificationError::SubproofMismatch.into());
+				}
+
+				let meta = composite_sumcheck_meta(self.oracles, &eval_point)?;
+				add_composite_sumcheck_to_constraints(
+					meta,
+					&mut self.new_sumcheck_constraints,
+					&composition,
+					eval,
+				)
+			}
 		}
 
 		Ok(())
diff --git a/crates/utils/src/serialization.rs b/crates/utils/src/serialization.rs
index a6782b367..eac708779 100644
--- a/crates/utils/src/serialization.rs
+++ b/crates/utils/src/serialization.rs
@@ -46,6 +46,8 @@ pub enum SerializationError {
 	DeserializerNameConflict { name: String },
 	#[error("FromUtf8Error: {0}")]
 	FromUtf8Error(#[from] std::string::FromUtf8Error),
+	#[error("Invalid construction of {name}")]
+	InvalidConstruction { name: &'static str },
 }
 
 // Copyright 2025 Irreducible Inc.


Reviews:
user: login: fkondej
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-02T12:17:54Z
body: Nice!

@djadjka please review evalcheck code
@bergkvist please review oracle serialization code
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T09:43:01Z
user: login: bergkvist
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T10:22:21Z
user: login: djadjka
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T10:35:51Z
user: login: djadjka
body: Evalcheck looks good to me
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T10:36:07Z
user: login: djadjka
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T12:48:30Z
user: login: djadjka
body: I agree that using ProjectedBivariate is not the best approach, but using separate structures, IMHO, would unnecessarily complicate the code. In general, it can still be named Bivariate since you use $eq\\_ind \cdot composition$.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T13:09:27Z
user: login: TomWambsgans
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T17:36:27Z
user: login: TomWambsgans
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T17:38:19Z
user: login: djadjka
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T10:30:07Z
user: login: TomWambsgans
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T11:54:05Z
user: login: djadjka
body: Here’s a small suggestion for improvement =) 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T13:23:04Z
user: login: TomWambsgans
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T13:50:26Z
user: login: TomWambsgans
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T13:53:47Z
user: login: djadjka
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T14:26:03Z
user: login: djadjka
body: looks good for me, ready for merge 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T14:26:43Z
state: APPROVED
author_association: NONE
submitted_at: 2025-03-07T15:36:56Z

Review comments:
diff_hunk: @@ -244,6 +244,21 @@ where
 				});
 			}
 		}
+		MultilinearPolyVariant::Composite(composition) => {
+			let uncombined_polys = composition
path: crates/core/src/constraint_system/validate.rs
body: Might be helpful to use the `MultilinearComposite` struct here to reuse code.
created_at: 2025-03-05T09:31:42Z
author_association: NONE
diff_hunk: @@ -722,6 +773,55 @@ impl<F: TowerField> LinearCombination<F> {
 	}
 }
 
+/// MLE of a multivariate polynomial evaluated on multilinear oracles.
+///
+/// i.e. the MLE of the evaluations of C(M1, M2, ..., Mn) on {0, 1}^μ where:
path: crates/core/src/oracle/multilinear.rs
body: tip: you can use LaTeX syntax in docstrings and it gets rendered correctly on https://docs.binius.xyz/binius_core/ because we've included the KaTeX JavaScript plugin there. So this will read better as:

```
/// i.e. the MLE of the evaluations of $C(M_1, M_2, \ldots, M_n)$ on $\{0, 1\}^\mu$ where:
```
created_at: 2025-03-05T09:40:50Z
author_association: NONE
diff_hunk: @@ -123,7 +124,7 @@ enum CircuitStep<F: Field> {
 /// and the object representing different polnomials can be stored in a homogeneous collection.
 #[derive(Debug, Clone)]
 pub struct ArithCircuitPoly<F: Field> {
-	expr: ArithExpr<F>,
+	pub(crate) expr: ArithExpr<F>,
path: crates/core/src/polynomial/arith_circuit.rs
user: login: bergkvist
body: Instead of making this field public, I recommend adding a method `pub fn expr(&self) -> &ArithExpr<F>` since that makes it easier to reason about potential mutation of the field. You can do this slightly less verbosely using the `Getters` derive macro.
created_at: 2025-03-05T10:22:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -383,6 +386,14 @@ where
 				self.incomplete_proof_claims
 					.insert(multilinear_id, eval_point, evalcheck_claim);
 			}
+
+			MultilinearPolyVariant::Composite(_) => {
path: crates/core/src/protocols/evalcheck/prove.rs
user: login: djadjka
body: Perhaps it would make sense to move it next to packed/shifted.
created_at: 2025-03-06T10:35:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -183,6 +208,44 @@ where
 	Ok(())
 }
 
+#[allow(clippy::too_many_arguments)]
+pub fn process_composite_sumcheck<U, F>(
+	comp: &CompositeMLE<F>,
+	meta: ProjectedBivariateMeta,
+	eval_point: &[F],
+	eval: F,
+	witness_index: &mut MultilinearExtensionIndex<U, F>,
+	constraint_builders: &mut Vec<ConstraintSetBuilder<F>>,
+	backend: &impl ComputationBackend,
+	projected: MultilinearExtension<PackedType<U, F>>,
+) -> Result<(), Error>
+where
+	U: UnderlierType + PackScalar<F>,
+	F: TowerField,
+{
+	process_projected_bivariate_witness(
path: crates/core/src/protocols/evalcheck/subclaims.rs
user: login: djadjka
body: You can just add poly to the witness without using process_projected_bivariate_witness, and you're right about parallelization. However, I think we can take data from memoized_queries since it's also tensor products.
created_at: 2025-03-06T12:48:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -183,6 +208,44 @@ where
 	Ok(())
 }
 
+#[allow(clippy::too_many_arguments)]
+pub fn process_composite_sumcheck<U, F>(
+	comp: &CompositeMLE<F>,
+	meta: ProjectedBivariateMeta,
+	eval_point: &[F],
+	eval: F,
+	witness_index: &mut MultilinearExtensionIndex<U, F>,
+	constraint_builders: &mut Vec<ConstraintSetBuilder<F>>,
+	backend: &impl ComputationBackend,
+	projected: MultilinearExtension<PackedType<U, F>>,
+) -> Result<(), Error>
+where
+	U: UnderlierType + PackScalar<F>,
+	F: TowerField,
+{
+	process_projected_bivariate_witness(
+		witness_index,
+		meta,
+		eval_point,
+		|_projected_eval_point| {
+			let eq = EqIndPartialEval::new(eval_point.to_vec());
+			let eq_mle = eq.multilinear_extension::<PackedType<U, F>, _>(backend)?;
+			Ok(MLEDirectAdapter::from(eq_mle).upcast_arc_dyn())
+		},
+		projected,
+	)?;
+
+	let mut oracle_ids = comp.inner().clone();
+	oracle_ids.push(meta.multiplier_id);
+
+	if comp.n_vars() > constraint_builders.len() {
+		constraint_builders.resize_with(comp.n_vars(), || ConstraintSetBuilder::new());
+	}
+	// Var(comp.n_polys()) corresponds to the eq MLE (meta.multiplier_id)
+	let expr = <_ as CompositionPoly<F>>::expression(comp.c()) * ArithExpr::Var(comp.n_polys());
path: crates/core/src/protocols/evalcheck/subclaims.rs
user: login: djadjka
body: you can use `add_composite_sumcheck_to_constraints` here 
created_at: 2025-03-06T13:00:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -126,7 +129,20 @@ pub fn packed_sumcheck_meta<F: TowerField>(
 	})
 }
 
-pub fn add_bivariate_sumcheck_to_constraints<F: Field>(
+pub fn composite_sumcheck_meta<F: TowerField>(
+	oracles: &mut MultilinearOracleSet<F>,
+	id: OracleId,
+	eval_point: &[F],
+) -> Result<ProjectedBivariateMeta, Error> {
+	Ok(ProjectedBivariateMeta {
+		multiplier_id: oracles.add_transparent(EqIndPartialEval::new(eval_point.to_vec()))?,
+		inner_id: id,
path: crates/core/src/protocols/evalcheck/subclaims.rs
user: login: djadjka
body: Maybe it makes sense to do inner_id as an Option too; it would allow you to filter projected and composite in calculate_projected_mles.
created_at: 2025-03-06T13:02:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -126,7 +129,20 @@ pub fn packed_sumcheck_meta<F: TowerField>(
 	})
 }
 
-pub fn add_bivariate_sumcheck_to_constraints<F: Field>(
+pub fn composite_sumcheck_meta<F: TowerField>(
+	oracles: &mut MultilinearOracleSet<F>,
+	id: OracleId,
+	eval_point: &[F],
+) -> Result<ProjectedBivariateMeta, Error> {
+	Ok(ProjectedBivariateMeta {
+		multiplier_id: oracles.add_transparent(EqIndPartialEval::new(eval_point.to_vec()))?,
+		inner_id: id,
path: crates/core/src/protocols/evalcheck/subclaims.rs
user: login: TomWambsgans
body: I made `inner_id` an Option, and I made `calculate_projected_mles` compute the eq MLE for composite oracles. It is better, thanks for the suggestion. I still find the distinction between shifted / packed / composite oracles hard to follow in `subclaims.rs`, but maybe it is ok like this ? I could try to use an enum for `ProjectedBivariateMeta` ?
created_at: 2025-03-06T17:36:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -126,7 +129,20 @@ pub fn packed_sumcheck_meta<F: TowerField>(
 	})
 }
 
-pub fn add_bivariate_sumcheck_to_constraints<F: Field>(
+pub fn composite_sumcheck_meta<F: TowerField>(
+	oracles: &mut MultilinearOracleSet<F>,
+	id: OracleId,
+	eval_point: &[F],
+) -> Result<ProjectedBivariateMeta, Error> {
+	Ok(ProjectedBivariateMeta {
+		multiplier_id: oracles.add_transparent(EqIndPartialEval::new(eval_point.to_vec()))?,
+		inner_id: id,
path: crates/core/src/protocols/evalcheck/subclaims.rs
user: login: TomWambsgans
body: (I renamed `calculate_projected_mles` to  `calculate_meta_mles` because for the composite oracles it is not really a projection, simply an eq MLE). If you have a better name I am interested.
created_at: 2025-03-06T17:38:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -183,9 +207,33 @@ where
 	Ok(())
 }
 
+#[allow(clippy::too_many_arguments)]
+pub fn process_composite_sumcheck<U, F>(
+	comp: &CompositeMLE<F>,
+	meta: ProjectedBivariateMeta,
+	eval: F,
+	witness_index: &mut MultilinearExtensionIndex<U, F>,
+	constraint_builders: &mut Vec<ConstraintSetBuilder<F>>,
+	eq_mle: MultilinearExtension<PackedType<U, F>>,
+) -> Result<(), Error>
+where
+	U: UnderlierType + PackScalar<F>,
+	F: TowerField,
+{
+	if !witness_index.has(meta.multiplier_id) {
path: crates/core/src/protocols/evalcheck/subclaims.rs
user: login: djadjka
body: Unfortunately, this block will always execute because you add a new multiplier_id for every composite case
created_at: 2025-03-07T10:03:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -285,34 +338,44 @@ where
 	F: TowerField,
 	Backend: ComputationBackend,
 {
-	let inner_multilins = metas
-		.iter()
-		.map(|meta| {
-			witness_index
-				.get_multilin_poly(meta.inner_id)
-				.map_err(Error::from)
-		})
-		.collect::<Result<Vec<_>, Error>>()?;
-
-	// Memoize queries for calculate_projected_mle
+	let mut queries_to_memoize = Vec::new();
 	for (meta, claim) in metas.iter().zip(projected_bivariate_claims) {
-		let eval_point = &claim.eval_point[meta.projected_n_vars..];
-		memoized_queries.full_query(eval_point, backend)?;
+		let point = if meta.inner_id.is_some() {
+			// packed / shifted
+			&claim.eval_point[meta.projected_n_vars..]
+		} else {
+			// composite
+			&claim.eval_point
+		};
+		queries_to_memoize.push(point);
 	}
+	memoized_queries.memoize_query_par(queries_to_memoize, backend)?;
 
-	inner_multilins
+	projected_bivariate_claims
 		.par_iter()
-		.zip(projected_bivariate_claims)
 		.zip(metas)
-		.map(|((inner_multilin, claim), meta)| {
-			let eval_point = &claim.eval_point[meta.projected_n_vars..];
-			let query = memoized_queries
-				.full_query_readonly(eval_point)
-				.ok_or(Error::MissingQuery)?;
-
-			backend
-				.evaluate_partial_high(&inner_multilin, query.to_ref())
+		.map(|(claim, meta)| {
+			if let Some(inner_id) = meta.inner_id {
+				// packed / shifted
+				let inner_multilin = witness_index.get_multilin_poly(inner_id)?;
+				let eval_point = &claim.eval_point[meta.projected_n_vars..];
+				let query = memoized_queries
+					.full_query_readonly(eval_point)
+					.ok_or(Error::MissingQuery)?;
+				backend
+					.evaluate_partial_high(&inner_multilin, query.to_ref())
+					.map_err(Error::from)
+			} else {
+				// composite
+				MultilinearExtension::from_values(
+					memoized_queries
+						.full_query_readonly(&claim.eval_point)
+						.ok_or(Error::MissingQuery)?
+						.expansion()
+						.to_vec(), // TODO avoid clone
path: crates/core/src/protocols/evalcheck/subclaims.rs
user: login: djadjka
body: I think the best solution will be to create a separate function that adds witnesses of `eq_ind`. This way, it will be possible to create only one `MLEDirectAdapter::(x).upcast_arc_dyn() `for every eval_point and simply clone it a few times (which will be fast because it's an Arc).
created_at: 2025-03-07T10:05:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -183,9 +207,33 @@ where
 	Ok(())
 }
 
+#[allow(clippy::too_many_arguments)]
+pub fn process_composite_sumcheck<U, F>(
+	comp: &CompositeMLE<F>,
+	meta: ProjectedBivariateMeta,
+	eval: F,
+	witness_index: &mut MultilinearExtensionIndex<U, F>,
+	constraint_builders: &mut Vec<ConstraintSetBuilder<F>>,
+	eq_mle: MultilinearExtension<PackedType<U, F>>,
+) -> Result<(), Error>
+where
+	U: UnderlierType + PackScalar<F>,
+	F: TowerField,
+{
+	if !witness_index.has(meta.multiplier_id) {
+		witness_index.update_multilin_poly(vec![(
+			meta.multiplier_id,
+			MLEDirectAdapter::from(eq_mle).upcast_arc_dyn(),
+		)])?;
+	}
+
+	add_composite_sumcheck_to_constraints(meta, constraint_builders, comp, eval);
+	Ok(())
+}
+
 #[derive(Clone, Copy)]
 pub struct ProjectedBivariateMeta {
-	inner_id: OracleId,
+	inner_id: Option<OracleId>, // Some if shifted / packed, None if composite
path: crates/core/src/protocols/evalcheck/subclaims.rs
user: login: djadjka
body: if you don't mind, could you please add this comment as a Rust doc comment?
Thank you! 
created_at: 2025-03-07T10:07:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -285,34 +338,44 @@ where
 	F: TowerField,
 	Backend: ComputationBackend,
 {
-	let inner_multilins = metas
-		.iter()
-		.map(|meta| {
-			witness_index
-				.get_multilin_poly(meta.inner_id)
-				.map_err(Error::from)
-		})
-		.collect::<Result<Vec<_>, Error>>()?;
-
-	// Memoize queries for calculate_projected_mle
+	let mut queries_to_memoize = Vec::new();
 	for (meta, claim) in metas.iter().zip(projected_bivariate_claims) {
-		let eval_point = &claim.eval_point[meta.projected_n_vars..];
-		memoized_queries.full_query(eval_point, backend)?;
+		let point = if meta.inner_id.is_some() {
path: crates/core/src/protocols/evalcheck/subclaims.rs
user: login: djadjka
body: That doesn't seem necessary because that point has already been added here.https://github.com/IrreducibleOSS/binius/blob/e2481a353f5b708dff3d7113bc65e9dfa4eb4d85/crates/core/src/protocols/evalcheck/prove.rs#L172
created_at: 2025-03-07T10:12:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -285,34 +338,44 @@ where
 	F: TowerField,
 	Backend: ComputationBackend,
 {
-	let inner_multilins = metas
-		.iter()
-		.map(|meta| {
-			witness_index
-				.get_multilin_poly(meta.inner_id)
-				.map_err(Error::from)
-		})
-		.collect::<Result<Vec<_>, Error>>()?;
-
-	// Memoize queries for calculate_projected_mle
+	let mut queries_to_memoize = Vec::new();
 	for (meta, claim) in metas.iter().zip(projected_bivariate_claims) {
-		let eval_point = &claim.eval_point[meta.projected_n_vars..];
-		memoized_queries.full_query(eval_point, backend)?;
+		let point = if meta.inner_id.is_some() {
path: crates/core/src/protocols/evalcheck/subclaims.rs
user: login: TomWambsgans
body: I think the line 172 only concerns the eval_points in `claims_without_evals` (which come from linear combinations and zero padded oracles) ? if I am right, we need to memoize again for the eq MLE coming from composite oracles (which I do in the new function `fill_eq_witness_for_composites` I have written following your suggestion. Please correct me if I am wrong
created_at: 2025-03-07T11:54:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,223 @@
+// Copyright 2024-2025 Irreducible Inc.
path: crates/circuits/tests/composite.rs
user: login: djadjka
body: Sorry, please move this test to the crates/circuits/src/lib.rs
created_at: 2025-03-07T12:59:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -273,48 +301,102 @@ where
 	Ok(())
 }
 
+/// shifted / packed oracle -> compute the projected MLE (i.e. the inner oracle evaluated on the projected eval_point)
+/// composite oracle -> None
+#[allow(clippy::type_complexity)]
 pub fn calculate_projected_mles<U, F, Backend>(
 	metas: &[ProjectedBivariateMeta],
 	memoized_queries: &mut MemoizedQueries<PackedType<U, F>, Backend>,
 	projected_bivariate_claims: &[EvalcheckMultilinearClaim<F>],
 	witness_index: &MultilinearExtensionIndex<U, F>,
 	backend: &Backend,
-) -> Result<Vec<MultilinearExtension<PackedType<U, F>>>, Error>
+) -> Result<Vec<Option<MultilinearExtension<PackedType<U, F>>>>, Error>
 where
 	U: UnderlierType + PackScalar<F>,
 	F: TowerField,
 	Backend: ComputationBackend,
 {
-	let inner_multilins = metas
-		.iter()
-		.map(|meta| {
-			witness_index
-				.get_multilin_poly(meta.inner_id)
-				.map_err(Error::from)
-		})
-		.collect::<Result<Vec<_>, Error>>()?;
-
-	// Memoize queries for calculate_projected_mle
+	let mut queries_to_memoize = Vec::new();
 	for (meta, claim) in metas.iter().zip(projected_bivariate_claims) {
-		let eval_point = &claim.eval_point[meta.projected_n_vars..];
-		memoized_queries.full_query(eval_point, backend)?;
+		if meta.inner_id.is_some() {
+			// packed / shifted
+			queries_to_memoize.push(&claim.eval_point[meta.projected_n_vars..]);
+		}
 	}
+	memoized_queries.memoize_query_par(&queries_to_memoize, backend)?;
 
-	inner_multilins
+	projected_bivariate_claims
 		.par_iter()
-		.zip(projected_bivariate_claims)
 		.zip(metas)
-		.map(|((inner_multilin, claim), meta)| {
-			let eval_point = &claim.eval_point[meta.projected_n_vars..];
-			let query = memoized_queries
-				.full_query_readonly(eval_point)
-				.ok_or(Error::MissingQuery)?;
-
-			backend
-				.evaluate_partial_high(&inner_multilin, query.to_ref())
-				.map_err(Error::from)
+		.map(|(claim, meta)| {
+			match meta.inner_id {
+				Some(inner_id) => {
+					{
+						// packed / shifted
+						let inner_multilin = witness_index.get_multilin_poly(inner_id)?;
+						let eval_point = &claim.eval_point[meta.projected_n_vars..];
+						let query = memoized_queries
+							.full_query_readonly(eval_point)
+							.ok_or(Error::MissingQuery)?;
+						Ok(Some(
+							backend
+								.evaluate_partial_high(&inner_multilin, query.to_ref())
+								.map_err(Error::from)?,
+						))
+					}
+				}
+				None => Ok(None), // composite
+			}
 		})
-		.collect::<Result<Vec<_>, Error>>()
+		.collect::<Result<Vec<Option<_>>, Error>>()
+}
+
+/// Each composite oracle induces a new eq oracle, for which we need to fill the witness
+pub fn fill_eq_witness_for_composites<U, F, Backend>(
+	metas: &[ProjectedBivariateMeta],
+	memoized_queries: &mut MemoizedQueries<PackedType<U, F>, Backend>,
+	projected_bivariate_claims: &[EvalcheckMultilinearClaim<F>],
+	witness_index: &mut MultilinearExtensionIndex<U, F>,
+	backend: &Backend,
+) -> Result<(), Error>
+where
+	U: UnderlierType + PackScalar<F>,
+	F: TowerField,
+	Backend: ComputationBackend,
+{
+	let (composite_metas, composite_eval_points): (Vec<&ProjectedBivariateMeta>, Vec<&[F]>) =
+		Iterator::unzip(
+			metas
+				.iter()
+				.zip(projected_bivariate_claims)
+				.filter(|(meta, _)| meta.inner_id.is_none())
+				.map(|(meta, claim)| (meta, claim.eval_point.as_ref())),
+		);
+	memoized_queries.memoize_query_par(&composite_eval_points, backend)?;
+
+	// composite eval_point -> eq MLE witness
+	let eq_mles: HashMap<&[F], MultilinearWitness<'_, _>> = composite_eval_points
+		.iter()
+		.map(|eval_point| {
+			let mle = MLEDirectAdapter::from(MultilinearExtension::from_values(
+				memoized_queries
+					.full_query(eval_point, backend)?
+					.expansion()
+					.to_vec(),
path: crates/core/src/protocols/evalcheck/subclaims.rs
user: login: djadjka
body: Unfortunately, there is still unnecessary cloning for the same val_points going on here. `clone -> to Arc -> drop Arc`




created_at: 2025-03-07T13:03:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -273,48 +301,102 @@ where
 	Ok(())
 }
 
+/// shifted / packed oracle -> compute the projected MLE (i.e. the inner oracle evaluated on the projected eval_point)
+/// composite oracle -> None
+#[allow(clippy::type_complexity)]
 pub fn calculate_projected_mles<U, F, Backend>(
 	metas: &[ProjectedBivariateMeta],
 	memoized_queries: &mut MemoizedQueries<PackedType<U, F>, Backend>,
 	projected_bivariate_claims: &[EvalcheckMultilinearClaim<F>],
 	witness_index: &MultilinearExtensionIndex<U, F>,
 	backend: &Backend,
-) -> Result<Vec<MultilinearExtension<PackedType<U, F>>>, Error>
+) -> Result<Vec<Option<MultilinearExtension<PackedType<U, F>>>>, Error>
 where
 	U: UnderlierType + PackScalar<F>,
 	F: TowerField,
 	Backend: ComputationBackend,
 {
-	let inner_multilins = metas
-		.iter()
-		.map(|meta| {
-			witness_index
-				.get_multilin_poly(meta.inner_id)
-				.map_err(Error::from)
-		})
-		.collect::<Result<Vec<_>, Error>>()?;
-
-	// Memoize queries for calculate_projected_mle
+	let mut queries_to_memoize = Vec::new();
 	for (meta, claim) in metas.iter().zip(projected_bivariate_claims) {
-		let eval_point = &claim.eval_point[meta.projected_n_vars..];
-		memoized_queries.full_query(eval_point, backend)?;
+		if meta.inner_id.is_some() {
+			// packed / shifted
+			queries_to_memoize.push(&claim.eval_point[meta.projected_n_vars..]);
+		}
 	}
+	memoized_queries.memoize_query_par(&queries_to_memoize, backend)?;
 
-	inner_multilins
+	projected_bivariate_claims
 		.par_iter()
-		.zip(projected_bivariate_claims)
 		.zip(metas)
-		.map(|((inner_multilin, claim), meta)| {
-			let eval_point = &claim.eval_point[meta.projected_n_vars..];
-			let query = memoized_queries
-				.full_query_readonly(eval_point)
-				.ok_or(Error::MissingQuery)?;
-
-			backend
-				.evaluate_partial_high(&inner_multilin, query.to_ref())
-				.map_err(Error::from)
+		.map(|(claim, meta)| {
+			match meta.inner_id {
+				Some(inner_id) => {
+					{
+						// packed / shifted
+						let inner_multilin = witness_index.get_multilin_poly(inner_id)?;
+						let eval_point = &claim.eval_point[meta.projected_n_vars..];
+						let query = memoized_queries
+							.full_query_readonly(eval_point)
+							.ok_or(Error::MissingQuery)?;
+						Ok(Some(
+							backend
+								.evaluate_partial_high(&inner_multilin, query.to_ref())
+								.map_err(Error::from)?,
+						))
+					}
+				}
+				None => Ok(None), // composite
+			}
 		})
-		.collect::<Result<Vec<_>, Error>>()
+		.collect::<Result<Vec<Option<_>>, Error>>()
+}
+
+/// Each composite oracle induces a new eq oracle, for which we need to fill the witness
+pub fn fill_eq_witness_for_composites<U, F, Backend>(
+	metas: &[ProjectedBivariateMeta],
+	memoized_queries: &mut MemoizedQueries<PackedType<U, F>, Backend>,
+	projected_bivariate_claims: &[EvalcheckMultilinearClaim<F>],
+	witness_index: &mut MultilinearExtensionIndex<U, F>,
+	backend: &Backend,
+) -> Result<(), Error>
+where
+	U: UnderlierType + PackScalar<F>,
+	F: TowerField,
+	Backend: ComputationBackend,
+{
+	let (composite_metas, composite_eval_points): (Vec<&ProjectedBivariateMeta>, Vec<&[F]>) =
+		Iterator::unzip(
+			metas
+				.iter()
+				.zip(projected_bivariate_claims)
+				.filter(|(meta, _)| meta.inner_id.is_none())
+				.map(|(meta, claim)| (meta, claim.eval_point.as_ref())),
+		);
+	memoized_queries.memoize_query_par(&composite_eval_points, backend)?;
+
+	// composite eval_point -> eq MLE witness
+	let eq_mles: HashMap<&[F], MultilinearWitness<'_, _>> = composite_eval_points
+		.iter()
+		.map(|eval_point| {
+			let mle = MLEDirectAdapter::from(MultilinearExtension::from_values(
+				memoized_queries
+					.full_query(eval_point, backend)?
+					.expansion()
+					.to_vec(),
path: crates/core/src/protocols/evalcheck/subclaims.rs
user: login: djadjka
body: I would suggest doing it this way.

```rust
let dedup_eval_points = metas
		.iter()
		.zip(projected_bivariate_claims)
		.filter(|(meta, _)| meta.inner_id.is_none())
		.map(|(_, claim)| claim.eval_point.as_ref())
		.collect::<HashSet<_>>();

	let eq_indicators = dedup_eval_points
		.into_iter()
		.map(|eval_point| {
			let mle = MLEDirectAdapter::from(MultilinearExtension::from_values(
				memoized_queries
					.full_query(eval_point, backend)?
					.expansion()
					.to_vec(),
			)?)
			.upcast_arc_dyn();
			Ok((eval_point, mle))
		})
		.collect::<Result<HashMap<_, _>, Error>>()?;

	for (meta, claim) in metas
		.iter()
		.zip(projected_bivariate_claims)
		.filter(|(meta, _)| meta.inner_id.is_none())
	{
		let eq_ind = eq_indicators
			.get(claim.eval_point.as_ref())
			.expect("was added above");

		witness_index.update_multilin_poly(vec![(meta.multiplier_id, eq_ind_witness.clone())])?;
	}
```
created_at: 2025-03-07T13:05:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -324,60 +301,102 @@ where
 	Ok(())
 }
 
-/// For each composite oracle, compute the MLE of eq (on eval_point)
-/// For each shifted / packed oracle, compute the projected MLE (i.e. the inner oracle evaluated on the projected eval_point)
-pub fn calculate_meta_mles<U, F, Backend>(
+/// shifted / packed oracle -> compute the projected MLE (i.e. the inner oracle evaluated on the projected eval_point)
+/// composite oracle -> None
+#[allow(clippy::type_complexity)]
+pub fn calculate_projected_mles<U, F, Backend>(
 	metas: &[ProjectedBivariateMeta],
 	memoized_queries: &mut MemoizedQueries<PackedType<U, F>, Backend>,
 	projected_bivariate_claims: &[EvalcheckMultilinearClaim<F>],
 	witness_index: &MultilinearExtensionIndex<U, F>,
 	backend: &Backend,
-) -> Result<Vec<MultilinearExtension<PackedType<U, F>>>, Error>
+) -> Result<Vec<Option<MultilinearExtension<PackedType<U, F>>>>, Error>
 where
 	U: UnderlierType + PackScalar<F>,
 	F: TowerField,
 	Backend: ComputationBackend,
 {
 	let mut queries_to_memoize = Vec::new();
 	for (meta, claim) in metas.iter().zip(projected_bivariate_claims) {
-		let point = if meta.inner_id.is_some() {
+		if meta.inner_id.is_some() {
 			// packed / shifted
-			&claim.eval_point[meta.projected_n_vars..]
-		} else {
-			// composite
-			&claim.eval_point
-		};
-		queries_to_memoize.push(point);
+			queries_to_memoize.push(&claim.eval_point[meta.projected_n_vars..]);
+		}
 	}
-	memoized_queries.memoize_query_par(queries_to_memoize, backend)?;
+	memoized_queries.memoize_query_par(&queries_to_memoize, backend)?;
 
 	projected_bivariate_claims
 		.par_iter()
 		.zip(metas)
 		.map(|(claim, meta)| {
-			if let Some(inner_id) = meta.inner_id {
-				// packed / shifted
-				let inner_multilin = witness_index.get_multilin_poly(inner_id)?;
-				let eval_point = &claim.eval_point[meta.projected_n_vars..];
-				let query = memoized_queries
-					.full_query_readonly(eval_point)
-					.ok_or(Error::MissingQuery)?;
-				backend
-					.evaluate_partial_high(&inner_multilin, query.to_ref())
-					.map_err(Error::from)
-			} else {
-				// composite
-				MultilinearExtension::from_values(
-					memoized_queries
-						.full_query_readonly(&claim.eval_point)
-						.ok_or(Error::MissingQuery)?
-						.expansion()
-						.to_vec(), // TODO avoid clone
-				)
-				.map_err(Error::from)
+			match meta.inner_id {
+				Some(inner_id) => {
+					{
+						// packed / shifted
+						let inner_multilin = witness_index.get_multilin_poly(inner_id)?;
+						let eval_point = &claim.eval_point[meta.projected_n_vars..];
+						let query = memoized_queries
+							.full_query_readonly(eval_point)
+							.ok_or(Error::MissingQuery)?;
+						Ok(Some(
+							backend
+								.evaluate_partial_high(&inner_multilin, query.to_ref())
+								.map_err(Error::from)?,
+						))
+					}
+				}
+				None => Ok(None), // composite
 			}
 		})
-		.collect::<Result<Vec<_>, Error>>()
+		.collect::<Result<Vec<Option<_>>, Error>>()
+}
+
+/// Each composite oracle induces a new eq oracle, for which we need to fill the witness
+pub fn fill_eq_witness_for_composites<U, F, Backend>(
+	metas: &[ProjectedBivariateMeta],
+	memoized_queries: &mut MemoizedQueries<PackedType<U, F>, Backend>,
+	projected_bivariate_claims: &[EvalcheckMultilinearClaim<F>],
+	witness_index: &mut MultilinearExtensionIndex<U, F>,
+	backend: &Backend,
+) -> Result<(), Error>
+where
+	U: UnderlierType + PackScalar<F>,
+	F: TowerField,
+	Backend: ComputationBackend,
+{
+	let (composite_metas, composite_eval_points): (Vec<&ProjectedBivariateMeta>, Vec<&[F]>) =
+		Iterator::unzip(
+			metas
+				.iter()
+				.zip(projected_bivariate_claims)
+				.filter(|(meta, _)| meta.inner_id.is_none())
+				.map(|(meta, claim)| (meta, claim.eval_point.as_ref())),
+		);
+	memoized_queries.memoize_query_par(&composite_eval_points, backend)?;
path: crates/core/src/protocols/evalcheck/subclaims.rs
user: login: djadjka
body: And we can drop memoized_queries.memoize_query_par because full_query memorizes it. If I'm not mistaken, this will only happen for the evalcheck challenge, because you're right, and we don't add it to the memoization in the prove.
created_at: 2025-03-07T13:19:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -273,48 +301,102 @@ where
 	Ok(())
 }
 
+/// shifted / packed oracle -> compute the projected MLE (i.e. the inner oracle evaluated on the projected eval_point)
+/// composite oracle -> None
+#[allow(clippy::type_complexity)]
 pub fn calculate_projected_mles<U, F, Backend>(
 	metas: &[ProjectedBivariateMeta],
 	memoized_queries: &mut MemoizedQueries<PackedType<U, F>, Backend>,
 	projected_bivariate_claims: &[EvalcheckMultilinearClaim<F>],
 	witness_index: &MultilinearExtensionIndex<U, F>,
 	backend: &Backend,
-) -> Result<Vec<MultilinearExtension<PackedType<U, F>>>, Error>
+) -> Result<Vec<Option<MultilinearExtension<PackedType<U, F>>>>, Error>
 where
 	U: UnderlierType + PackScalar<F>,
 	F: TowerField,
 	Backend: ComputationBackend,
 {
-	let inner_multilins = metas
-		.iter()
-		.map(|meta| {
-			witness_index
-				.get_multilin_poly(meta.inner_id)
-				.map_err(Error::from)
-		})
-		.collect::<Result<Vec<_>, Error>>()?;
-
-	// Memoize queries for calculate_projected_mle
+	let mut queries_to_memoize = Vec::new();
 	for (meta, claim) in metas.iter().zip(projected_bivariate_claims) {
-		let eval_point = &claim.eval_point[meta.projected_n_vars..];
-		memoized_queries.full_query(eval_point, backend)?;
+		if meta.inner_id.is_some() {
+			// packed / shifted
+			queries_to_memoize.push(&claim.eval_point[meta.projected_n_vars..]);
+		}
 	}
+	memoized_queries.memoize_query_par(&queries_to_memoize, backend)?;
 
-	inner_multilins
+	projected_bivariate_claims
 		.par_iter()
-		.zip(projected_bivariate_claims)
 		.zip(metas)
-		.map(|((inner_multilin, claim), meta)| {
-			let eval_point = &claim.eval_point[meta.projected_n_vars..];
-			let query = memoized_queries
-				.full_query_readonly(eval_point)
-				.ok_or(Error::MissingQuery)?;
-
-			backend
-				.evaluate_partial_high(&inner_multilin, query.to_ref())
-				.map_err(Error::from)
+		.map(|(claim, meta)| {
+			match meta.inner_id {
+				Some(inner_id) => {
+					{
+						// packed / shifted
+						let inner_multilin = witness_index.get_multilin_poly(inner_id)?;
+						let eval_point = &claim.eval_point[meta.projected_n_vars..];
+						let query = memoized_queries
+							.full_query_readonly(eval_point)
+							.ok_or(Error::MissingQuery)?;
+						Ok(Some(
+							backend
+								.evaluate_partial_high(&inner_multilin, query.to_ref())
+								.map_err(Error::from)?,
+						))
+					}
+				}
+				None => Ok(None), // composite
+			}
 		})
-		.collect::<Result<Vec<_>, Error>>()
+		.collect::<Result<Vec<Option<_>>, Error>>()
+}
+
+/// Each composite oracle induces a new eq oracle, for which we need to fill the witness
+pub fn fill_eq_witness_for_composites<U, F, Backend>(
+	metas: &[ProjectedBivariateMeta],
+	memoized_queries: &mut MemoizedQueries<PackedType<U, F>, Backend>,
+	projected_bivariate_claims: &[EvalcheckMultilinearClaim<F>],
+	witness_index: &mut MultilinearExtensionIndex<U, F>,
+	backend: &Backend,
+) -> Result<(), Error>
+where
+	U: UnderlierType + PackScalar<F>,
+	F: TowerField,
+	Backend: ComputationBackend,
+{
+	let (composite_metas, composite_eval_points): (Vec<&ProjectedBivariateMeta>, Vec<&[F]>) =
+		Iterator::unzip(
+			metas
+				.iter()
+				.zip(projected_bivariate_claims)
+				.filter(|(meta, _)| meta.inner_id.is_none())
+				.map(|(meta, claim)| (meta, claim.eval_point.as_ref())),
+		);
+	memoized_queries.memoize_query_par(&composite_eval_points, backend)?;
+
+	// composite eval_point -> eq MLE witness
+	let eq_mles: HashMap<&[F], MultilinearWitness<'_, _>> = composite_eval_points
+		.iter()
+		.map(|eval_point| {
+			let mle = MLEDirectAdapter::from(MultilinearExtension::from_values(
+				memoized_queries
+					.full_query(eval_point, backend)?
+					.expansion()
+					.to_vec(),
path: crates/core/src/protocols/evalcheck/subclaims.rs
user: login: TomWambsgans
body: > Unfortunately, there is still unnecessary cloning for the same val_points going on here. `clone -> to Arc -> drop Arc`
  
Yes indeed, thanks for catching it. Your code looks good to me, I will use it. I just added a `memoized_queries
		.memoize_query_par(&dedup_eval_points, backend)` instead of the sequential  `full_query`.
created_at: 2025-03-07T13:50:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -324,60 +301,102 @@ where
 	Ok(())
 }
 
-/// For each composite oracle, compute the MLE of eq (on eval_point)
-/// For each shifted / packed oracle, compute the projected MLE (i.e. the inner oracle evaluated on the projected eval_point)
-pub fn calculate_meta_mles<U, F, Backend>(
+/// shifted / packed oracle -> compute the projected MLE (i.e. the inner oracle evaluated on the projected eval_point)
+/// composite oracle -> None
+#[allow(clippy::type_complexity)]
+pub fn calculate_projected_mles<U, F, Backend>(
 	metas: &[ProjectedBivariateMeta],
 	memoized_queries: &mut MemoizedQueries<PackedType<U, F>, Backend>,
 	projected_bivariate_claims: &[EvalcheckMultilinearClaim<F>],
 	witness_index: &MultilinearExtensionIndex<U, F>,
 	backend: &Backend,
-) -> Result<Vec<MultilinearExtension<PackedType<U, F>>>, Error>
+) -> Result<Vec<Option<MultilinearExtension<PackedType<U, F>>>>, Error>
 where
 	U: UnderlierType + PackScalar<F>,
 	F: TowerField,
 	Backend: ComputationBackend,
 {
 	let mut queries_to_memoize = Vec::new();
 	for (meta, claim) in metas.iter().zip(projected_bivariate_claims) {
-		let point = if meta.inner_id.is_some() {
+		if meta.inner_id.is_some() {
 			// packed / shifted
-			&claim.eval_point[meta.projected_n_vars..]
-		} else {
-			// composite
-			&claim.eval_point
-		};
-		queries_to_memoize.push(point);
+			queries_to_memoize.push(&claim.eval_point[meta.projected_n_vars..]);
+		}
 	}
-	memoized_queries.memoize_query_par(queries_to_memoize, backend)?;
+	memoized_queries.memoize_query_par(&queries_to_memoize, backend)?;
 
 	projected_bivariate_claims
 		.par_iter()
 		.zip(metas)
 		.map(|(claim, meta)| {
-			if let Some(inner_id) = meta.inner_id {
-				// packed / shifted
-				let inner_multilin = witness_index.get_multilin_poly(inner_id)?;
-				let eval_point = &claim.eval_point[meta.projected_n_vars..];
-				let query = memoized_queries
-					.full_query_readonly(eval_point)
-					.ok_or(Error::MissingQuery)?;
-				backend
-					.evaluate_partial_high(&inner_multilin, query.to_ref())
-					.map_err(Error::from)
-			} else {
-				// composite
-				MultilinearExtension::from_values(
-					memoized_queries
-						.full_query_readonly(&claim.eval_point)
-						.ok_or(Error::MissingQuery)?
-						.expansion()
-						.to_vec(), // TODO avoid clone
-				)
-				.map_err(Error::from)
+			match meta.inner_id {
+				Some(inner_id) => {
+					{
+						// packed / shifted
+						let inner_multilin = witness_index.get_multilin_poly(inner_id)?;
+						let eval_point = &claim.eval_point[meta.projected_n_vars..];
+						let query = memoized_queries
+							.full_query_readonly(eval_point)
+							.ok_or(Error::MissingQuery)?;
+						Ok(Some(
+							backend
+								.evaluate_partial_high(&inner_multilin, query.to_ref())
+								.map_err(Error::from)?,
+						))
+					}
+				}
+				None => Ok(None), // composite
 			}
 		})
-		.collect::<Result<Vec<_>, Error>>()
+		.collect::<Result<Vec<Option<_>>, Error>>()
+}
+
+/// Each composite oracle induces a new eq oracle, for which we need to fill the witness
+pub fn fill_eq_witness_for_composites<U, F, Backend>(
+	metas: &[ProjectedBivariateMeta],
+	memoized_queries: &mut MemoizedQueries<PackedType<U, F>, Backend>,
+	projected_bivariate_claims: &[EvalcheckMultilinearClaim<F>],
+	witness_index: &mut MultilinearExtensionIndex<U, F>,
+	backend: &Backend,
+) -> Result<(), Error>
+where
+	U: UnderlierType + PackScalar<F>,
+	F: TowerField,
+	Backend: ComputationBackend,
+{
+	let (composite_metas, composite_eval_points): (Vec<&ProjectedBivariateMeta>, Vec<&[F]>) =
+		Iterator::unzip(
+			metas
+				.iter()
+				.zip(projected_bivariate_claims)
+				.filter(|(meta, _)| meta.inner_id.is_none())
+				.map(|(meta, claim)| (meta, claim.eval_point.as_ref())),
+		);
+	memoized_queries.memoize_query_par(&composite_eval_points, backend)?;
path: crates/core/src/protocols/evalcheck/subclaims.rs
user: login: TomWambsgans
body: Why not doing it in parrallel with `memoize_query_par`  (instead of consecutive `full_query`) ? I am not sure to understand 
created_at: 2025-03-07T13:53:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -324,60 +301,102 @@ where
 	Ok(())
 }
 
-/// For each composite oracle, compute the MLE of eq (on eval_point)
-/// For each shifted / packed oracle, compute the projected MLE (i.e. the inner oracle evaluated on the projected eval_point)
-pub fn calculate_meta_mles<U, F, Backend>(
+/// shifted / packed oracle -> compute the projected MLE (i.e. the inner oracle evaluated on the projected eval_point)
+/// composite oracle -> None
+#[allow(clippy::type_complexity)]
+pub fn calculate_projected_mles<U, F, Backend>(
 	metas: &[ProjectedBivariateMeta],
 	memoized_queries: &mut MemoizedQueries<PackedType<U, F>, Backend>,
 	projected_bivariate_claims: &[EvalcheckMultilinearClaim<F>],
 	witness_index: &MultilinearExtensionIndex<U, F>,
 	backend: &Backend,
-) -> Result<Vec<MultilinearExtension<PackedType<U, F>>>, Error>
+) -> Result<Vec<Option<MultilinearExtension<PackedType<U, F>>>>, Error>
 where
 	U: UnderlierType + PackScalar<F>,
 	F: TowerField,
 	Backend: ComputationBackend,
 {
 	let mut queries_to_memoize = Vec::new();
 	for (meta, claim) in metas.iter().zip(projected_bivariate_claims) {
-		let point = if meta.inner_id.is_some() {
+		if meta.inner_id.is_some() {
 			// packed / shifted
-			&claim.eval_point[meta.projected_n_vars..]
-		} else {
-			// composite
-			&claim.eval_point
-		};
-		queries_to_memoize.push(point);
+			queries_to_memoize.push(&claim.eval_point[meta.projected_n_vars..]);
+		}
 	}
-	memoized_queries.memoize_query_par(queries_to_memoize, backend)?;
+	memoized_queries.memoize_query_par(&queries_to_memoize, backend)?;
 
 	projected_bivariate_claims
 		.par_iter()
 		.zip(metas)
 		.map(|(claim, meta)| {
-			if let Some(inner_id) = meta.inner_id {
-				// packed / shifted
-				let inner_multilin = witness_index.get_multilin_poly(inner_id)?;
-				let eval_point = &claim.eval_point[meta.projected_n_vars..];
-				let query = memoized_queries
-					.full_query_readonly(eval_point)
-					.ok_or(Error::MissingQuery)?;
-				backend
-					.evaluate_partial_high(&inner_multilin, query.to_ref())
-					.map_err(Error::from)
-			} else {
-				// composite
-				MultilinearExtension::from_values(
-					memoized_queries
-						.full_query_readonly(&claim.eval_point)
-						.ok_or(Error::MissingQuery)?
-						.expansion()
-						.to_vec(), // TODO avoid clone
-				)
-				.map_err(Error::from)
+			match meta.inner_id {
+				Some(inner_id) => {
+					{
+						// packed / shifted
+						let inner_multilin = witness_index.get_multilin_poly(inner_id)?;
+						let eval_point = &claim.eval_point[meta.projected_n_vars..];
+						let query = memoized_queries
+							.full_query_readonly(eval_point)
+							.ok_or(Error::MissingQuery)?;
+						Ok(Some(
+							backend
+								.evaluate_partial_high(&inner_multilin, query.to_ref())
+								.map_err(Error::from)?,
+						))
+					}
+				}
+				None => Ok(None), // composite
 			}
 		})
-		.collect::<Result<Vec<_>, Error>>()
+		.collect::<Result<Vec<Option<_>>, Error>>()
+}
+
+/// Each composite oracle induces a new eq oracle, for which we need to fill the witness
+pub fn fill_eq_witness_for_composites<U, F, Backend>(
+	metas: &[ProjectedBivariateMeta],
+	memoized_queries: &mut MemoizedQueries<PackedType<U, F>, Backend>,
+	projected_bivariate_claims: &[EvalcheckMultilinearClaim<F>],
+	witness_index: &mut MultilinearExtensionIndex<U, F>,
+	backend: &Backend,
+) -> Result<(), Error>
+where
+	U: UnderlierType + PackScalar<F>,
+	F: TowerField,
+	Backend: ComputationBackend,
+{
+	let (composite_metas, composite_eval_points): (Vec<&ProjectedBivariateMeta>, Vec<&[F]>) =
+		Iterator::unzip(
+			metas
+				.iter()
+				.zip(projected_bivariate_claims)
+				.filter(|(meta, _)| meta.inner_id.is_none())
+				.map(|(meta, claim)| (meta, claim.eval_point.as_ref())),
+		);
+	memoized_queries.memoize_query_par(&composite_eval_points, backend)?;
path: crates/core/src/protocols/evalcheck/subclaims.rs
user: login: djadjka
body: Oh, sorry, I have yogurt for brains. I switched to your PR from another protocol where there was only one eval_point.
Yes, here theoretically there could be several points, and your code is better.
created_at: 2025-03-07T14:26:03Z
author_association: CONTRIBUTOR

PR comments:
user: login: TomWambsgans
created_at: 2025-03-02T05:18:43Z
body: NOT ready for merge: even though the new evalcheck test pass, it doesnt work end to end on [this project](https://github.com/TomWambsgans/conservative-xmss-aggregation/tree/binius-composite) (cargo run panics on `Evalcheck(Evalcheck(Verification(IncorrectEvaluation("Composition: merkle_table::pre_hash_main"))))`) . Investigation in progress 
author_association: CONTRIBUTOR
user: login: ghost
created_at: 2025-03-02T11:07:13Z
body: Nice job!! I'll review when it passes for your project. Please let me know if you need help investigating/debugging.
author_association: NONE
user: login: TomWambsgans
created_at: 2025-03-02T22:05:49Z
body: My mistake is that I've built a "true" composition of multilinears, instead of the MLE of the evaluations of a composition of multlinears on the hypercube. Should be fixed in the coming days
author_association: CONTRIBUTOR
user: login: ghost
created_at: 2025-03-03T08:42:01Z
body: > My mistake is that I've built a "true" composition of multilinears, instead of the MLE of the evaluations of a composition of multlinears on the hypercube. Should be fixed in the coming days

Yes, exactly. Let me know if you have questions about how to implement that.
author_association: NONE
user: login: TomWambsgans
created_at: 2025-03-04T19:22:54Z
body: It's already better, now it's working on the [other project](https://github.com/TomWambsgans/conservative-xmss-aggregation/tree/binius-composite). However, there are still some buggy configurations (c.f. my latest commit). I've still got some work to do
author_association: CONTRIBUTOR
user: login: bergkvist
created_at: 2025-03-05T10:50:57Z
body: Serialization logic looks good to me
author_association: CONTRIBUTOR
user: login: TomWambsgans
created_at: 2025-03-05T11:19:05Z
body: Thank you for your feedback. The suggestions have been committed. Now I need to debug the failing example. I don't like my naming (`MultilinearComposition` is confusing with`MultilinearComposite`), I will polish things once all bugs are fixed.
author_association: CONTRIBUTOR
user: login: ghost
created_at: 2025-03-05T13:08:43Z
body: > Thank you for your feedback. The suggestions have been committed. Now I need to debug the failing example. I don't like my naming (`MultilinearComposition` is confusing with`MultilinearComposite`), I will polish things once all bugs are fixed.

We use "composition" to refer to `C(X, Y, Z)` and "composite" to refer to `C(F_1(X_1, ..., X_N), F_2(X_1, ..., X_N), F_3(X_1, ..., X_N))`.
author_association: NONE
user: login: TomWambsgans
created_at: 2025-03-06T10:48:59Z
body: I have fixed the bug. I have written a [complex example](https://github.com/IrreducibleOSS/binius/pull/61/files#diff-85a24fd71c6355c0741a227c2d95209bda0e674046712ae9351b2affe348583f), which works fine. I changed some naming (`MultilinearPolyVariant::Composite(CompositeMLE)`. `CompositeMLE` to avoid the confusion with `MultilinearComposite`). 

I think (and hope) everything works now, but I still need to improve the code quality, especially in `subclaims.rs`:
I have reused the structure `ProjectedBivariateMeta` (previously used for shifted + packed oracles, which are the oracles whose evalcheck is reduced to a sumcheck). The new “composite” oracle also reduces an evalcheck to a sumcheck, but the expression in the sumcheck is not bivariate as in shift / pack. Instead, it is equal to the circuit of the composite oracle, multiplied by “eq” MLE. As a result, the naming in `subclaims.rs` is confusing currently because everything has been planned for a bivariate sumcheck.

I don’t know if I am supposed to keep the end-to-end example I have written, and if yes, in which file / folder to put it ? (Currently it is in `/examples`)

author_association: CONTRIBUTOR
user: login: TomWambsgans
created_at: 2025-03-06T11:02:09Z
body: Other remark: in [`process_sumcheck`](https://github.com/IrreducibleOSS/binius/blob/0a6bdc7c3897c0684323ce7331a1550b7e4167f1/crates/core/src/protocols/evalcheck/prove.rs#L236), for the composite oracles, I compute the witness for the eq MLE (c.f. `backend` which is passed as parameter, [here](https://github.com/IrreducibleOSS/binius/blob/0a6bdc7c3897c0684323ce7331a1550b7e4167f1/crates/core/src/protocols/evalcheck/prove.rs#L629)). This is bad because it is not parallelized. I should probably compute it in the previous function [calculate_projected_mles](https://github.com/IrreducibleOSS/binius/blob/0a6bdc7c3897c0684323ce7331a1550b7e4167f1/crates/core/src/protocols/evalcheck/prove.rs#L223). Currently, for composite oracles, I believe `calculate_projected_mles` re-computes the evaluation of the composite oracle on the `eval_point`, which is useless. Instead, I think it should compute the eq MLE associated with the composite oracle.

author_association: CONTRIBUTOR
user: login: djadjka
created_at: 2025-03-06T11:08:08Z
body: We mostly use examples for circuits, so I think you can move it to the tests and just add ignore if it's slow.

As for your other comments, let me check them again and I will return to you soon 


author_association: CONTRIBUTOR
user: login: ghost
created_at: 2025-03-06T17:34:29Z
body: This looks great! Can you please rebase this onto main and resolve the conflicts? It should be good to merge after that.
author_association: NONE
user: login: TomWambsgans
created_at: 2025-03-06T17:56:26Z
body: > This looks great! Can you please rebase this onto main and resolve the conflicts? It should be good to merge after that.

Done
author_association: CONTRIBUTOR
user: login: TomWambsgans
created_at: 2025-03-06T18:27:06Z
body: I think some additional work is required to improve the code quality in `subclaims.rs`, because the struct [`ProjectedBivariateMeta`](https://github.com/IrreducibleOSS/binius/blob/e2481a353f5b708dff3d7113bc65e9dfa4eb4d85/crates/core/src/protocols/evalcheck/subclaims.rs#L235) can now represent packed / shifted / composite, and this is (at least for me) a bit confusing. However this could be improved in the future after merging this PR, depends on how readable you find it currently
author_association: CONTRIBUTOR
user: login: djadjka
created_at: 2025-03-06T18:29:09Z
body: @TomWambsgans Sorry, I want to review your update before merging.
I will try to do it tomorrow morning.
thank you!
author_association: CONTRIBUTOR
user: login: TomWambsgans
created_at: 2025-03-06T18:31:10Z
body: > @TomWambsgans Sorry, I want to review your update before merging. I will try to do it tomorrow morning. thank you!

Yes, of course, especially since I'm not completely satisfied with `subclaims.rs`.
author_association: CONTRIBUTOR