PrivacyCircle STARK Verifier

Field Arithmetic: M31 → CM31 → QM31

Deep dive into the Mersenne-31 field tower implementation in Solidity

Overview

The Circle STARK verifier operates over a three-level algebraic extension tower built on the Mersenne-31 prime. This page covers the mathematical definitions, Solidity implementation details, packing conventions, and test vectors for each level.

All arithmetic is implemented in M31Lib.sol (311 lines).

1. M31 — The Base Field

Definition

Fp where p=2311=2,147,483,647\mathbb{F}_p \text{ where } p = 2^{31} - 1 = 2{,}147{,}483{,}647

This is the largest Mersenne prime that fits in 31 bits. Elements are integers in [0,p)[0, p).

Why Mersenne-31?

Mersenne primes enable shift-based modular reduction. For any product x=a×bx = a \times b (at most 62 bits):

xmod(2311)=(x31)+(x&(2311))x \bmod (2^{31} - 1) = (x \gg 31) + (x \mathbin{\&} (2^{31} - 1))

Proof: Write x=h231+x = h \cdot 2^{31} + \ell. Then x=h(p+1)+=hp+h+h+(modp)x = h \cdot (p+1) + \ell = h \cdot p + h + \ell \equiv h + \ell \pmod{p}.

The result may be p\geq p, requiring at most one subtraction. This is dramatically cheaper than general modular reduction.

Solidity Implementation

// M31 addition: single add + conditional subtract
function m31Add(uint32 a, uint32 b) internal pure returns (uint32) {
    uint256 sum = uint256(a) + uint256(b);
    if (sum >= P) sum -= P;
    return uint32(sum);
}

// M31 multiplication: Mersenne reduction
function m31Mul(uint32 a, uint32 b) internal pure returns (uint32) {
    uint256 product = uint256(a) * uint256(b);
    uint256 lo = product & P;     // low 31 bits
    uint256 hi = product >> 31;   // high bits
    uint256 r = lo + hi;
    if (r >= P) r -= P;
    return uint32(r);
}

// M31 inversion via Fermat: a^(p-2) mod p
function m31Inv(uint32 a) internal pure returns (uint32) {
    require(a != 0, "M31: division by zero");
    return m31Exp(a, P - 2);
}

Representation

M31 elements are stored as uint32. The maximum valid value is p1=2,147,483,646p - 1 = 2{,}147{,}483{,}646. Zero is represented as 0, not as pp.

Test Vectors

OperationInputResult
m31Add(P-1, 1)2147483646+12147483646 + 100
m31Add(P-1, P-1)2147483646+21474836462147483646 + 214748364621474836452147483645
m31Mul(2, 2)4444
m31Mul(P-1, P-1)(p1)2modp(p-1)^2 \bmod p11
m31Inv(2)2p2modp2^{p-2} \bmod p10737418241073741824

2. CM31 — Complex Extension

Definition

Fp2=Fp[i]  /  (i2+1)\mathbb{F}_{p^2} = \mathbb{F}_p[i] \;/\; (i^2 + 1)

Elements are a+bia + bi where a,bFpa, b \in \mathbb{F}_p and i2=1i^2 = -1.

This is the Gaussian integers modulo pp. The polynomial x2+1x^2 + 1 is irreducible over Fp\mathbb{F}_p because 1-1 is a quadratic non-residue modulo pp (since p3(mod4)p \equiv 3 \pmod 4).

Packing Convention

CM31 is packed into uint64:

  • Low 32 bits: real part (aa)
  • High 32 bits: imaginary part (bb)
function cm31Pack(uint32 real, uint32 imag) internal pure returns (uint64) {
    return uint64(real) | (uint64(imag) << 32);
}

Arithmetic

Multiplication: Standard complex multiplication

(a0+a1i)(b0+b1i)=(a0b0a1b1)+(a0b1+a1b0)i(a_0 + a_1 i)(b_0 + b_1 i) = (a_0 b_0 - a_1 b_1) + (a_0 b_1 + a_1 b_0)i

4 M31 multiplications + 1 addition + 1 subtraction.

Conjugation: (a+bi)(abi)(a + bi) \mapsto (a - bi). Negates only the imaginary part.

Inversion: Uses the norm identity

1a+bi=abia2+b2\frac{1}{a + bi} = \frac{a - bi}{a^2 + b^2}

The denominator a2+b2a^2 + b^2 is an M31 element (guaranteed nonzero for nonzero CM31), inverted via m31Inv.

Negation: (a+bi)=(a)+(b)i-(a + bi) = (-a) + (-b)i. Negates both components.

cm31Neg vs cm31Conj

These are distinct operations:

  • cm31Neg(a + bi) = (a,b)(-a, -b) — negates both parts
  • cm31Conj(a + bi) = (a,b)(a, -b) — negates only imaginary

This distinction is critical for QM31 complex conjugation (see Section 3).

3. QM31 — Quartic Extension (SecureField)

Definition

Fp4=Fp2[u]  /  (u2(2+i))\mathbb{F}_{p^4} = \mathbb{F}_{p^2}[u] \;/\; (u^2 - (2 + i))

Elements are a+bua + bu where a,bFp2a, b \in \mathbb{F}_{p^2} (CM31) and u2=2+iu^2 = 2 + i.

The constant R=2+iR = 2 + i is chosen so that u2Ru^2 - R is irreducible over Fp2\mathbb{F}_{p^2}. This means 2+i2 + i must be a quadratic non-residue in CM31.

QM31 is Stwo's SecureField — the field used for all random challenges, ensuring they are drawn from a space of size 2124\sim 2^{124}.

Packing Convention

QM31 is packed into uint128:

  • Low 64 bits: first CM31 component (aa)
  • High 64 bits: second CM31 component (bb)

Which expands to four M31 values:

  • Bits [0..31]: m0m_0 (real part of aa)
  • Bits [32..63]: m1m_1 (imaginary part of aa)
  • Bits [64..95]: m2m_2 (real part of bb)
  • Bits [96..127]: m3m_3 (imaginary part of bb)
function qm31FromM31(uint32 a, uint32 b, uint32 c, uint32 d) internal pure returns (uint128) {
    return qm31Pack(cm31Pack(a, b), cm31Pack(c, d));
}

Arithmetic

Multiplication: In Fp2[u]/(u2R)\mathbb{F}_{p^2}[u]/(u^2 - R):

(a+bu)(c+du)=(ac+Rbd)+(ad+bc)u(a + bu)(c + du) = (ac + R \cdot bd) + (ad + bc)u

This requires 4 CM31 multiplications + 1 CM31 multiplication by RR + 2 CM31 additions.

Since R=2+iR = 2 + i is a small constant, the multiplication by RR is:

R(x+yi)=(2xy)+(x+2y)iR \cdot (x + yi) = (2x - y) + (x + 2y)i

which is 4 M31 multiplications + 2 M31 additions + 1 M31 subtraction — slightly cheaper than a general CM31 multiply.

Inversion: Analogous to CM31:

1a+bu=abua2Rb2\frac{1}{a + bu} = \frac{a - bu}{a^2 - R \cdot b^2}

The denominator a2Rb2a^2 - R \cdot b^2 is a CM31 element, inverted via cm31Inv.

Complex Conjugation

Bug Alert: This Was Wrong Initially

The initial implementation performed element-wise CM31 conjugation: (m0,m1,m2,m3)(m_0, -m_1, m_2, -m_3). This is incorrect.

Stwo defines complex conjugation for QM31 as:

impl ComplexConjugate for QM31 {
    fn complex_conjugate(&self) -> Self {
        Self(self.0, -self.1)  // Negate ENTIRE second CM31
    }
}

For q=(m0,m1,m2,m3)q = (m_0, m_1, m_2, m_3):

OperationResultDescription
QM31 conjugate (correct)(m0,m1,m2,m3)(m_0, m_1, -m_2, -m_3)Negate entire second CM31
CM31 conjugate on each half (wrong)(m0,m1,m2,m3)(m_0, -m_1, m_2, -m_3)Negate imaginary of each CM31

The correct Solidity implementation:

function _conjQ(uint128 q) internal pure returns (uint128) {
    return M31Lib.qm31Pack(
        M31Lib.qm31First(q),          // Keep first CM31 unchanged
        M31Lib.cm31Neg(M31Lib.qm31Second(q))  // Negate ENTIRE second CM31
    );
}

Note: cm31Neg (negate both real and imaginary) is used, not cm31Conj (negate only imaginary). This is the algebraic conjugation in the extension Fp2[u]/(u2R)\mathbb{F}_{p^2}[u]/(u^2 - R): it maps uuu \mapsto -u, which negates the coefficient of uu as a whole CM31 element.

Why This Matters

The complex conjugate appears in every DEEP quotient line equation:

a=vˉv,c=yˉya = \bar{v} - v, \quad c = \bar{y} - y

If vˉ\bar{v} is computed incorrectly, the line coefficients aa and cc are wrong. This propagates through the numerator and corrupts the quotient. Since each quotient is accumulated with a power of α\alpha, a single wrong quotient eventually corrupts the entire FRI input.

The bug was subtle because:

  • For QM31 elements with m1=0m_1 = 0 or m2=0m_2 = 0, both implementations agree
  • The first several columns' quotients appeared correct
  • The error only became visible after accumulating enough wrong contributions

4. Circle Point Arithmetic

Definition

Points (x,y)(x, y) on the unit circle x2+y2=1x^2 + y^2 = 1 over Fp\mathbb{F}_p.

Group Operation

(x1,y1)(x2,y2)=(x1x2y1y2,  x1y2+y1x2)(x_1, y_1) \circ (x_2, y_2) = (x_1 x_2 - y_1 y_2, \; x_1 y_2 + y_1 x_2)

This is complex multiplication restricted to the unit circle. The identity is (1,0)(1, 0).

Generator

G=(2,1268011823)G = (2, 1268011823) with G=231=p+1|G| = 2^{31} = p + 1.

The generator order equals p+1p + 1 (not p1p - 1 as in multiplicative groups). This is a fundamental difference from standard STARK domains.

Scalar Multiplication

To compute GkG^k, we use binary square-and-multiply:

function circleMul(uint32 x, uint32 y, uint256 scalar)
    internal pure returns (uint32 rx, uint32 ry)
{
    rx = 1; ry = 0;  // Identity
    while (scalar > 0) {
        if (scalar & 1 == 1) {
            (rx, ry) = circleAdd(rx, ry, x, y);
        }
        (x, y) = circleAdd(x, y, x, y);
        scalar >>= 1;
    }
}

Doubling (x-coordinate only)

For FRI line domains, we only need the x-coordinate after doubling:

doublex(x)=2x21\text{double}_x(x) = 2x^2 - 1

This avoids computing the y-coordinate entirely.

5. Gas Costs

Approximate gas costs for each operation (measured via forge test):

OperationGasNotes
M31 add/sub~30Single conditional branch
M31 mul~50Mersenne reduction
M31 inv~4,00031 iterations of square-and-multiply
CM31 mul~2504 M31 muls + 2 M31 adds
CM31 inv~5,0002 M31 muls + 1 M31 inv
QM31 mul~1,2004 CM31 muls + CM31-by-R + 2 CM31 adds
QM31 inv~7,0002 CM31 muls + 1 CM31 inv + 1 CM31 neg
Circle point add~3004 M31 muls + 1 M31 add + 1 M31 sub
Circle scalar mul (31-bit)~10,000~31 iterations of add + double

QM31 multiplication is the dominant cost in the verifier — it appears in every quotient accumulation and every FRI fold.

On this page