FFT / IFFT

from polynomial evaluation to divide-and-conquer butterflies, roots of unity, finite-field FFTs, and why ZK provers care about them

Why do we need the FFT?

In the previous topic, we saw that a polynomial can be represented in two important ways:

coefficients evaluations

Suppose we have:

f(x) = a₀ + a₁x + a₂x² + … + aₙ₋₁xⁿ⁻¹

and we want to evaluate it at many points:

f(x₀), f(x₁), …, f(xₙ₋₁)

If we evaluate the polynomial independently at every point, the straightforward method takes roughly O(n²) work.

The Fast Fourier Transform is a clever algorithm that computes the same transformation in O(n log n) time by exploiting a special structure in the evaluation points.

Core mental model: FFT is not a different mathematical transform from the DFT. It is a fast algorithm for computing the same transform by recursively reusing work.

Start with the DFT

Before understanding the FFT, we need to understand the Discrete Fourier Transform.

Choose an integer n and a special element ω satisfying:

ωⁿ = 1

Ideally, ω has exact order n:

ωⁿ = 1
ωᵏ ≠ 1 for 0 < k < n

Such an element is called a primitive n-th root of unity.

Now evaluate the polynomial at the structured points:

1, ω, ω², …, ωⁿ⁻¹

At point ωᵏ:

f(ωᵏ) = Σⱼ aⱼ(ωᵏ)ʲ
= Σⱼ aⱼωʲᵏ

Therefore the DFT is:

forward DFT
yₖ = Σⱼ₌₀ⁿ⁻¹ aⱼωʲᵏ

This is exactly the evaluation of a polynomial at the powers of ω.

The DFT as a matrix

The DFT can also be written as matrix multiplication.

[y₀] [1 1 1 …] [a₀]
[y₁] = [1 ω ω² …] [a₁]
[y₂] [1 ω² ω⁴ …] [a₂]
[ ⋮ ] [ ⋮ ⋮ ⋮ ⋱] [ ⋮ ]

The matrix contains powers of ω. A generic matrix-vector multiplication needs multiplications, which is exactly the expensive part we want to avoid.

The FFT does not change this matrix. Instead, it discovers that the matrix has a recursive structure.

The key identity behind the FFT

Assume n is even. Split the polynomial into its even and odd coefficients:

f(x) = a₀ + a₁x + a₂x² + a₃x³ + …
= (a₀ + a₂x² + a₄x⁴ + …)
+ x(a₁ + a₃x² + a₅x⁴ + …)

Define:

E(x) = a₀ + a₂x + a₄x² + …
O(x) = a₁ + a₃x + a₅x² + …

Then:

f(x) = E(x²) + xO(x²)

This equation is the heart of radix-2 FFT.

The breakthrough: an n-point polynomial evaluation problem becomes two problems of size n/2, plus a small amount of work to combine their answers.

Why roots of unity make the split work

We evaluate at:

1, ω, ω², …, ωⁿ⁻¹

For the first half of the points, we use:

f(ωᵏ) = E(ω²ᵏ) + ωᵏO(ω²ᵏ)

Now look at the second half:

f(ωᵏ⁺ⁿᐟ²)
= E(ω²ᵏ⁺ⁿ) + ωᵏ⁺ⁿᐟ²O(ω²ᵏ⁺ⁿ)

Because ωⁿ = 1:

ω²ᵏ⁺ⁿ = ω²ᵏ

And because ωⁿᐟ² = −1 for a primitive even-order root:

f(ωᵏ⁺ⁿᐟ²) = E(ω²ᵏ) − ωᵏO(ω²ᵏ)

So once we know the even and odd transforms, two outputs are obtained with just:

yₖ = Eₖ + ωᵏOₖ
yₖ₊ₙᐟ² = Eₖ − ωᵏOₖ

The butterfly

Those two equations are called a butterfly operation.

             Eₖ
              │
              ├────── (+) ────── yₖ
              │        ↑
              │       ωᵏ
              │        │
             Oₖ ───────┘
              │
              ├────── (−) ────── yₖ₊ₙ/₂
              │
              └── multiply by ωᵏ

More precisely:

t = ωᵏOₖ
yₖ = Eₖ + t
yₖ₊ₙᐟ² = Eₖ − t

The same pattern appears repeatedly at every level of the FFT recursion.

A complete 4-point FFT by hand

Take four coefficients:

a₀, a₁, a₂, a₃

The polynomial is:

f(x) = a₀ + a₁x + a₂x² + a₃x³

Split into even and odd parts:

E(x) = a₀ + a₂x
O(x) = a₁ + a₃x

Now each is only a 2-point transform. If ω is a primitive 4th root of unity, then:

ω² = −1

The first two outputs are:

y₀ = E₀ + O₀
y₁ = E₁ + ωO₁

and the remaining two are:

y₂ = E₀ − O₀
y₃ = E₁ − ωO₁

Notice what happened: four evaluations were reduced to smaller two-point transforms and a handful of butterflies.

Where does O(n log n) come from?

At every recursion level, the total number of elements being processed is still n.

For a power-of-two input, the recursion has:

log₂(n) levels

Each level performs roughly n work, so:

n × log₂(n) = O(n log n)
naive DFT

For every output, sum n terms. Total: approximately n² operations.

FFT

Reuse the even/odd structure recursively. Total: approximately n log₂ n operations.

For large proving systems, this difference is enormous.

What is the IFFT?

The inverse FFT reverses the forward transformation.

The forward transform is:

yₖ = Σⱼ aⱼωʲᵏ

To recover the coefficients, use the inverse root:

ω⁻¹

and divide by n:

inverse DFT
aⱼ = n⁻¹ Σₖ yₖω⁻ʲᵏ

The inverse FFT is simply a fast algorithm for computing this inverse transform.

Important: IFFT is not “FFT with a different output.” It is the inverse linear transformation. The inverse root of unity and multiplication by the field element n⁻¹ are essential.

Why does the inverse formula work?

The key identity is the orthogonality of roots of unity:

Σₖ₌₀ⁿ⁻¹ ωᵏᵐ = { n, if m ≡ 0 (mod n) { 0, otherwise

Start with the proposed inverse:

n⁻¹ Σₖ yₖω⁻ʲᵏ

Substitute the forward transform:

n⁻¹ Σₖ (Σᵣ aᵣωʳᵏ)ω⁻ʲᵏ
= n⁻¹ Σᵣ aᵣ Σₖ ωᵏ(r−j)

Every term disappears except the one where r=j. That surviving term contributes n, which is canceled by n⁻¹.

aⱼ = aⱼ

That is the algebraic reason the inverse transform recovers the original coefficients.

FFT over a finite field

In ZK systems, we usually do not perform FFTs over floating-point complex numbers. We perform them inside a finite field.

The same mathematics works as long as the field contains a primitive n-th root of unity.

ωⁿ = 1
ord(ω) = n

For a prime field Fₚ, the non-zero elements form a multiplicative group of size:

p − 1

Therefore an n-th root of unity can exist only when:

n | (p − 1)

This is one of the most important field-selection requirements for FFT-friendly proving systems.

Example: a 4-point FFT in F₁₇

Take the field:

F₁₇ = {0,1,2,…,16}

We need a primitive 4th root of unity. Take:

ω = 4

Check:

4² = 16 ≡ −1 (mod 17)
4⁴ = 16² ≡ 1 (mod 17)

And neither nor equals 1, so 4 has order 4.

Therefore our evaluation domain is:

{1, 4, 16, 13}

because:

4⁰ = 1
4¹ = 4
4² = 16
4³ = 64 ≡ 13 (mod 17)
4⁴ = 52 ≡ 1 (mod 17)

The FFT evaluates the polynomial exactly at those four field elements. There are no floating-point approximations.

Recursive radix-2 FFT

The mathematical recursion can be written almost directly as code:

FFT(a) = combine(FFT(even(a)), FFT(odd(a)))

For each pair of outputs:

t = ωᵏ · odd[k]
out[k] = even[k] + t
out[k+n/2] = even[k] − t

At the base case, a one-element transform is already complete.

From the math to Rust

Here is a small recursive FFT implementation over F₁₇. It deliberately mirrors the derivation above rather than hiding the algorithm behind a library.

Recursive FFT / IFFT over F₁₇
rust
// Radix-2 FFT / IFFT over F_17.
// Input length must be a power of two.

const P: u64 = 17;

fn add(a: u64, b: u64) -> u64 {
    (a + b) % P
}

fn sub(a: u64, b: u64) -> u64 {
    (a + P - b) % P
}

fn mul(a: u64, b: u64) -> u64 {
    (a * b) % P
}

fn pow(mut base: u64, mut exp: u64) -> u64 {
    let mut result = 1;

    while exp > 0 {
        if exp & 1 == 1 {
            result = mul(result, base);
        }

        base = mul(base, base);
        exp >>= 1;
    }

    result
}

fn inv(a: u64) -> u64 {
    assert!(a != 0);
    pow(a, P - 2)
}

fn is_power_of_two(n: usize) -> bool {
    n != 0 && (n & (n - 1)) == 0
}

// Recursive radix-2 FFT.
// root is a primitive n-th root of unity.
fn fft(values: &[u64], root: u64) -> Vec<u64> {
    let n = values.len();

    assert!(is_power_of_two(n));

    if n == 1 {
        return vec![values[0] % P];
    }

    // Split into even and odd coefficients.
    let even: Vec<u64> =
        values.iter().step_by(2).copied().collect();

    let odd: Vec<u64> =
        values.iter().skip(1).step_by(2).copied().collect();

    // root^2 is a primitive (n/2)-th root.
    let half_root = mul(root, root);

    let even_fft = fft(&even, half_root);
    let odd_fft = fft(&odd, half_root);

    let mut output = vec![0; n];

    // Butterfly stage.
    let mut omega = 1;

    for k in 0..n / 2 {
        let t = mul(omega, odd_fft[k]);

        output[k] =
            add(even_fft[k], t);

        output[k + n / 2] =
            sub(even_fft[k], t);

        omega = mul(omega, root);
    }

    output
}

fn ifft(values: &[u64], root: u64) -> Vec<u64> {
    let n = values.len();

    // Inverse FFT uses root^(-1).
    let inverse_root = inv(root);

    let mut result =
        fft(values, inverse_root);

    // Multiply every coefficient by n^(-1).
    let n_inv = inv(n as u64);

    for value in &mut result {
        *value = mul(*value, n_inv);
    }

    result
}

fn main() {
    // 4 is a primitive 4th root of unity in F_17.
    let root = 4;

    // f(x) = 1 + 2x + 3x^2 + 4x^3
    let coefficients = vec![1, 2, 3, 4];

    let evaluations =
        fft(&coefficients, root);

    println!("evaluations = {:?}", evaluations);

    let recovered =
        ifft(&evaluations, root);

    println!("recovered    = {:?}", recovered);

    assert_eq!(recovered, coefficients);
}

Notice how closely the implementation follows the mathematics:

  1. Split the input into even and odd coefficients.
  2. Recursively transform both halves.
  3. Square the root to obtain the root for the smaller problem.
  4. Multiply the odd result by the appropriate power of ω.
  5. Add and subtract to form the butterfly outputs.
  6. For IFFT, use ω⁻¹.
  7. Finally multiply every result by n⁻¹.

Why production FFT implementations look different

The recursive version is excellent for understanding the algorithm, but a high-performance prover normally avoids allocating new vectors at every recursion level.

Production implementations commonly use an iterative in-place FFT:

educational version

Recursive, simple, directly matches the mathematical derivation.

production version

Usually iterative and in-place, minimizing allocations and improving cache behavior.

For radix-2 FFT, the iterative algorithm typically starts with a bit-reversal permutation, then executes butterfly layers of size 2, 4, 8, and so on until the entire vector has been transformed.

n = 8
stages: 2 → 4 → 8

The mathematics is the same. Only the execution order changes.

Why FFT is so important in ZK

FFT is not just a generic optimization that happens to be useful for cryptography. It is one of the core pieces that makes large polynomial workloads practical.

ZK componentFFT connection
trace interpolationConvert execution-trace values into polynomial representations.
low-degree extensionEvaluate trace polynomials over a larger structured domain.
polynomial multiplicationFFT can turn convolution into pointwise multiplication.
AIR / compositionLarge polynomial expressions can be evaluated efficiently over domains.
FRIWorks on evaluations of low-degree polynomials over structured domains.
KZGPolynomial operations and evaluations are central to commitment/opening workflows.

A simplified STARK-style flow is:

execution trace trace values → FFT / interpolation trace polynomial
trace polynomial → evaluate on larger domain LDE FRI

FFT and polynomial multiplication

There is another extremely important reason FFT matters: polynomial multiplication is convolution of coefficients.

If:

A(x) = Σᵢ aᵢxⁱ
B(x) = Σⱼ bⱼxʲ

then:

C(x) = A(x)B(x)
cₖ = Σᵢ aᵢbₖ₋ᵢ

That is exactly convolution.

FFT converts this into a much easier operation:

A coefficients → FFT → A evaluations
B coefficients → FFT → B evaluations
A(x)·B(x) ← pointwise multiply ← A evaluations × B evaluations
product coefficients ← IFFT ← product evaluations

So polynomial multiplication can be reduced to:

FFT(A) + FFT(B) + pointwise multiplication + IFFT

This is the same high-level idea behind fast convolution algorithms.

FFT-friendly domains and 2-adicity

Radix-2 FFT repeatedly needs domains whose size can be divided by two:

n, n/2, n/4, n/8, …, 1

Therefore powers of two are especially convenient:

n = 2ᵏ

For a prime field Fₚ, we need:

2ᵏ | (p−1)

The largest power of two dividing p−1 is related to the field's 2-adicity.

This matters enormously in ZK engineering because large radix-2 evaluation domains allow efficient FFTs and FRI-friendly polynomial processing.

Common mistakes

mistake 01

Thinking FFT and DFT are different mathematical transforms. FFT is a fast way to compute the DFT.

mistake 02

Forgetting the inverse scaling factor n⁻¹ in IFFT.

mistake 03

Using an element that satisfies ωⁿ=1 but does not actually have order n.

mistake 04

Assuming the complex-number FFT is directly what a STARK prover uses. ZK provers generally work in finite fields.

mistake 05

Ignoring domain size requirements. A radix-2 FFT needs the appropriate roots of unity at every recursive level.

mistake 06

Writing a recursive implementation and assuming it is automatically production-fast. Allocation and memory behavior matter.

The whole idea in one map

first-principles map
1. A polynomial can be represented by coefficients or evaluations.
2. Evaluating at n arbitrary points naively costs O(n²).
3. Choose structured points 1, ω, ω², …, ωⁿ⁻¹.
4. Require ω to be a primitive n-th root of unity.
5. Split coefficients into even and odd parts.
6. Recursively transform both halves.
7. Combine them using the butterfly.
8. This gives O(n log n) instead of O(n²).
9. IFFT uses ω⁻¹ and multiplies by n⁻¹.
10. In ZK, the transform happens over finite fields.
11. Roots of unity and field 2-adicity determine FFT-friendly domains.
12. FFT/IFFT power interpolation, LDE, polynomial multiplication, and major parts of STARK/FRI pipelines.

If polynomials answer “what algebraic object represents the computation?”, FFT answers “how can we move between its coefficient and evaluation representations fast enough to actually build a prover?”