Circle Group
As discussed in the previous section, Mersenne prime field lacks a smooth subgroup whose order is a large power of two. This property makes such fields unsuitable for instantiating STARK protocols. To address this, we consider extensions of that have smooth subgroups, which are suitable for performing FFTs and implementing the FRI protocol.
For a field extension of , we define the circle curve as the set of points satisfying the relation:
In Stwo implementation, a point on the circle is defined as follows:
pub struct CirclePoint<F> {
pub x: F,
pub y: F,
}
The set forms a cyclic group under the operation defined by:
Here, the group is defined additively, which differs from the multiplicative notation used in the Circle STARKs paper. In this documentation, we adopt the additive notation for consistency with the implementation. The above group operation is implemented as:
fn add(self, rhs: Self) -> Self::Output {
let x = self.x.clone() * rhs.x.clone() - self.y.clone() * rhs.y.clone();
let y = self.x * rhs.y + self.y * rhs.x;
Self { x, y }
}
The identity element in this group is , implemented as:
pub fn zero() -> Self {
Self {
x: F::one(),
y: F::zero(),
}
}
Negation in the circle group corresponds to the conjugation map , defined by:
This is same as complex conjugation in complex numbers. In Stwo, the conjugate of a CirclePoint
is computed as:
pub fn conjugate(&self) -> CirclePoint<F> {
Self {
x: self.x.clone(),
y: -self.y.clone(),
}
}
The total number of points in the circle group is given by . Specifically, for , the number of points is , which, as discussed earlier, is a large power of two and can thus be used in STARK protocol instantiations. This result is proven in Lemma 1 of the Circle STARKs paper.
In Stwo implementation, the generator of the group is defined as: Subgroups of of size can be generated using the following function:
pub fn subgroup_gen(n: u32) -> CirclePoint<F> {
assert!(n <= M31_CIRCLE_LOG_ORDER); // M31_CIRCLE_LOG_ORDER = 31
let s = 1 << (M31_CIRCLE_LOG_ORDER - n);
M31_CIRCLE_GEN.mul(s) // M31_CIRCLE_GEN = g = (2, 1268011823)
}
To generate a subgroup of size , the function computes , i.e. it applies the group law to the generator with itself times, as shown below:
pub fn mul(&self, mut scalar: u128) -> CirclePoint<F> {
let mut res = Self::zero();
let mut cur = self.clone();
while scalar > 0 {
if scalar & 1 == 1 {
res = res + cur.clone();
}
cur = cur.double();
scalar >>= 1;
}
res
}
Hence, the point serves as a generator of a subgroup of order .
Circle Domain
In a STARK protocol, the computation trace is interpolated as a low-degree polynomial over a domain using FFT. For Circle STARKs, this domain consists of points on the circle curve and is referred to as the circle domain. The circle domain of size is constructed as the union of two disjoint cosets: Here, is a subgroup of size , and is the coset offset such that . This union is also called the twin-coset. The second coset in the union can be viewed as the negation (or conjugation) of the first: Therefore, it suffices to store only the half coset , and generate the full domain via its conjugates. The circle domain is defined in Stwo as:
pub struct CircleDomain {
pub half_coset: Coset,
}
The following figure shows a circle domain of size 8. It is constructed from the half coset of size 4 (shown as red points) and its negation (shown as blue points).
To iterate over all points in the circle domain, we can iterate over the half coset and its conjugates:
pub fn iter(&self) -> CircleDomainIterator {
self.half_coset
.iter()
.chain(self.half_coset.conjugate().iter())
}
Canonic Coset
For a specific choice of offset , the twin-coset becomes a coset of a larger subgroup. In particular, if is a generator of a subgroup of order , then: This result is proven in Proposition 1 of the Circle STARKs paper. Such domains are called standard position coset, or are referred to as canonic cosets. They are implemented as follows:
pub struct CanonicCoset {
pub coset: Coset,
}
Here, CanonicCoset
represents the full coset , while CircleDomain
is represented with its half coset . Thus to compute the CircleDomain
from the CanonicCoset
, first calculate the half coset , which will be used to initialize the CircleDomain
as shown below:
pub fn circle_domain(&self) -> CircleDomain {
CircleDomain::new(self.half_coset())
}
The following figure shows a canonic coset of size 8. It is constructed from the coset of size 8 followed by an offset by , where is the generator of subgroup .
We can verify whether a given CircleDomain
is canonic by checking the step size of the half coset against the initial coset offset. In the CircleDomain
implementation, only the half coset is explicitly stored. If CircleDomain
is canonic, must be a generator of the subgroup , which has order i.e. . Recall that the generator of the subgroup is .
Thus, the step size between consecutive elements in the half coset is , and the initial point is . Therefore, the ratio between the step size and the initial coset offset is:
This means that in a canonic coset, the step size is exactly four times the initial coset offset. This condition is used to check whether a CircleDomain
is canonic, as shown below:
pub fn is_canonic(&self) -> bool {
self.half_coset.initial_index * 4 == self.half_coset.step_size
}
In the next section, we will dive into polynomials defined over the circle.