# Redefining the `combine` Function for the Univariate Skip Optimization This note details the necessary modifications to the `combine` function, originally designed for the sumcheck protocol over a Boolean hypercube, to support the univariate skip optimization. This optimization requires handling polynomials over a mixed domain $D \times H^j$, where $D$ is a multiplicative subgroup of a finite field. ## How the Original `combine` Function Works and Its Limitations for the Univariate Skip The existing `combine` function in `statement.rs` is purpose-built for multilinear polynomials evaluated over the input domain of the Boolean hypercube $\{0,1\}^m$. Its core purpose is to compute the batched evaluation polynomial $W(X)$ defined as: $$ W(X) = \sum_{i=0}^{t-1} \gamma^i \cdot \text{eq}_{z_i}(X) $$ where: * $t$ is the number of constraints. * $\gamma$ is a random challenge. * $z_i \in \mathbb{F}^m$ are the constraint points, whose coordinates are elements of the field $\mathbb{F}$. * $\text{eq}_{z_i}(X)$ is the equality polynomial, which evaluates to $1$ if $X = z_i$ and $0$ for all other $X \in \{0,1\}^m$. The implementation computes the evaluations of the batched polynomial $W(X)$ for all $X \in \{0,1\}^m$ by leveraging an iterative process. This method is possible because, for the Boolean hypercube domain, the equality polynomial has a special structure. For a given constraint point $z = (z_1, \dots, z_m) \in \mathbb{F}^m$, this polynomial is defined as a product of simple, independent terms for each variable: $$ \text{eq}_z(X_1, \dots, X_m) = \prod_{j=1}^{m} \underbrace{\left(X_j z_j + (1-X_j)(1-z_j)\right)}_{\text{Term for variable } X_j \text{ and coordinate } z_j} $$ The algorithm materializes the full table of $2^m$ evaluations by starting with the initial weights (powers of $\gamma$) and then, for each dimension $j=1, \dots, m$, updating the entire table. At each step $j$, the table is split into two halves corresponding to points where $X_j = 0$ and $X_j = 1$. The update rules are derived directly from the formula above: * For the half of the table where $X_j = 0$, all values are multiplied by the factor $(1 - z_j)$. * For the half of the table where $X_j = 1$, all values are multiplied by the factor $z_j$. This recursive update is efficient because it processes one dimension at a time. The entire method is fundamentally tied to the input domain being the Boolean hypercube. The decomposition of the equality polynomial into a product of independent, linear terms is a unique property of this domain. For any other input domain, such as a multiplicative subgroup, the equality polynomial becomes a non-decomposable Lagrange basis polynomial, which does not permit this simple, dimension-by-dimension construction. The univariate skip optimization fundamentally changes this **input domain structure**, rendering the existing logic inapplicable. ## The New Domain Structure: $D \times H^{n-k}$ The univariate skip optimization partitions the $n$ variables into two sets: 1. The first $k$ variables are mapped to a single variable $X$ that lives in a multiplicative subgroup $D \subset \mathbb{F}$ of size $|D| = 2^k$. 2. The remaining $j = n-k$ variables remain defined over the Boolean hypercube $H^{n-k} = \{0,1\}^{n-k}$. Consequently, our evaluation domain is no longer $\{0,1\}^n$ but the mixed domain $D \times H^{n-k}$. A point in this new domain is represented as $(x, \mathbf{y})$, where $x \in D$ and $\mathbf{y} \in H^{n-k}$. This change requires us to redefine the equality polynomial $\text{eq}_z$ for a constraint point $z = (y, \mathbf{b})$ where $y \in D$ and $\mathbf{b} \in H^{n-k}$. The new equality polynomial is a product of two independent equality polynomials: $$ \text{eq}_{(y, \mathbf{b})}(X, \mathbf{Y}) = \underbrace{\text{eq}_D(X, y)}_{\text{Part 1: Subgroup}} \cdot \underbrace{\text{eq}_{H^{n-k}}(\mathbf{Y}, \mathbf{b})}_{\text{Part 2: Hypercube}} $$ The hypercube part, $\text{eq}_{H^{n-k}}$, can still be handled by the original logic. The novel part is defining and computing $\text{eq}_D(X, y)$. ## The Equality Polynomial for a Multiplicative Subgroup $D$ The equality polynomial $\text{eq}_D(X, y)$ is the Lagrange basis polynomial $L_y(X)$ for the point $y \in D$ over the domain $D$. For a cyclic group $D$ of order $|D|$, this polynomial has a standard, compact form: \begin{aligned} \text{eq}_D(X, y) = L_y(X) &= \frac{1}{|D|} \sum_{i=0}^{|D|-1} \left(\frac{X}{y}\right)^i \\ &= \frac{1}{|D|} \sum_{i=0}^{|D|-1} (Xy^{-1})^i \end{aligned} This formula ensures that $\text{eq}_D(y, y) = 1$ and $\text{eq}_D(x, y) = 0$ for any other $x \in D, x \neq y$. :::info ### Examples Let's expand this formula for the specific cases shown on the whiteboard to build intuition. **Case 1: $D = \{\pm 1\}$** Here, $|D| = 2$. For any $y \in D$, we have $y^{-1} = y$. The formula becomes: $$ \text{eq}_D(X, y) = \frac{1}{2} \sum_{i=0}^{1} (Xy)^i = \frac{1}{2} (1 + Xy) $$ **Case 2: $D = \{\pm 1, \pm i\}$ (the 4th roots of unity)** Here, $|D| = 4$. For any $y \in D$, we have $y^{-1} = y^3$. The formula becomes (for any element $y \in D$, we have $y^4 = 1$): $$ \begin{align*} \text{eq}_D(X, y) &= \frac{1}{4} \sum_{i=0}^{3} (Xy^{-1})^i = \frac{1}{4} \sum_{i=0}^{3} (Xy^3)^i \\ &= \frac{1}{4} \left( 1 + (Xy^3) + (Xy^3)^2 + (Xy^3)^3 \right) \\ &= \frac{1}{4} \left( 1 + Xy^3 + X^2y^6 + X^3y^9 \right) \\ &= \frac{1}{4} (1 + Xy^3 + X^2\underbrace{y^2}_{\substack{ y^6 = y^{4} \cdot y^2 = 1 \cdot y^2 = y^2}} + X^3\underbrace{y}_{\substack{y^9 = (y^4)^2 \cdot y = 1^2 \cdot y = y}}) \end{align*} $$ ::: :::success ### Python script The following Python script provides an empirical test, confirming that for any multiplicative subgroup for a given modulus, the formula correctly evaluates to 1 when its inputs are identical and 0 otherwise, thereby validating our required conditions. ```python import math def get_prime_factors(n): """Finds the unique prime factors of a number n.""" factors = set() # Handle factor 2 while n % 2 == 0: factors.add(2) n = n // 2 # Handle odd factors for i in range(3, int(math.sqrt(n)) + 1, 2): while n % i == 0: factors.add(i) n = n // i # If n is a prime number greater than 2 if n > 2: factors.add(n) return list(factors) def find_generator(p): """Finds a generator for the multiplicative group of integers modulo p.""" if not isinstance(p, int) or p <= 1: raise ValueError("p must be a prime number.") # The order of the multiplicative group F_p* is p-1. order = p - 1 # Find the prime factors of the order. prime_factors = get_prime_factors(order) # Iterate through potential generators from 2 to p-1. for g in range(2, p): is_generator = True # A number g is a generator if for all prime factors q of (p-1), # g^((p-1)/q) is not congruent to 1 mod p. for q in prime_factors: if pow(g, order // q, p) == 1: is_generator = False break # If it passes all checks, it's a generator. if is_generator: return g return None # Should not happen for a prime p def find_subgroups(p, max_order): """ Finds all multiplicative subgroups of F_p* up to a given max_order. """ # Order of the full multiplicative group F_p*. group_order = p - 1 # Find a generator for the full group. g = find_generator(p) subgroups = [] # The order of any subgroup must divide the order of the group. # Find all divisors of the group's order. for d in range(1, group_order + 1): if group_order % d == 0 and d <= max_order: # For each divisor d, there is a unique subgroup of that order. # Its generator 'h' can be found by g^((p-1)/d). h = pow(g, group_order // d, p) # Generate the subgroup elements by taking powers of its generator h. subgroup = sorted([pow(h, i, p) for i in range(d)]) subgroups.append(subgroup) return subgroups def eq_D(X, y, D, p): """ Calculates the Lagrange basis polynomial L_y(X) over a subgroup D. The formula is: (1/|D|) * Σ_{i=0}^{|D|-1} (X * y^{-1})^i mod p """ # Get the order of the subgroup, |D|. order = len(D) # Calculate the modular multiplicative inverse of the order, (1/|D|) mod p. order_inv = pow(order, -1, p) # Calculate the modular multiplicative inverse of y, (y^{-1}) mod p. y_inv = pow(y, -1, p) # Calculate the base of the power in the summation, (X * y^{-1}) mod p. base = (X * y_inv) % p # Compute the sum of the geometric series Σ base^i for i from 0 to |D|-1. total_sum = sum(pow(base, i, p) for i in range(order)) % p # Multiply the final sum by the inverse of the order to get the result. result = (total_sum * order_inv) % p return result def verify_subgroup(D, p): """ Verifies that eq_D(X, y) is (for all X, y in D): - 1 if X == y - 0 if X != y. """ print(f"\n--- Verifying Subgroup D = {D} (mod {p}) ---") for y in D: for X in D: value = eq_D(X, y, D, p) if X == y: assert value == 1, ( f"PANIC: eq_D(X={X}, y={y}) should be 1, but was {value}" ) else: assert value == 0, ( f"PANIC: eq_D(X={X}, y={y}) should be 0, but was {value}" ) print(f"✅ All checks passed for subgroup D = {D}.") # Define the prime modulus for our finite field F_p. p = 17 # Define the maximum order of the subgroups you want to verify. max_order_to_check = 16 # Automatically find all valid subgroups. all_subgroups = find_subgroups(p, max_order_to_check) # Loop through the found subgroups and verify each one. for subgroup in all_subgroups: verify_subgroup(subgroup, p) print( f"\nAll subgroups of F_{p} up to order {max_order_to_check} verified successfully." ) ``` ::: ## The New `combine` Logic - NO NEED TO READ THESE ARE SOME IDEAS ABOUT IMPLEMENTATION Our goal is to compute the evaluations of the batched polynomial $W(X, \mathbf{Y})$ over its new mixed domain, which is the Cartesian product of a multiplicative subgroup $D$ and a Boolean hypercube $H^{n-k}$. The polynomial is defined as a sum over all $t$ constraints. For each point $(X, \mathbf{Y})$ in the domain, its value is: $$ W(X, \mathbf{Y}) = \sum_{i=0}^{t-1} \underbrace{\gamma^i}_{\text{Challenge}} \cdot \underbrace{\text{eq}_{D}(X, y_i)}_{\text{Subgroup Part}} \cdot \underbrace{\text{eq}_{H^{n-k}}(\mathbf{Y}, \mathbf{b}_i)}_{\text{Hypercube Part}} $$Here, each constraint point $z_i$ has been split into its two components: $y_i \in D$ and $\mathbf{b}_i \in H^{n-k}$. To implement this, we'll use a direct and simple algorithm. The core idea is to think of the final evaluation table as a canvas. We will compute the contribution of each constraint individually and "add" it to this canvas, one layer at a time, until the final result is complete.