Multilinear Extensions
A function defined on the -dimensional boolean hypercube has a unique multilinear extension (MLE) : the unique polynomial of degree at most 1 in each variable that agrees with on all boolean inputs. Concretely,
The product term is the equality polynomial , which equals 1 when on the boolean hypercube and interpolates smoothly elsewhere.
MLEs are the central data structure in sumcheck-based proof systems: the sumcheck protocol operates over multilinear polynomials, and essentially all witness data in Jolt is represented as MLEs.
For a more formal introduction, see Section 3.5 of Proofs, Arguments, and Zero Knowledge.
Representation
An MLE over variables is stored as a vector of evaluations over the boolean hypercube . The -th entry of this vector stores where is the binary representation of . Jolt uses big-endian indexing: the most significant bit of corresponds to .
Variable binding
Given represented by evaluations, binding the most significant variable to a challenge produces a new MLE represented by evaluations. For each index :
where and for the same suffix . This is a single pass. Binding the least significant variable instead pairs even/odd entries: .
Which variable gets bound in each round is determined by the BindingOrder: HighToLow (most significant first) or LowToHigh (least significant first).
Implementations in Jolt
The MLE implementations live in crates/jolt-poly/src/. Polynomial<T> stores a Boolean-hypercube evaluation table, while the MultilinearEvaluation, MultilinearBinding, and MultilinearPoly traits separate point evaluation, sumcheck binding, and PCS access. The same generic type stores both full field elements and compact scalar types.
Dense polynomials
Polynomial<F> (dense.rs) is the straightforward representation: a Vec<F> of field elements. This is the baseline MLE type used when coefficients are full-sized field elements.
Binding operates in-place on the evaluation vector. In HighToLow order, the vector is split into left and right halves (corresponding to the most significant variable being 0 or 1) and interpolated:
#![allow(unused)] fn main() { let (left, right) = self.Z.split_at_mut(n); left.iter_mut().zip(right.iter()).for_each(|(a, b)| { *a += r * (*b - *a); }); }
In LowToHigh order, adjacent even/odd pairs are interpolated instead:
#![allow(unused)] fn main() { for i in 0..n { self.Z[i] = self.Z[2 * i] + r * (self.Z[2 * i + 1] - self.Z[2 * i]); } }
Both orders have parallel variants (bind_parallel) that use Rayon. The HighToLow parallel path binds in place, while the LowToHigh parallel path cannot.
Compact polynomials
Polynomial<T> stores coefficients as small scalars such as bool, u8, or i64 rather than full field elements. This is the "pay-per-bit" representation that makes Dory commitments efficient.
Binding promotes the compact evaluation table to field elements:
#![allow(unused)] fn main() { // For a pair (a, b) of small scalars: match a.cmp(&b) { Ordering::Equal => a.to_field(), Ordering::Less => a.to_field() + r * (b - a).to_field(), Ordering::Greater => a.to_field() - r * (a - b).to_field(), } }
Subsequent operations use Polynomial<F>. Concrete scalar types remain monomorphized, so small-scalar optimizations apply without an enum dispatch layer.
Other specialized representations
Several other types in crates/jolt-poly/src/ exploit common structure:
OneHotPolynomial(one_hot.rs): Stores the optional nonzero index for each row rather than a dense vector of 0s and 1s. Used for and polynomials in Twist and Shout.RlcSource(multilinear.rs): Represents a lazy random linear combination of multiple PCS sources.EqPolynomial(eq.rs): The equality MLE , commonly used as a building block in sumcheck and MLE evaluation.