Varun Thakore

Circle STARKs: Circle FFT

This is Part III of a four-part series on Circle STARKs. The original appeared on the ZK Security blog.


Recap#

In Part I of this series, we explored efficient field arithmetic over Mersenne primes and a key challenge in instantiating the STARK protocol using them, specifically the absence of a smooth multiplicative subgroup.

In Part II, we examined how this issue can be addressed using the circle group. We introduced the structure of its cosets, i.e., twin-coset and standard position coset, which can be used to instantiate STARK protocols. We also discussed the space of polynomials defined over the circle group, and the construction of vanishing polynomial over these cosets.

Introduction#

There are two common ways to represent a univariate polynomial f(X)f(X) of degree less than NN:

  1. Monomial Basis:

    f(X)=∑j=0N−1cj⋅bj(X) f(X) = \sum_{j = 0}^{N - 1} c_j \cdot b_j(X)

    where cjc_j are the coefficients and bj(X)b_j(X) are the monomial basis polynomials such that:

    b(X)=[1,X,X2,⋯ ,XN−1] b(X) = [1, X, X^2, \cdots, X^{N - 1}]
  2. Lagrange Basis:

    f(X)=∑j=0N−1vj⋅λj(X) f(X) = \sum_{j = 0}^{N - 1} v_j \cdot \lambda_j(X)

    where vj=f(ωj)v_j = f(\omega_j) is the evaluation of ff at point ωj\omega_j and λj(X)\lambda_j(X) is the Lagrange basis polynomial satisfying:

    • λj(ωj)=1\lambda_j(\omega_j) = 1
    • λj(ωi)=0\lambda_j(\omega_i) = 0 for all i≠ji \ne j

    The Lagrange basis polynomial is defined as:

    λj(X)=∏0≤i<N,i≠jX−ωiωj−ωi \lambda_j(X) = \prod_{\substack{0 \le i < N, \\ i \ne j}} \frac{X - \omega_i}{\omega_j - \omega_i}

In general, the FFT algorithm performs a transformation from one polynomial basis to another. For example, it can convert a polynomial from the monomial basis to the Lagrange basis, and vice versa. This change of basis is useful because certain operations, such as polynomial multiplication, become significantly more efficient in the Lagrange basis.

In STARK protocols, the computation trace is usually represented as a set of polynomial evaluations at the points in a multiplicative subgroup, which means the coefficients of the polynomial can be computed using a Cooley-Tukey style FFT.

In Circle STARKs, the multiplicative subgroup is replaced by a twin-coset: not a group, but rather a union of cosets of the unit circle. The elements follow the “addition of angles” or “complex multiplication” group law, and each element corresponds to a point (x,y)(x, y) on the circle. In the Circle FFT, given evaluations over a twin-coset, we interpolate a bivariate polynomial.

In this post, we develop an intuitive understanding of the Circle FFT by drawing parallels to the classical Cooley–Tukey FFT. Throughout the post, we use FFT to refer to interpolation, moving from the Lagrange basis to a monomial basis, and inverse FFT to refer to evaluation, moving from the monomial basis to the Lagrange basis.

Cooley-Tukey FFT#

In this section, we present a generic version of the Cooley–Tukey FFT algorithm. This general version will eventually help us understand the Circle FFT as a concrete instance of the generic construct. To build intuition, we’ll walk through a concrete example alongside the general description.

Problem Statement#

Given the polynomial evaluations over a domain DnD_n of size N=knN = k^n, our goal is to compute the polynomial’s coefficients with respect to some basis polynomials such that:

f(X)=∑j=0kn−1cj⋅bj(n)(X) f(X) = \sum_{j = 0}^{k^n - 1} c_j \cdot b_j^{(n)}(X)

Here, cjc_j are the coefficients we wish to compute and bj(n)(X)b_j^{(n)}(X) denotes the basis polynomials.

Example

Cooley–Tukey FFT with k=2k = 2, n=3n = 3

Given the evaluations v⃗\vec{v} of a polynomial over a multiplicative subgroup D3D_3 of size N=23=8N = 2^3 = 8, the goal is to compute the coefficients (c0,c1,…,c7)(c_0, c_1, \ldots, c_7) of a polynomial with degree <N=8< N = 8.

For this example, let D3D_3 be a multiplicative subgroup of F17∗\mathbb{F}_{17}^* generated by ω=9\omega = 9. Then:

D3={ωi∣i=0,1,…,7}=[1,9,13,15,16,8,4,2] D_3 = \{\omega^i \mid i = 0, 1, \ldots, 7\} = [1, 9, 13, 15, 16, 8, 4, 2]

Suppose the evaluations of the polynomial over D3D_3 are:

v⃗={f(ωi)∣i=0,1,…,7}=[11,10,15,1,9,11,15,6] \vec{v} = \{f(\omega^i) \mid i = 0, 1, \ldots, 7\} = [11, 10, 15, 1, 9, 11, 15, 6]

Algorithm#

The Cooley–Tukey FFT algorithm follows a classic divide-and-conquer strategy. It proceeds as follows:

  • Step 1: Decompose the polynomial f(X)f(X) into a collection of lower-degree polynomials and find the evaluations of those lower-degree polynomials over a smaller domain.

  • Step 2: Recursively apply FFT to each of the lower-degree polynomials to compute their coefficients.

  • Step 3: Combine the coefficients of the lower-degree polynomials to compute the coefficients of the original polynomial f(X)f(X).

At each recursive layer ll of the FFT algorithm, we decompose a polynomial f(X)f(X) using the following key components:

  • ψ\psi: The polynomial map ψ\psi is a kk-to-11 map, meaning that it maps kk elements of the domain DlD_l to a single element in the smaller domain Dl−1D_{l-1}.

    ψ:Dl→Dl−1\psi: D_l \rightarrow D_{l-1}

    Since ∣Dl∣=kl|D_l| = k^l, this implies ∣Dl−1∣=kl−1|D_{l-1}| = k^{l-1}. A common example of such a map over a multiplicative subgroup is: ψ(X)=Xk \psi(X) = X^k .

  • t0,t1,⋯ ,tk−1t_0, t_1, \cdots, t_{k-1}: The twiddles t0,t1,⋯ ,tk−1t_0, t_1, \cdots, t_{k-1} are polynomials which map elements from the domain DlD_l to the field F\mathbb{F}, i.e., for j∈{0,1,⋯ ,k−1}j \in \{0, 1, \cdots, k-1\}

    tj:Dl→Ft_j: D_l \rightarrow \mathbb{F}

    For a fixed z∈Dl−1z \in D_{l-1}, the fiber of zz under the map ψ\psi consists of all elements in DlD_l that map to zz. Let SzS_z denote the fiber of an element z∈Dl−1z \in D_{l-1} under the polynomial map ψ\psi, that is:

    Sz=ψ−1(z)={x∈Dl∣ψ(x)=z}={x0,x1,⋯ ,xk−1}S_z = \psi^{-1}(z) = \{x \in D_l \mid \psi(x) = z\} = \{x_0,x_1, \cdots, x_{k-1}\}

    Since ψ\psi is a kk-to-1 map, the fiber of each z∈Dl−1z \in D_{l-1} is a subset of DlD_l, containing exactly kk elements. The restriction of f(X)f(X) to the fiber SzS_z is a polynomial f(X)∣Szf(X)|_{S_z} with the following evaluations:

    f∣Sz={f(x0),f(x1),⋯ ,f(xk−1)} f|_{S_z} = \{f(x_0),f(x_1), \cdots, f(x_{k-1})\}

    Thus f(X)∣Szf(X)|_{S_z} is of degree less than kk and it lies in a polynomial space of dimension at most kk. The twiddles must be chosen to span this kk-dimensional polynomial space. This ensures that the polynomial f(X)f(X) can be correctly expressed in terms of the twiddles. Thus the twiddles must form a basis of a kk-dimensional polynomial space over a fiber. This requirement will become clearer as we work through the algorithm and the example.

The polynomial map ψ\psi and the twiddles t0,t1,⋯ ,tk−1t_0, t_1, \cdots, t_{k-1} need not be same across all layers of the FFT algorithm. We now describe each of the steps in more detail.

Step 1: Decompose f(X)f(X) and compute evaluations of sub-polynomials#

We decompose the polynomial f(X)f(X) into smaller-degree polynomials fi(X)f_i(X) as follows:

f(X)=t0(X)⋅f0(ψ(X))+t1(X)⋅f1(ψ(X))+⋯+tk−1(X)⋅fk−1(ψ(X)) f(X) = t_0(X) \cdot f_0(\psi(X)) + t_1(X) \cdot f_1(\psi(X)) + \cdots + t_{k-1}(X) \cdot f_{k-1}(\psi(X))

Each fi(X)f_i(X) is a polynomial of lower degree than f(X)f(X), specifically:

deg⁡(fi)≤deg⁡(f)deg⁡(ψ)<knk=kn−1 \deg(f_i) \leq \frac{\deg(f)}{\deg(\psi)} < \frac{k^n}{k} = k^{n-1}

Next, we compute the evaluations of each fi(X)f_i(X) over Dn−1=ψ(Dn)D_{n-1} = \psi(D_n) using the evaluations of f(X)f(X) over DnD_n. Let SzS_z denote the fiber of an element z∈Dn−1z \in D_{n-1} under the polynomial map ψ\psi. Since it has kk elements, let us denote it as follows:

Sz={x0,x1,⋯ ,xk−1} S_z = \{ x_0, x_1, \cdots, x_{k-1}\}

Such that for all xi∈Szx_i \in S_z, ψ(xi)=z\psi(x_i)=z. Substituting all xi∈Szx_i \in S_z, into the decomposition of f(X)f(X) gives:

f(x0)=t0(x0)⋅f0(z)+t1(x0)⋅f1(z)+⋯+tk−1(x0)⋅fk−1(z) f(x_0) = t_0(x_0) \cdot f_0(z) + t_1(x_0) \cdot f_1(z) + \cdots + t_{k-1}(x_0) \cdot f_{k-1}(z)

f(x1)=t0(x1)⋅f0(z)+t1(x1)⋅f1(z)+⋯+tk−1(x1)⋅fk−1(z) f(x_1) = t_0(x_1) \cdot f_0(z) + t_1(x_1) \cdot f_1(z) + \cdots + t_{k-1}(x_1) \cdot f_{k-1}(z)

⋮ \vdots

f(xk−1)=t0(xk−1)⋅f0(z)+t1(xk−1)⋅f1(z)+⋯+tk−1(xk−1)⋅fk−1(z) f(x_{k-1}) = t_0(x_{k-1}) \cdot f_0(z) + t_1(x_{k-1}) \cdot f_1(z) + \cdots + t_{k-1}(x_{k-1}) \cdot f_{k-1}(z)

Note that since for all xi∈Szx_i \in S_z, ψ(xi)=z\psi(x_i)=z, the f0(z),f1(z),…,fk−1(z)f_0(z), f_1(z), \ldots, f_{k-1}(z) are same across all the equations. This gives us a system of kk linear equations in the kk unknowns f0(z),f1(z),…,fk−1(z)f_0(z), f_1(z), \ldots, f_{k-1}(z). Solving this system allows us to determine the values of each fi(z)f_i(z). Since the system has a constant size kk, so solving it using e.g. Gaussian elimination takes constant time.

Note

Solving the above system of linear equations reduces to inverting the k×kk \times k twiddle matrix, i.e.,

[f0(z) f1(z) ⋮ fk−1(z)]=[t0(x0)t1(x0)⋯tk−1(x0) t0(x1)t1(x1)⋯tk−1(x1) ⋮⋮⋱⋮ t0(xk−1)t1(xk−1)⋯tk−1(xk−1)]−1⋅[f(x0) f(x1) ⋮ f(xk−1)] \begin{bmatrix} f_0(z) \\\ f_1(z) \\\ \vdots \\\ f_{k-1}(z) \end{bmatrix} = \begin{bmatrix} t_0(x_0) & t_1(x_0) & \cdots & t_{k-1}(x_0) \\\ t_0(x_1) & t_1(x_1) & \cdots & t_{k-1}(x_1) \\\ \vdots & \vdots & \ddots & \vdots \\\ t_0(x_{k-1}) & t_1(x_{k-1}) & \cdots & t_{k-1}(x_{k-1}) \end{bmatrix}^{-1} \cdot \begin{bmatrix} f(x_0) \\\ f(x_1) \\\ \vdots \\\ f(x_{k-1}) \end{bmatrix}

The requirement that the twiddles form a basis of a kk-dimensional polynomial space over a fiber of size kk ensures that the k×kk \times k twiddle matrix is invertible and thus the above system of linear equations has a unique solution.

Also note that the twiddles are initialized before the start of the algorithm and therefore the corresponding matrix inversions can be precomputed, making it a k2k^2 as oppose to k3k^3 operations.

By repeating this process for every z∈Dn−1z \in D_{n-1}, we obtain the full set of evaluations for all the polynomials f0,f1,…,fk−1f_0, f_1, \ldots, f_{k-1} over the domain Dn−1D_{n-1}, enabling us to recursively apply the FFT on each of them.

Example

Step 1: All calculations are over F17\mathbb{F}_{17}, i.e, modulo 1717. First, decompose the polynomial f(X)f(X) using the map ψ(X)=X2\psi(X) = X^2 and twiddles t0(X)=1,t1(X)=Xt_0(X) = 1, t_1(X) = X:

f(X)=f0(X2)+Xf1(X2) f(X) = f_0(X^2) + X f_1(X^2)

Given the evaluations v⃗=[11,10,15,1,9,11,15,6]\vec{v} = [11, 10, 15, 1, 9, 11, 15, 6] of f(X)f(X) over the domain

D3=[1,9,13,15,16,8,4,2], D_3 = [1, 9, 13, 15, 16, 8, 4, 2],

we aim to compute the evaluations of f0f_0 and f1f_1 over the smaller domain

D2=ψ(D3)=[1,13,16,4]. D_2 = \psi(D_3) = [1, 13, 16, 4].

For z=1∈D2z=1 \in D_2, the preimage set is Sz=[1,16]S_z = [1, 16], since ψ(1)=1\psi(1) = 1 and ψ(16)=1\psi(16) = 1.
Substitute the values from set SzS_z into the decomposition equation we get:

f(1)=f0(ψ(1))+f1(ψ(1))  ⟹  11=f0(1)+f1(1) f(1) = f_0(\psi(1)) + f_1(\psi(1)) \implies 11 = f_0(1) + f_1(1)

f(16)=f0(ψ(16))+f1(ψ(16))  ⟹  9=f0(1)+16⋅f1(1) f(16) = f_0(\psi(16)) + f_1(\psi(16)) \implies 9 = f_0(1) + 16 \cdot f_1(1)

Solving this system of equations yields:

f0(1)=10,f1(1)=1 f_0(1) = 10, \quad f_1(1) = 1

Repeating this process for all z∈D2z \in D_2, we obtain:

v0⃗=[10,2,15,12],v1⃗=[1,16,0,14] \vec{v_0} = [10, 2, 15, 12], \quad \vec{v_1} = [1, 16, 0, 14]

Step 2: Recursively apply FFT to sub-polynomials#

Now that we have the evaluations of each fif_i over the domain Dn−1D_{n-1}, the next step is to compute their coefficients. This brings us back to the same problem as before, but for polynomials of smaller degree i.e. deg⁡(fi)<kn−1\deg(f_i) < k^{n-1} and over a smaller domain Dn−1D_{n-1}. We apply the same divide-and-conquer strategy.

The polynomial map ψ\psi induces a sequence of progressively smaller domains:

Dn→ψDn−1→ψ⋯→ψD0 D_n \xrightarrow{\psi} D_{n-1} \xrightarrow{\psi} \cdots \xrightarrow{\psi} D_0

where each domain Dj=ψ(Dj+1)D_j = \psi(D_{j+1}) for j=0,1,…,n−1j = 0, 1, \ldots, n - 1, and ∣Dj∣=kj|D_j| = k^j. We have assumed that the polynomial map ψ\psi is the same across all layers, but in general, each layer may use a different map.

We recursively apply FFT over smaller domains until we reach the base case: a constant polynomial, where all the evaluations over the domain are equal. In this case, interpolating gives a constant polynomial whose sole coefficient is the evaluation value itself. Thus, we simply return this evaluation value as the coefficient.

Once the base case is resolved, we work backward through the recursive calls, using the decomposition equation at each level to reconstruct the coefficients of the original fif_i.

Example

Step 2: Given the evaluations of f0f_0 and f1f_1 over D2=[1,13,16,4]D_2 = [1, 13, 16, 4]:

v0⃗=[10,2,15,12],v1⃗=[1,16,0,14] \vec{v_0} = [10, 2, 15, 12], \quad \vec{v_1} = [1, 16, 0, 14]

we recursively apply the FFT algorithm to compute the coefficients of f0f_0 and f1f_1.

At each layer, we decompose the polynomial using the same polynomial map ψ(X)=X2\psi(X) = X^2 and the same twiddles t0(X)=1,t1(X)=Xt_0(X) = 1, t_1(X) = X as before. For example, the decomposition of f0(X)f_0(X) is:

f0(X)=f00(X2)+X⋅f01(X2) f_0(X) = f_{00}(X^2) + X \cdot f_{01}(X^2)

Omitting the intermediate steps of the recursive calls, we eventually obtain the coefficients of f0(X)f_0(X) and f1(X)f_1(X) as follows:

f0(X)=14+10X+7X2+13X3 f_0(X) = 14 + 10X + 7X^2 + 13X^3

f1(X)=12+15X+14X2+11X3 f_1(X) = 12 + 15X + 14X^2 + 11X^3

Step 3: Combine the coefficients#

Finally, we combine the coefficients of the polynomials f0,f1,…,fk−1f_0, f_1, \ldots, f_{k-1} to compute the coefficients of the original polynomial f(X)f(X) using the decomposition equation.

f(X)=t0(X)⋅f0(ψ(X))+t1(X)⋅f1(ψ(X))+⋯+tk−1(X)⋅fk−1(ψ(X)) f(X) = t_0(X) \cdot f_0(\psi(X)) + t_1(X) \cdot f_1(\psi(X)) + \cdots + t_{k-1}(X) \cdot f_{k-1}(\psi(X))

Example

Step 3: Given the coefficients of f0(X)f_0(X) and f1(X)f_1(X):

f0(X)=14+10X+7X2+13X3 f_0(X) = 14 + 10X + 7X^2 + 13X^3

f1(X)=12+15X+14X2+11X3 f_1(X) = 12 + 15X + 14X^2 + 11X^3

we reconstruct the original polynomial f(X)f(X) using the decomposition:

f(X)=f0(X2)+X⋅f1(X2) f(X) = f_0(X^2) + X \cdot f_1(X^2)

Substituting the expressions for f0f_0 and f1f_1, we get:

f(X)=14+10X2+7X4+13X6+X⋅(12+15X2+14X4+11X6) f(X) = 14 + 10X^2 + 7X^4 + 13X^6 + X \cdot (12 + 15X^2 + 14X^4 + 11X^6)

f(X)=14+12X+10X2+15X3+7X4+14X5+13X6+11X7 f(X) = 14 + 12X + 10X^2 + 15X^3 + 7X^4 + 14X^5 + 13X^6 + 11X^7

Sequence of Domains for Circle FFT#

In Circle FFT, the map ψ\psi is instantiated by a 2-to-1 map tailored for the circle group setting. The domain used here is the twin-coset:

Dn=Q⋅Gn−1∪Q−1⋅Gn−1, D_n = Q \cdot G_{n-1} \cup Q^{-1} \cdot G_{n-1},

where Gn−1G_{n-1} is a subgroup of the circle group of size 2n−12^{n-1} and Q≠Q−1Q \neq Q^{-1}, hence the cosets Q⋅Gn−1Q \cdot G_{n-1} and Q−1⋅Gn−1Q^{-1} \cdot G_{n-1} are disjoint and ∣Dn∣=2n|D_n| = 2^n. This section describes two specific 2-to-1 maps on DnD_n that are central to the Circle FFT construction:

  1. Quotient map ϕJ\phi_J: This map sends each point in DnD_n to an element of the quotient set Dn/JD_n / J: ϕJ:Dn→Dn/J. \phi_J: D_n \rightarrow D_n / J. where JJ denotes the inversion map J(P)=−PJ(P) = -P. Quotient map ϕJ\phi_J maps two distinct points PP and J(P)J(P) from DnD_n to a single element in Dn/JD_n / J. This can be interpreted as saying that two points are considered equivalent if they differ only by sign. This is same as the projection map πx\pi_x that projects the points (x,y)(x,y) and (x,−y)(x,-y) to the same xx-coordinate: πx((x,y))=x \pi_x((x, y)) = x Since ϕJ\phi_J or πx\pi_x map two points from DnD_n to a single element in Dn/JD_n / J, the size of Dn/JD_n / J will be half the size of DnD_n.
  2. Squaring map π\pi: This map was introduced in Part II. When applied to the twin-coset DnD_n of size 2n2^n, it produces a smaller twin-coset Dn−1D_{n-1} of size 2n−12^{n-1}. This is also a 2-to-1 map which halves the size of DnD_{n}. By recursively applying this map: Dj=π(Dj+1)for j=n−1 down to 1, D_j = \pi(D_{j+1}) \quad \text{for } j = n-1 \text{ down to } 1, we generate a sequence of twin-cosets, until we reach D1D_1 (twin-coset of size 2). Each DjD_j in this sequence corresponds to a twin-coset associated with the subgroup Gj−1G_{j-1}, and has size ∣Dj∣=2j|D_j| = 2^j.

In Circle FFT, we use both the projection map πx\pi_x and the squaring map π\pi to construct the sequence of domains. A key property to note is that the maps πx\pi_x and π\pi commute, meaning:

πx∘π(Dn)=π∘πx(Dn). \pi_x \circ \pi (D_n) = \pi \circ \pi_x (D_n).

Intuitively, this means that projecting to the xx-coordinate and then squaring is the same as squaring first and then projecting to the xx-coordinate. This is further illustrated by the following two animations.

The following animation shows applying the projection map πx\pi_x to a twin-coset D3D_3 followed by the squaring map π\pi to get the quotient domain S2S_2.

The following animation shows applying the squaring map π\pi to a twin-coset D3D_3 followed by the projection map πx\pi_x to get the quotient domain S2S_2.

By applying these maps at each layer, we obtain a sequence of domains for the Circle FFT, similar to the domains in the Cooley–Tukey FFT. The key difference lies in the choice of maps: in Cooley–Tukey, we applied a single polynomial map ψ\psi at each layer, whereas in Circle FFT, we apply ψ=πx\psi = \pi_x in the first layer, followed by ψ=π\psi = \pi in subsequent layers.

Let Sj=Dj/JS_j = D_j / J denote the quotient of the twin-coset DjD_j under the involution JJ. The map π:Sj→Sj−1\pi: S_j \rightarrow S_{j-1} is a 2-to-1 map, defined by:

π:x↦2x2−1 \pi: x \mapsto 2x^2 - 1

This is obtained using the doubling map and the equality y2=1−x2y^2 = 1 - x^2 to compute the xx-coordinate:

π(x,y)=(x,y)⋅(x,y)=(2x2−1,2xy) \pi(x, y) = (x, y) \cdot (x, y) = (2x^2-1, 2xy)

The sequence of domains for Circle FFT is shown as follows:

Dn→πxSn→πSn−1→π⋯→πS1 D_n \xrightarrow{\pi_x} S_n \xrightarrow{\pi} S_{n-1} \xrightarrow{\pi} \cdots \xrightarrow{\pi} S_1

We will now describe the Circle FFT algorithm over these sequence of domains.

Circle FFT#

Let Fp\mathbb{F}_p be a Mersenne prime field with p=231−1p = 2^{31} - 1, and let F\mathbb{F} denote its algebraic closure.

Given evaluations over a twin-coset DnD_n, the Circle FFT interpolates a bivariate polynomial f(X,Y)f(X, Y) from the space LN(F)L_N(\mathbb{F}). This space consists of bivariate polynomials over the circle curve X2+Y2=1X^2 + Y^2 = 1 of total degree ≤N/2\leq N/2:

LN(F)=F[X,Y]/(X2+Y2−1) L_N(\mathbb{F}) = \mathbb{F}[X, Y] / (X^2 + Y^2 - 1)

Let’s walk through the Circle FFT algorithm. As in the Cooley–Tukey case, we’ll also work through a concrete example alongside the algorithm.

Example

Circle FFT on the twin-coset D3D_3 over Mersenne prime p=25−1=31p = 2^5 -1 = 31

Given the evaluations v⃗\vec{v} of a bivariate polynomial f(X,Y)f(X,Y) over the twin-coset D3D_3 of size N=23=8N = 2^3 = 8, the goal is to compute the coefficients of f(X,Y)f(X,Y).

Consider the following twin-coset D3D_3 and the evaluations v⃗\vec{v} of f(X,Y)f(X,Y) over D3D_3:

D3=[(7,18),(13,7),(24,13),(18,24),(7,13),(13,24),(24,18),(18,7)] D_3 = [(7, 18), (13, 7), (24, 13), (18, 24), (7, 13), (13, 24), (24, 18), (18, 7)] v⃗=[13,16,9,30,29,27,13,21] \vec{v} = [13, 16, 9, 30, 29, 27, 13, 21]

Algorithm#

Circle FFT follows the same divide-and-conquer strategy as the Cooley–Tukey FFT. The bivariate polynomial is first decomposed into lower-degree polynomials. We then recursively compute the coefficients of these smaller polynomials and finally combine them to compute the coefficients of the original polynomial.

The key difference lies in how the polynomial is split. In Cooley–Tukey FFT, a single map ψ\psi is applied recursively at every level. In contrast, Circle FFT uses two distinct 2-to-1 maps: the projection map πx\pi_x is applied in the first step, and the squaring map π\pi is used in all subsequent recursive steps.

Effectively, Circle FFT mirrors Cooley–Tukey, but with a split using ψ=πx\psi = \pi_x initially, followed by recursive splits using ψ=π\psi = \pi. We now walk through each step in detail.

Step 1: Decompose f(X,Y)f(X, Y) and compute evaluations of sub-polynomials#

In the first step, we split the bivariate polynomial f(X,Y)f(X, Y) over DnD_n using the projection map πx\pi_x and twiddles t0(X,Y)=1t_0(X, Y) = 1, t1(X,Y)=Yt_1(X, Y) = Y:

f(X,Y)=t0(X,Y)⋅f0(πx(X,Y))+t1(X,Y)⋅f1(πx(X,Y))=f0(X)+Y⋅f1(X) f(X, Y) = t_0(X, Y) \cdot f_0(\pi_x(X, Y)) + t_1(X, Y) \cdot f_1(\pi_x(X, Y)) = f_0(X) + Y \cdot f_1(X)

Given the evaluations of f(X,Y)f(X, Y) over DnD_n, our goal is to compute the evaluations of f0(X)f_0(X) and f1(X)f_1(X) over the domain Sn=πx(Dn)S_n = \pi_x(D_n).

For any pair of points P=(x,y)P = (x, y) and its conjugate J(P)=(x,−y)J(P) = (x, -y) in DnD_n that project to the same x∈Snx \in S_n, substituting into the decomposition gives:

f(P)=f0(x)+y⋅f1(x),f(J(P))=f0(x)−y⋅f1(x) f(P) = f_0(x) + y \cdot f_1(x), \quad f(J(P)) = f_0(x) - y \cdot f_1(x)

This system yields two equations in the unknowns f0(x)f_0(x) and f1(x)f_1(x), which can be solved directly.

Applying this to all pairs of points in DnD_n, gives the evaluations of f0(X)f_0(X) and f1(X)f_1(X) over the domain SnS_n.

Example

Step 1: All calculations are over F31\mathbb{F}_{31}, i.e, modulo 3131. First, decompose the polynomial f(X,Y)f(X, Y) using twiddles t0(X,Y)=1t_0(X, Y) = 1, t1(X,Y)=Yt_1(X, Y) = Y and map ψ(X,Y)=πx(X,Y)=X\psi(X, Y) = \pi_x(X, Y) = X:

f(X,Y)=t0(X,Y)⋅f0(ψ(X,Y))+t1(X,Y)⋅f1(ψ(X,Y))=f0(X)+Y⋅f1(X) f(X, Y) = t_0(X, Y) \cdot f_0(\psi(X, Y)) + t_1(X, Y) \cdot f_1(\psi(X, Y)) = f_0(X) + Y \cdot f_1(X)

Given the evaluations v⃗=[13,16,9,30,29,27,13,21]\vec{v} = [13, 16, 9, 30, 29, 27, 13, 21] of f(X,Y)f(X, Y) over the twin-coset

D3=[(7,18),(13,7),(24,13),(18,24),(7,13),(13,24),(24,18),(18,7)] D_3 = [(7, 18), (13, 7), (24, 13), (18, 24), (7, 13), (13, 24), (24, 18), (18, 7)]

we aim to compute the evaluations of f0f_0 and f1f_1 over the smaller domain S3S_3

S3=πx(D3)=[7,13,24,18]. S_3 = \pi_x(D_3) = [7, 13, 24, 18].

For P=(7,18)P=(7, 18) and J(P)=(7,13)J(P)=(7, 13) in D3D_3 which have the same projection under πx\pi_x, substituting them into the decomposition equation gives:

f(7,18)=f0(7)+18⋅f1(7)  ⟹  13=f0(7)+18⋅f1(7) f(7, 18) = f_0(7) + 18 \cdot f_1(7) \implies 13 = f_0(7) + 18 \cdot f_1(7)

f(7,13)=f0(7)+13⋅f1(7)  ⟹  29=f0(7)+13⋅f1(7) f(7, 13) = f_0(7) + 13 \cdot f_1(7) \implies 29 = f_0(7) + 13 \cdot f_1(7)

Solving this system of equations yields:

f0(7)=21,f1(7)=3 f_0(7) = 21, \quad f_1(7) = 3

Repeating this process for all pairs PP and J(P)J(P) in D3D_3, we obtain:

v0⃗=[21,6,11,10],v1⃗=[3,28,7,6] \vec{v_0} = [21, 6, 11, 10], \quad \vec{v_1} = [3, 28, 7, 6]

Step 2: Recursively apply FFT to sub-polynomials#

Given the evaluations of f0(X)f_0(X) and f1(X)f_1(X) over SnS_n, we now compute their coefficients.

This step mirrors the recursive structure of the Cooley–Tukey FFT. Each polynomial is recursively split using the squaring map π(X)=2X2−1\pi(X) = 2X^2 - 1, and the process continues over successively smaller domains.

We begin by decomposing f0(X)f_0(X) using the squaring map π\pi and twiddles t00(X)=1t_{00}(X) = 1, t01(X)=Xt_{01}(X) = X:

f0(X)=t00(X)⋅f00(π(X))+t01(X)⋅f01(π(X))=f00(π(X))+X⋅f01(π(X)) f_0(X) = t_{00}(X) \cdot f_{00}(\pi(X)) + t_{01}(X) \cdot f_{01}(\pi(X)) = f_{00}(\pi(X)) + X \cdot f_{01}(\pi(X))

We compute the evaluations of f00(X)f_{00}(X) and f01(X)f_{01}(X) over Sn−1=π(Sn)S_{n-1} = \pi(S_n) by solving a system of linear equations, as before.

This recursive process continues until we reach the base case: all evaluations of the polynomial over the domain are same. At that level, the coefficient is simply the evaluation of the constant polynomial.

Finally, we reconstruct the coefficients of f0(X)f_0(X) by working backward through the recursive calls, using the decomposition equation at each level. The same process applies to compute the coefficients of f1(X)f_1(X).

Example

Step 2: Given the evaluations of f0f_0 and f1f_1 over S3=[7,13,24,18]S_3 = [7, 13, 24, 18]:

v0⃗=[21,6,11,10],v1⃗=[3,28,7,6] \vec{v_0} = [21, 6, 11, 10], \quad \vec{v_1} = [3, 28, 7, 6]

we recursively apply the FFT algorithm to compute the coefficients of f0f_0 and f1f_1.

At each layer, we decompose the polynomial using the polynomial map π(X)\pi(X) and the twiddles t0(X)=1,t1(X)=Xt_0(X) = 1, t_1(X) = X as before. For example, the decomposition of f0(X)f_0(X) is:

f0(X)=f00(π(X))+X⋅f01(π(X)) f_0(X) = f_{00}(\pi(X)) + X \cdot f_{01}(\pi(X))

Omitting the intermediate steps of the recursive calls, we eventually obtain the coefficients of f0(X)f_0(X) and f1(X)f_1(X) as follows:

f0(X)=12+26⋅X+π(X)+28⋅X⋅π(X) f_0(X) = 12 + 26 \cdot X + \pi(X) + 28 \cdot X \cdot \pi(X)

f1(X)=11+26⋅X+14⋅π(X)+20⋅X⋅π(X) f_1(X) = 11 + 26 \cdot X + 14 \cdot \pi(X) + 20 \cdot X \cdot \pi(X)

Step 3: Combine the coefficients#

Finally, we combine the coefficients of f0(X)f_0(X) and f1(X)f_1(X) to compute the coefficients of the original bivariate polynomial f(X,Y)f(X, Y), using the decomposition:

f(X,Y)=f0(X)+Y⋅f1(X) f(X, Y) = f_0(X) + Y \cdot f_1(X)

Example

Step 3: Given the coefficients of f0(X)f_0(X) and f1(X)f_1(X):

f0(X)=12+26⋅X+π(X)+28⋅X⋅π(X) f_0(X) = 12 + 26 \cdot X + \pi(X) + 28 \cdot X \cdot \pi(X)

f1(X)=11+26⋅X+14⋅π(X)+20⋅X⋅π(X) f_1(X) = 11 + 26 \cdot X + 14 \cdot \pi(X) + 20 \cdot X \cdot \pi(X)

we reconstruct the original polynomial f(X,Y)f(X, Y) using the decomposition:

f(X,Y)=f0(X)+Y⋅f1(X) f(X, Y) = f_0(X) + Y \cdot f_1(X)

Substituting the expressions for f0f_0 and f1f_1, we get:

f(X)=12+26⋅X+π(X)+28⋅X⋅π(X)+f(X) = 12 + 26 \cdot X + \pi(X) + 28 \cdot X \cdot \pi(X) + \qquad \qquad \qquad

Y⋅(11+26⋅X+14⋅π(X)+20⋅X⋅π(X)) \qquad Y \cdot (11 + 26 \cdot X + 14 \cdot \pi(X) + 20 \cdot X \cdot \pi(X))f(X)=12+26⋅X+π(X)+28⋅X⋅π(X)+f(X) = 12 + 26 \cdot X + \pi(X) + 28 \cdot X \cdot \pi(X) + \quad \quad \quad \quad \quad \quad

11⋅Y+26⋅X⋅Y+14⋅Y⋅π(X)+20⋅X⋅Y⋅π(X)\quad \quad \quad \quad \quad \quad 11 \cdot Y + 26 \cdot X \cdot Y + 14 \cdot Y \cdot \pi(X) + 20 \cdot X \cdot Y \cdot \pi(X)

Implementation#

Here’s the Circle FFT implementation written in SageMath.

def cfft(evals, D):
    n = len(evals)
    assert (n & (n - 1)) == 0
    
    # Step 1: Split using map \pi_x
    S = pi_x(D)
    eval0 = []
    eval1 = []
    for e0, e1, P in zip(evals[:n//2], evals[n//2:], D):
        eval0.append((e0 + e1) / 2)
        eval1.append((e0 - e1) / (2 * get_y(P)))
    
    # Step 2: Recursively apply FFT using map \pi
    coeffs0 = cfft_pi(eval0, S)
    coeffs1 = cfft_pi(eval1, S)

    # Step 3: Recombine to compute coefficients
    coeffs = to_natural_order(coeffs0 + coeffs1)

    return coeffs

def cfft_pi(evals, S):
    n = len(evals)
    assert (n & (n - 1)) == 0

    if n == 1:
        return [evals[0]]
    
    S_next = pi(S)
    eval0 = []
    eval1 = []
    for e0, e1, x in zip(evals[:n//2], evals[n//2:], S):
        eval0.append((e0 + e1) / 2)
        eval1.append((e0 - e1) / (2 * x))
    
    coeffs0 = cfft_pi(eval0, S_next)
    coeffs1 = cfft_pi(eval1, S_next)
        
    return coeffs0 + coeffs1

Basis for FFT#

The FFT algorithm computes the coefficients with respect to some basis polynomials. For example, the Cooley-Tukey algorithm computes the coefficients cjc_j with respect to basis polynomials bj(n)(X)b_j^{(n)}(X) such that

f(X)=∑j=0kn−1cj⋅bj(n)(X) f(X) = \sum_{j = 0}^{k^n - 1} c_j \cdot b_j^{(n)}(X)

The basis with respect to which the coefficients are computed depend on the choice of the polynomial map ψ\psi and the twiddles t0,t1,⋯tk−1t_0, t_1, \cdots t_{k-1} used at each layer of the FFT algorithm.

To see this in action, let us walk through a simple example. Consider the Cooley–Tukey FFT algorithm over a domain of size 4 to compute the coefficients of a polynomial of degree at most 3. First, we decompose the original polynomial f(X)f(X):

f(X)=t0(X)⋅f0(ψ(X))+t1(X)⋅f1(ψ(X)) f(X) = t_0(X) \cdot f_0(\psi(X)) + t_1(X) \cdot f_1(\psi(X))

Then, we decompose the two sub-polynomials f0(X)f_{0}(X) and f1(X)f_{1}(X):

f0(X)=t0(X)⋅f00(ψ(X))+t1(X)⋅f01(ψ(X)) f_{0}(X) = t_0(X) \cdot f_{00}(\psi(X)) + t_1(X) \cdot f_{01}(\psi(X)) f1(X)=t0(X)⋅f10(ψ(X))+t1(X)⋅f11(ψ(X)) f_{1}(X) = t_0(X) \cdot f_{10}(\psi(X)) + t_1(X) \cdot f_{11}(\psi(X))

Suppose f00(X)f_{00}(X), f01(X)f_{01}(X), f10(X)f_{10}(X) and f11(X)f_{11}(X) are constant polynomials. Then

f0(X)=a⋅t0(X)+b⋅t1(X) f_{0}(X) = a \cdot t_0(X) + b \cdot t_1(X) f1(X)=c⋅t0(X)+d⋅t1(X) f_{1}(X) = c \cdot t_0(X) + d \cdot t_1(X)

Substitute the above values in decomposition of f(X)f(X):

f(X)=t0(X)⋅(a⋅t0(ψ(X))+b⋅t1(ψ(X)))+t1(X)⋅(c⋅t0(ψ(X))+d⋅t1(ψ(X))) f(X) = t_0(X) \cdot (a \cdot t_0(\psi(X)) + b \cdot t_1(\psi(X))) + t_1(X) \cdot (c \cdot t_0(\psi(X)) + d \cdot t_1(\psi(X)))

Thus the above algorithm computes the coefficients with respect to the basis: t0(X)⋅t0(ψ(X))t_0(X) \cdot t_0(\psi(X)), t0(X)⋅t1(ψ(X))t_0(X) \cdot t_1(\psi(X)), t1(X)⋅t0(ψ(X))t_1(X) \cdot t_0(\psi(X)) and t1(X)⋅t1(ψ(X))t_1(X) \cdot t_1(\psi(X)).

Thus depending on the choice of polynomial map ψ\psi and twiddles t0,t1t_0, t_1, the FFT algorithm outputs the coefficients with respect to a particular basis.

  • Choice 1: We use the polynomial map ψ(X)=X2\psi(X) = X^2 and twiddles t0(X)=1,t1(X)=Xt_0(X) = 1, t_1(X) = X. This gives us coefficients with respect to the monomial basis as follows:

    b(X)={1,X,X2,X3}b(X) = \{1, X, X^2, X^3\}
  • Choice 2: We use the polynomial map ψ(X)=X2\psi(X) = X^2 and twiddles t0(X)=X,t1(X)=X+1t_0(X) = X, t_1(X) = X+1. This gives us coefficients with respect to the following basis:

    b(X)={X⋅X2,  X2⋅(X+1),  X⋅(X2+1),  (X2+1)⋅(X+1)}b(X) = \{X \cdot X^2,\; X^2 \cdot (X + 1),\; X \cdot (X^2 + 1),\; (X^2 + 1) \cdot (X + 1)\}

Similarly, circle FFT outputs coefficients cjc_j with respect to some basis bj(n)(X,Y)b_j^{(n)}(X, Y) such that:

f(X,Y)=∑j=02n−1cj⋅bj(n)(X,Y) f(X, Y) = \sum_{j = 0}^{2^n - 1} c_j \cdot b_j^{(n)}(X, Y)

And the basis with respect to which the coefficients are computed depends on the choice of the polynomial map and the twiddles. In the concrete example of Circle FFT over twin-coset D3D_3, we saw that the algorithm computed the coefficient with respect to the following basis:

bj(3)(X,Y)=[1,X,π(X),X⋅π(X),Y,Y⋅X,Y⋅π(X),Y⋅X⋅π(X)]b^{(3)}_j(X, Y) = [1, X, \pi(X), X \cdot \pi(X), Y, Y \cdot X, Y \cdot \pi(X), Y \cdot X \cdot \pi(X)]

Using induction on nn, we can show that the Circle FFT algorithm outputs coefficients with respect to the following basis [Theorem 2, Circle STARKs]:

bj(n)(X,Y):=Yj0⋅Xj1⋅π(X)j2⋅π2(X)j3⋯πn−2(X)jn−1 b^{(n)}_j(X, Y) := Y^{j_0} \cdot X^{j_1} \cdot \pi(X)^{j_2} \cdot \pi^2(X)^{j_3} \cdots \pi^{n-2}(X)^{j_{n-1}}

where 0≤j≤2n−10 \leq j \leq 2^n - 1 and (j0,…,jn−1)∈{0,1}n(j_0, \ldots, j_{n-1}) \in \{0, 1\}^n is the binary representation of jj, i.e.,

j=j0+j1⋅2+⋯+jn−1⋅2n−1.j = j_0 + j_1 \cdot 2 + \cdots + j_{n-1} \cdot 2^{n-1}.

Note

In the paper, the elements of the basis b(n)(X,Y)b^{(n)}(X, Y) are defined as:

bjn(X,Y):=Yj0⋅v1(X)j1⋯vn−1(X)jn−1 b^{n}_j(X, Y) := Y^{j_0} \cdot v_1(X)^{j_1} \cdots v_{n-1}(X)^{j_{n-1}}

Here, each vk(X)v_k(X) (for 1≤k≤n−11 \leq k \leq n - 1) is the vanishing polynomial of the standard position coset of size 2k2^k, as described in Part II.

This aligns with our earlier definition of the basis, using the substitution:

vk(X)=πk−1(X),1≤k≤n−1. v_k(X) = \pi^{k-1}(X), \quad 1 \leq k \leq n - 1.

Dimension Gap#

Let the space spanned by the basis polynomials in b(n)(X,Y)b^{(n)}(X, Y) be LN′(F)L'_N(\mathbb{F}). The basis b(n)(X,Y)b^{(n)}(X, Y) has a total N=2nN=2^n elements and thus the dimension of the space LN′(F)L'_N(\mathbb{F}) is NN. However, the space of bivariate polynomials over the circle curve is LN(F)L_N(\mathbb{F}) which has dimension N+1N+1, as discussed in Part II.

We can identify the missing highest total degree element in LN′(F)L'_N(\mathbb{F}) by examining the basis. The highest total degree element in basis b(n)(X,Y)b^{(n)}(X, Y) is:

Y⋅X⋅π(X)⋅π2(X)⋅π3(X)⋯πn−2(X)Y \cdot X \cdot \pi(X) \cdot \pi^2(X) \cdot \pi^3(X) \cdots \pi^{n-2}(X)

Using deg(πj(X)=2j)deg(\pi^j(X) = 2^j), the highest degree of XX in the above term is:

1+2+22+23+⋯+2n−2=2n−1−1=N/2−11 + 2 + 2^2 + 2^3 + \cdots + 2^{n-2} = 2^{n-1} - 1 = N/2 - 1

Since the highest degree of XX in LN′(F)L'_N(\mathbb{F}) is N/2−1N/2 - 1, we can represent the space LN′(F)L'_N(\mathbb{F}) as follows:

LN′(F)=F[X]≤N/2−1+Y⋅F[X]≤N/2−1 L'_N(\mathbb{F}) = \mathbb{F}[X]^{ \leq N/2 - 1} + Y \cdot \mathbb{F}[X]^{ \leq N/2 - 1}

where F[X]≤N/2−1\mathbb{F}[X]^{ \leq N/2 - 1} represents polynomials of degree at most N/2−1N/2 - 1 with coefficients in F\mathbb{F}. Similarly, we can represent the space LN(F)L_N(\mathbb{F}) as:

LN(F)=F[X]≤N/2+Y⋅F[X]≤N/2−1 L_N(\mathbb{F}) = \mathbb{F}[X]^{ \leq N/2} + Y \cdot \mathbb{F}[X]^{ \leq N/2 - 1}

Thus the space LN′(F)L'_N(\mathbb{F}) does not include the monomial XN/2X^{N/2}, which lies in the space LN(F)L_N(\mathbb{F}). Therefore,

LN(F)=LN′(F)+⟨XN/2⟩ L_N(\mathbb{F}) = L'_N(\mathbb{F}) + \langle X^{N/2} \rangle

Since the space spanned by XN/2X^{N/2} is same as the space spanned by the vanishing polynomial vn(X)v_n(X) which has degree deg(vn)=2n−1=N/2deg(v_n) = 2^{n-1} =N/2, we can also write:

LN(F)=LN′(F)+⟨vn⟩ L_N(\mathbb{F}) = L'_N(\mathbb{F}) + \langle v_n \rangle

A consequence of this dimension gap is that we cannot interpolate some polynomials over the circle i.e. those with XN/2X^{N/2}. We will see how to handle this in the next part of this series.

Conclusion#

In this blog post, we explored the Circle FFT and its structural similarity to the classical Cooley–Tukey FFT. We also discussed the dimension gap between the space of polynomials over the circle curve and the space spanned by the basis of the Circle FFT.

We did not cover the inverse FFT, i.e., evaluating a polynomial at a given set of points. However, the approach is conceptually similar and all operations can be run in reverse. This is left as an exercise to the reader.

In the next part, we’ll address how this dimension gap is handled within the FRI protocol and see how all the ideas from the previous posts come together to build the Circle STARK.