title | tags | ||||
---|---|---|---|---|---|
47. Circle Starks |
|
Authors: Evta, looking forward to your joining
Circle STARKs, a novel construction based on the circle curve over a Mersenne prime (
Mersenne31 refers to the prime field defined by the modulus
In addition, addition operations may result in values exceeding the modulus
For standard multiplication, the product of two large integers
On the other hand, the modulus for BabyBear is
The Circle FFT is applicable when the prime
In contrast, Mersenne31 is a prime field with a modulus of
The ingenuity of Circle STARKs lies in their construction over finite fields
Under this mapping, the point
In the context of a prime
The identity element of this group is
The doubling form is as follows: given a prime
According to the double angle formulas for trigonometric functions:
$\cos(2\theta) = 2\cos^2(\theta) - 1$ $\sin(2\theta) = 2\sin(\theta)\cos(\theta)$
If we replace
$x' = \cos(2\theta) = 2\cos^2(\theta) - 1 = 2x^2 - 1$ $y' = \sin(2\theta) = 2\sin(\theta)\cos(\theta) = 2xy$
Focusing on points on the circle that occupy "odd" positions, this forms a structure akin to a Fast Fourier Transform (FFT). This doubling mapping can be applied to FFT computations by merging point sets into a one-dimensional array, then taking random linear combinations, and continuously reducing the problem size through the doubling mapping to achieve efficient calculations.
Now, this represents our FFT. First, we consolidate all points aligned vertically. The formulas equivalent to those used in standard FRI,
Next, we can take a random linear combination, resulting in a one-dimensional function
From the second round onward, the mapping alters:
Through this process, Circle FRI elegantly unites geometry, algebra, and number theory, offering powerful tools for efficient computation in cryptographic settings.
Code
Python
# This is the folding step in FRI, where you combine the evaluations at two
# sets of N coordinates each into evaluations at one set of N coordinates.
# We do three rounds of folding at a time, so each FRI step drops the degree
# by 8x
def fold(values, coeff, first_round):
for i in range(FOLDS_PER_ROUND):
full_len, half_len = values.shape[-1], values.shape[-1]//2
left, right = values[::2], values[1::2]
f0 = (left + right) * HALF
if i == 0 and first_round:
twiddle = (
invy[full_len: full_len * 2]
[folded_rbos[full_len:full_len*2:2]]
)
else:
twiddle = (
invx[full_len*2: full_len * 3]
[folded_rbos[full_len:full_len*2:2]]
)
twiddle_box = zeros_like(left)
twiddle_box[:] = twiddle.reshape((half_len,) + (1,) * (left.ndim-1))
f1 = (left - right) * HALF * twiddle_box
values = f0 + f1 * coeff
return values
C++
hashDigest_t addSubproof(
state_t<unique_ptr<dataWithCommitment>>& currState,
const vector<FieldElement>& evaluationBasis,
const subproofLocation_t& pathToProof,
const subproofLocation_t& pathFromRoot,
const bool L0isMSB
){
//
// Base case of recursion
//
if(pathToProof.size() == 0){
return currState.localState->getCommitment();
}
//
// Updating paths
//
const auto& currWay = pathToProof[0];
const subproofLocation_t nextPathToProof(pathToProof.begin()+1, pathToProof.end());
subproofLocation_t nextPathFromRoot(1,currWay);
nextPathFromRoot.insert(nextPathFromRoot.end(),pathFromRoot.begin(),pathFromRoot.end());
//
// Basis of next evaluation
//
const vector<FieldElement> basisForColumnsProof(getColumnBasis(evaluationBasis,L0isMSB));
//
// Check if current univariate already evaluated, and evaluate if needed
//
if(currState.subproofs.count(currWay) == 0){
const vector<FieldElement> BasisL0 = getL0Basis(evaluationBasis,L0isMSB);
const vector<FieldElement> BasisL1 = getL1Basis(evaluationBasis,L0isMSB);
const unsigned short logSigmentLen = getL0Basis(basisForColumnsProof,L0isMSB).size();
const unsigned short logNumSigments = getL1Basis(basisForColumnsProof,L0isMSB).size();
const unsigned short logSigmentsInBlock = std::min((unsigned short)10,logNumSigments);
const size_t sigmentLen = POW2(logSigmentLen);
const size_t sigmentsInBlock = POW2(logSigmentsInBlock);
///
/// The following is a trick for faster evaluation
///
/// We have : values of a polynomial over a space L_0
/// We want : the polynomials value over another point x not in L_0
///
/// the Lagrange polynomial for \alpha \in L_0 is:
///
/// l_\alpha (x) =
/// \frac{ \prod_{\beta \ne \alpha \in L_0} (x - \beta) }{ \prod_{\beta \ne \alpha \in L_0} (\alpha - \beta) } =
/// \frac{ Z_{L_0}(x) }{ (x-\alpha) \cdot \prod_{\beta \ne \alpha \in L_0} (\alpha - \beta) }
///
/// We Define:
///
/// C_\alpha := \prod_{\beta \ne \alpha \in L_0} (\alpha - \beta)
///
/// Thus, given values p(\alpha) for any \alpha in L_0, the value over $x$ is:
///
/// p(x) =
/// \sum_{\alpha \in L_0} p(\alpha) \cdot l_\alpha (x) =
/// Z_{L_0} (x) \cdot \sum_{\alpha \in L_0} \frac{ p(\alpha) }{(x-\alpha) \cdot C_\alpha}
///
/// In this formula many factors are independent of $x$ and can be precomputed, and this is what used bellow
///
// global auxiliary values
const size_t L0_size = POW2(BasisL0.size());
vector<FieldElement> spaceElements(L0_size);
for(unsigned int i=0; i<L0_size; i++){
spaceElements[i] = getSpaceElementByIndex(BasisL0,Algebra::zero(),i);
}
//compute Z_{L_0}
const Algebra::SubspacePolynomial Z_L0(Algebra::elementsSet_t(BasisL0.begin(),BasisL0.end()));
//compute C_\alpha vector
vector<FieldElement> C_alpha(L0_size,Algebra::one());
{
for(unsigned int i=0; i<L0_size; i++){
const FieldElement& alpha = spaceElements[i];
for(unsigned int j=0; j<L0_size; j++){
if(i==j)continue;
const FieldElement& beta = spaceElements[j];
C_alpha[i] *= (alpha - beta);
}
}
}
const auto sigmentConstructor = [&](const size_t sigmentsBlockIdx, FieldElement* res){
vector<FieldElement> vecToInveresePointwise(sigmentsInBlock*sigmentLen*L0_size);
for(unsigned int localSigmentIdx = 0; localSigmentIdx < sigmentsInBlock; localSigmentIdx++){
const size_t sigmentIdx = sigmentsBlockIdx*sigmentsInBlock + localSigmentIdx;
for(unsigned int i=0; i< sigmentLen; i++){
const size_t globalIndex = sigmentIdx * sigmentLen + i;
const FieldElement currOffset = getSpaceElementByIndex(BasisL1,zero(),globalIndex);
for(size_t j=0; j< L0_size; j++){
const FieldElement alpha = spaceElements[j];
const size_t elementIndex = localSigmentIdx*sigmentLen*L0_size + i*L0_size + j;
vecToInveresePointwise[elementIndex] = ((currWay+currOffset)-alpha)*C_alpha[j];
}
}
}
const vector<FieldElement> denuminators = Algebra::invertPointwise(vecToInveresePointwise);
for(unsigned int localSigmentIdx = 0; localSigmentIdx < sigmentsInBlock; localSigmentIdx++){
const size_t sigmentIdx = sigmentsBlockIdx*sigmentsInBlock + localSigmentIdx;
FieldElement* currSigRes = res + localSigmentIdx*sigmentLen;
for(unsigned int i=0; i< sigmentLen; i++){
const size_t globalIndex = sigmentIdx * sigmentLen + i;
const FieldElement currOffset = getSpaceElementByIndex(BasisL1,zero(),globalIndex);
currSigRes[i] = Algebra::zero();
for(size_t j=0; j< L0_size; j++){
const size_t currElemIdx = getBasisLIndex_byL0L1indices(evaluationBasis,j,globalIndex,L0isMSB);
const FieldElement alpha = spaceElements[j];
const FieldElement currVal = currState.localState->getElement(currElemIdx);
const size_t elementIndex = localSigmentIdx*sigmentLen*L0_size + i*L0_size + j;
currSigRes[i] += currVal * denuminators[elementIndex];
}
currSigRes[i] *= Z_L0.eval(currWay+currOffset);
}
}
};
currState.subproofs[currWay].localState =
unique_ptr<dataWithCommitment>(
new dataWithCommitment(
logSigmentLen + logSigmentsInBlock,
logNumSigments - logSigmentsInBlock,
sigmentConstructor
)
);
}
//
// Continue recursively
//
return addSubproof(currState.subproofs[currWay], basisForColumnsProof, nextPathToProof, nextPathFromRoot,L0isMSB);
}
Circle FFT is an algorithm closely related to the Fast Reed-Solomon Interactive Oracle Proof of Proximity (FRI) and is specifically designed for polynomial operations in Riemann-Roch spaces.
To start, the ordinary Fast Fourier Transform (FFT) is an algorithm that converts polynomials from point-value representation to coefficient representation. Given the point values of
The Circle FFT algorithm recursively decomposes the problem into smaller subproblems. Specifically, it breaks down the problem into "even" and "odd" components and progressively reduces the subdomains being processed to efficiently compute the Fourier coefficients.
The double coset
Here,
Initial Decomposition (Even and Odd Terms)
In the initial step, the function
This satisfies the relationship:
This decomposition allows
Subsequent Steps (Smaller Subproblems)
In the following steps, the algorithm recursively handles the functions obtained from the previous step
Thus, we have:
where
Output of Circle FFT
The final output of the algorithm is a set of coefficients
This indicates that the algorithm generates a set of coefficients that can represent the original function
Simplification in Practical Applications
In practical implementations, the factor of 2 in the decomposition of even and odd terms is often omitted, yielding an FFT result scaled against the basis. This simplification reduces the number of multiplication and addition operations, thereby enhancing the algorithm's efficiency.
Circle FFT is similar to traditional FFT but operates on what are known as Riemann-Roch spaces. This mathematical concept means that the polynomials processed in Circle FFT are those modulo the unit circle defined by the equation
In Circle FFT, the coefficients of the polynomials are not typical monomials (e.g.,
Initially, the polynomial space
Here, the ideal
From an algebraic geometry perspective,
Proposition 2: Twin Cosets and Standard Position Cosets highlights two important properties of
-
Rotation Invariance: For any
$f \in L_N(F)$ and$P \in C(F_p)$ ,$f \circ T_P \in L_N(F)$ . -
Dimension:
$L_N(F) = N + 1$ , with any non-trivial$f \in L_N(F)$ having no more than$N$ roots on$C(F)$ .
Through definitions and proofs, a basis for
Subsequently, Circle Codes are a type of algebraic geometry code, akin to generalized Reed-Solomon codes, generated from the linear codes of polynomial space
The primary application of Circle FFT is to perform low-degree extensions, which means generating
The definition introduces a special basis known as
Circle FFT is not the only type of "heterogeneous" FFT. Elliptic Curve FFT (ECFFT) is another, more complex, and powerful variant capable of functioning over arbitrary finite fields (such as prime fields and binary fields). However, the implementation of ECFFT is more intricate and less efficient. Therefore, in specific scenarios (like when using
Similar to ECFFT, Circle FFT is a non-harmonic FFT. The group structure of the circle curve plays a central role in its construction, relying on two crucial self-maps: the group squaring map
Code
Python
# Converts a list of evaluations to a list of coefficients. Note that the
# coefficients are in a "weird" basis: 1, y, x, xy, 2x^2-1...
def fft(vals, is_top_level=True):
vals = vals.copy()
shape_suffix = vals.shape[1:]
size = vals.shape[0]
for i in range(log2(size)):
vals = vals.reshape((1 << i, size >> i) + shape_suffix)
full_len = vals.shape[1]
half_len = full_len >> 1
L = vals[:, :half_len]
R = vals[:, half_len:][:, ::-1, ...] # flip along axis 1
f0 = L + R
if i==0 and is_top_level:
twiddle = invy[full_len: full_len + half_len]
else:
twiddle = invx[full_len*2: full_len*2 + half_len]
twiddle_box = twiddle.reshape((1, half_len) + (1,) * (L.ndim - 2))
f1 = (L - R) * twiddle_box
vals[:, :half_len] = f0
vals[:, half_len:] = f1
return (
(vals.reshape((size,) + shape_suffix))[rbos[size:size*2]] / size
)
C++
#include "algebraLib/novelFFT.hpp"
#include "algebraLib/ErrorHandling.hpp"
#include <FFT.h>
#include <string.h>
namespace Algebra{
using std::vector;
namespace{
vector<SubspacePolynomial> calc_X_exp(const vector<FieldElement>& orderedBasis){
vector<SubspacePolynomial> X_exp;
{
for(unsigned int i=0; i<orderedBasis.size(); i++){
const elementsSet_t currBasis(orderedBasis.begin(),orderedBasis.begin()+i);
X_exp.push_back(SubspacePolynomial(currBasis));
SubspacePolynomial& currPoly = X_exp[X_exp.size()-1];
const FieldElement currElem = orderedBasis[i];
const FieldElement factor = one()/currPoly.eval(currElem);
currPoly.multiplyByConstant(factor);
}
}
...
for(size_t polyIdx = 0; polyIdx < numPolys; polyIdx++){
FieldElement* currPoly = &polysVals[polyIdx];
//handle case (18)
{
const size_t mc_case18 = m | ((c|1UL)<<currShift_c);
const size_t innerIdx = mc_case18 ^ oddMask_c;
currPoly[width*mc_case18] = currPoly[width*innerIdx] + currPoly[width*mc_case18];
}
//handle case (17)
{
const size_t mc_case17 = m | (c<<currShift_c);
const size_t prevIdx2 = mc_case17 ^ oddMask_c;
currPoly[width*mc_case17] = currPoly[width*mc_case17] + X_exp_precomp[c>>1] * currPoly[width*prevIdx2];
}
}
}
}
return polysVals;
}
vector<FieldElement> convertPolysBasis(const vector<FieldElement> orderedBasis, const vector<SubspacePolynomial> X_exp, vector<vector<FieldElement>>&& polysCoeffs, const size_t width, const FieldElement& pad_value){
ALGEBRALIB_ASSERT(width >= polysCoeffs.size(), "Width must be at least as the number of polys");
const size_t numPolys = polysCoeffs.size();
const size_t spaceSize = 1UL<<orderedBasis.size();
//evaluate using standard FFT on the space
vector<FieldElement> res(width * spaceSize, pad_value);
{
FFF::Element* basis_vec = (FFF::Element*)(&(orderedBasis[0]));
FFF::Element shift_fff(zero());
FFF::Basis basis(basis_vec, orderedBasis.size(),shift_fff);
FFF::FFT fftInstance(basis,FFF::FFT_OP);
#pragma omp parallel for
for(unsigned int i=0; i< numPolys; i++){
vector<FieldElement>& currCoeffs = polysCoeffs[i];
ALGEBRALIB_ASSERT(currCoeffs.size() <= spaceSize, "FFT is supported only for evaluation spaces of size at least as the polynomial degree");
if(currCoeffs.size() < spaceSize){
currCoeffs.resize(spaceSize,zero());
}
auto c_poly = (FFF::Element*)(&currCoeffs[0]);
fftInstance.AlgFFT(&c_poly,spaceSize);
...
novelFFT::novelFFT(const vector<FieldElement>& orderedBasis, vector<FieldElement>&& srcEval) :
novelFFT(orderedBasis, calc_X_exp(orderedBasis), std::move(srcEval)){};
novelFFT::novelFFT(const vector<FieldElement>& orderedBasis, vector<vector<FieldElement>>&& polysCoeffs, const size_t width, const FieldElement& pad_value) :
novelFFT(orderedBasis, calc_X_exp(orderedBasis), std::move(polysCoeffs), width, pad_value){};
void novelFFT::FFT(const vector<FieldElement>& affineShift, FieldElement* dst, const size_t diff_coset)const{
const unsigned short basisSize = orderedBasis_.size();
const size_t spaceSize = 1UL<<basisSize;
//copy coefficient to destination
{
const unsigned int max_threads_machine = omp_get_max_threads();
const size_t bufferBytesLen = polys_.size() * sizeof(FieldElement);
const size_t blockBytesLen = bufferBytesLen / max_threads_machine;
const size_t blockRemeinder = bufferBytesLen % max_threads_machine;
#pragma omp parallel for
for(long long blockIdx = 0; blockIdx < max_threads_machine; blockIdx++){
for(unsigned int cosetIdx = 0; cosetIdx < affineShift.size(); cosetIdx++){
memcpy((char*)(dst + (cosetIdx*diff_coset)) + (blockIdx*blockBytesLen), ((char*)&polys_[0]) + (blockIdx*blockBytesLen), blockBytesLen);
}
}
if(blockRemeinder > 0){
for(unsigned int cosetIdx = 0; cosetIdx < affineShift.size(); cosetIdx++){
memcpy((char*)(dst + (cosetIdx*diff_coset)) + (bufferBytesLen - blockRemeinder), (char*)(&polys_[0]) + (bufferBytesLen - blockRemeinder), blockRemeinder);
}
}
}
//execute the FFT
{
for (int i=basisSize-1; i >= 0; i--){
const unsigned short currShift_c = i;
const size_t currMask_m = (1UL<<currShift_c)-1;
const size_t oddMask_c = 1UL<<currShift_c;
const unsigned short len_c = basisSize-currShift_c;
const unsigned short logNumPrecompute = len_c-1;
const size_t numPrecompute = 1UL<<(logNumPrecompute);
const size_t numPrecomputeMask = numPrecompute-1;
const size_t numShifts = affineShift.size();
vector<vector<FieldElement>> X_exp_precomp(affineShift.size(),vector<FieldElement>(numPrecompute));
#pragma omp parallel for
for(unsigned long long expIdx=0; expIdx < (numShifts<<logNumPrecompute); expIdx++){
const size_t c = expIdx & numPrecomputeMask;
const size_t cosetId = expIdx >> logNumPrecompute;
const FieldElement c_elem = getSpaceElementByIndex(orderedBasis_,affineShift[cosetId],c<<(currShift_c+1));
X_exp_precomp[cosetId][c] = X_exp_[i].eval(c_elem);
}
...
const size_t mc_case17index = width_*mc_case17;
const size_t mc_case18index = width_*mc_case18;
const long long cs1 = c>>1;
for(size_t cosetId=0; cosetId < affineShift.size(); cosetId++){
const FieldElement Xpre = X_exp_precomp[cosetId][cs1];
const FFF::Element XpreElem = *(FFF::Element*)(&Xpre);
const size_t cosetOffset = diff_coset*cosetId;
FieldElement* baseVec = dst + cosetOffset;
FFF::Element* currVecFFF = (FFF::Element*)(baseVec);
//
// Irreducible specific implementation
//
FFF::Element::do_FFT_step(XpreElem, &currVecFFF[mc_case18index], &currVecFFF[mc_case17index],numPolys_);
/*
* General code - less field specific optimizations
*/
//const size_t ub = polys_.size() * diff_poly_fixed;
//for(size_t polyOffset=0; polyOffset < ub; polyOffset+=(diff_poly_fixed*2)){
//FieldElement* currVec = baseVec + polyOffset;
//handle case (17)
//currVec[mc_case17index] = currVec[mc_case17index] + Xpre * currVec[mc_case18index];
//handle case (18)
//currVec[mc_case18index] = currVec[mc_case17index] + currVec[mc_case18index];
//}
}
}
}
}
}
}
} // namespace Algebra
In STARK protocols, the operation of quotienting plays a crucial role in demonstrating the value of a polynomial at specific points. To prove that a polynomial
For instance, if we have a polynomial
In essence, every quotient
where
Given
This construction guarantees the existence of a vanishing polynomial and proves its uniqueness under degree constraints in the polynomial space
To simplify and optimize the computation of vanishing polynomials, we focus on those defined in the FFT domain, particularly the standard position co-sets and dual co-sets. In this context, the construction of the vanishing polynomial can be executed quickly with low computational complexity concerning field operations.
In STARK proofs, the polynomial equation to be verified is akin to
This illustrates that the vanishing polynomial emerges from the folding function. In conventional STARKs, the folding function takes the form
Moreover, the introduction of the quotient polynomial
These vanishing and quotient polynomials are integral to the STARK architecture, particularly in the algebraic linking steps of the proof structure (DEEP), ensuring the correctness and symmetry of polynomial verifications.
In STARK protocols, evaluating polynomials is conducted in a "bit-reversed order" rather than a natural order (e.g.,
In Circle STARKs, the folding structure is slightly different. In the initial step, we pair the points
Code
Python
# Generate a STARK proof for a given claim
#
# check_constraint(state, next_state, constraints, is_extended)
#
# Verifies that the constraint is satisfied at two adjacent rows of the trace.
# Must be degree <= H_degree+1
#
# trace: the computation trace
#
# constants: Constants that check_constraint has access to. This typically
# includes opcodes and other constaints that differ row-by-row
#
# public_args: the rows of the trace that are revealed publicly
#
# prebuilt_constants_tree: The constants only need to be put into a Merkle tree
# once, so if you already did it, you can reuse the
# tree.
#
# H-degree: this plus one is the max degree of check_constraint. Must be a
# power of two
def mk_stark(check_constraint,
trace,
constants,
public_args,
prebuilt_constants_tree=None,
H_degree=2):
import time
rounds, constants_width = constants.shape[:2]
trace_length = trace.shape[0]
trace_width = trace.shape[1]
print('Trace length: {}'.format(trace_length))
START = time.time()
constants = pad_to(constants, trace_length)
# Trace must satisfy
# C(T(x), T(x+G), K(x), A(x)) = Z(x) * H(x)
# Note that we multiply L[F1, F2] into C, to ensure that C does not
# have to satisfy at the last coordinate in the set
# We use the last row of the trace to make its degree N-1 rather than N.
# This keeps deg(H) later on comfortably less than N*H_degree-1, which
# makes it more efficient to bary-evaluate
trace = tweak_last_row(trace)
# The larger domain on which we run our polynomial math
ext_degree = H_degree * 2
ext_domain = sub_domains[
trace_length*ext_degree:
trace_length*ext_degree*2
]
trace_ext = inv_fft(pad_to(fft(trace), trace_length*ext_degree))
print('Generated trace extension', time.time() - START)
# Decompose the trace into the public part and the private part:
# trace = public * V + private. We commit to the private part, and show
# the public part in the clear
V, I = public_args_to_vanish_and_interp(
trace_length,
public_args,
trace[cp.array(public_args)],
)
V_ext = inv_fft(pad_to(fft(V), trace_length*ext_degree))
I_ext = inv_fft(pad_to(fft(I), trace_length*ext_degree))
print('Generated V,I', time.time() - START)
trace_quotient_ext = (
(trace_ext - I_ext) / V_ext.reshape(V_ext.shape+(1,))
)
constants_ext = inv_fft(pad_to(fft(constants), trace_length*ext_degree))
rolled_trace_ext = M31.append(
trace_ext[ext_degree:],
trace_ext[:ext_degree]
)
# Zero on the last two columns of the trace. We multiply this into C
# to make it zero across the entire trace
# We Merkelize the trace quotient (CPU-dominant) and compute C
# (GPU-dominant) in parallel
def f1():
return merkelize_top_dimension(trace_quotient_ext)
def f2():
return compute_H(
ext_domain,
trace_ext,
rolled_trace_ext,
constants_ext,
check_constraint,
trace_length,
)
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as executor:
# Submit the tasks to the executor
future1 = executor.submit(f1)
future2 = executor.submit(f2)
# Get the results
TQ_tree = future1.result()
H_ext = future2.result()
#TQ_tree = f1()
#H_ext = f2()
print('Generated tree and C_ext!', time.time() - START)
#H_coeffs = fft(H_ext)
#print(cp.where(H_coeffs[trace_length * H_degree:].value % modulus != 0), H_ext.shape)
#assert confirm_max_degree(H_coeffs, trace_length * H_degree)
output_width = H_ext.shape[1]
if prebuilt_constants_tree is not None:
K_tree = prebuilt_constants_tree
else:
K_tree = merkelize_top_dimension(constants_ext)
# Now, we generate a random point w, at which we evaluate our polynomials
G = sub_domains[trace_length//2]
w = projective_to_point(
ExtendedM31(get_challenges(TQ_tree[1], modulus, 4))
)
w_plus_G = w + G
# Trace quotient, at w and w+G
TQ_bary = (bary_eval(trace, w) - bary_eval(I, w)) / bary_eval(V, w)
TQ_bary2 = (
(bary_eval(trace, w_plus_G) - bary_eval(I, w_plus_G))
/ bary_eval(V, w_plus_G)
)
# Constants, at w and w+G
K_bary = bary_eval(constants, w)
K_bary2 = bary_eval(constants, w_plus_G)
# H, at w and w+G. We _could_ also compute it with compute_H, but
# somehow a bary-evaluation is faster (!!) than calling the function
bump = sub_domains[trace_length*ext_degree].to_extended()
w_bump = w + bump
wpG_bump = w_plus_G + bump
H_ef = H_ext[::2]
H_bary = bary_eval(H_ef, w_bump)
H_bary2 = bary_eval(H_ef, wpG_bump)
stack_ext = M31.append(
trace_quotient_ext,
constants_ext,
H_ext,
axis=1
)
stack_width = trace_width + constants_width + output_width
S_at_w = ExtendedM31.append(TQ_bary, K_bary, H_bary)
S_at_w_plus_G = ExtendedM31.append(TQ_bary2, K_bary2, H_bary2)
# Compute a random linear combination of everything in stack_ext4, using
# S_at_w and S_at_w_plus_G as entropy
print("Computed evals at w and w+G", time.time() - START)
entropy = TQ_tree[1] + K_tree[1] + S_at_w.tobytes() + S_at_w_plus_G.tobytes()
fold_factors = ExtendedM31(
get_challenges(entropy, modulus, stack_width * 4)
.reshape((stack_width, 4))
)
#assert eq(
# bary_eval(stack_ext, w, True, True),
# S_at_w
#)
merged_poly = fold(stack_ext, fold_factors)
#merged_poly_coeffs = fft(merged_poly)
#assert confirm_max_degree(merged_poly_coeffs, trace_length * H_degree)
print('Generated merged poly!', time.time() - START)
# Do the quotient trick, to prove that the evaluation we gave is
# correct. Namely, prove: (random_linear_combination(S) - I) / L is a
# polynomial, where L is 0 at w w+G, and I is S_at_w and S_at_w_plus_G
L3 = line_function(w, w_plus_G, ext_domain)
I3 = interpolant(
w,
fold(S_at_w, fold_factors),
w_plus_G,
fold(S_at_w_plus_G, fold_factors),
ext_domain
)
#assert eq(
# fold_ext(S_at_w, fold_factors),
# bary_eval(merged_poly, w, True)
#)
master_quotient = (merged_poly - I3) / L3
print('Generated master_quotient!', time.time() - START)
#master_quotient_coeffs = fft(master_quotient)
#assert confirm_max_degree(master_quotient_coeffs, trace_length * H_degree)
# Generate a FRI proof of (random_linear_combination(S) - I) / L
fri_proof = prove_low_degree(master_quotient, extra_entropy=entropy)
fri_entropy = (
entropy +
b''.join(fri_proof["roots"]) +
fri_proof["final_values"].tobytes()
)
challenges_raw = get_challenges(
fri_entropy, trace_length*ext_degree, NUM_CHALLENGES
)
fri_top_leaf_count = trace_length*ext_degree >> FOLDS_PER_ROUND
challenges_top = challenges_raw % fri_top_leaf_count
challenges_bottom = challenges_raw >> log2(fri_top_leaf_count)
challenges = rbo_index_to_original(
trace_length*ext_degree,
challenges_top * FOLD_SIZE_RATIO + challenges_bottom
)
challenges_next = (challenges+ext_degree) % (trace_length*ext_degree)
return {
"fri": fri_proof,
"TQ_root": TQ_tree[1],
"TQ_branches": [get_branch(TQ_tree, c) for c in challenges],
"TQ_leaves": trace_quotient_ext[challenges],
"TQ_next_branches": [get_branch(TQ_tree, c) for c in challenges_next],
"TQ_next_leaves": trace_quotient_ext[challenges_next],
"K_root": K_tree[1],
"K_branches": [get_branch(K_tree, c) for c in challenges],
"K_leaves": constants_ext[challenges],
"S_at_w": S_at_w,
"S_at_w_plus_G": S_at_w_plus_G,
}
C++
while (!verifier.doneInteracting()) {
std::cout << "communication iteration #" << msgNum++ << ":";
bool doStatusLoop = true;
Timer roundTimer;
std::thread barManager([&]() {
unsigned int sleepInterval = 10;
unsigned int sleepTime = 10;
while (doStatusLoop) {
std::cout << "." << std::flush;
for (unsigned int i = 0; (i < sleepTime) && doStatusLoop; i++) {
std::this_thread::sleep_for(
std::chrono::milliseconds(sleepInterval));
}
sleepTime *= 2;
}
});
startVerifier();
const auto vMsg = verifier.sendMessage();
verifierTime += t.getElapsed();
startProver();
t.restart();
const auto pMsg = prover.receiveMessage(vMsg);
proverTime += t.getElapsed();
startVerifier();
t.restart();
verifier.receiveMessage(pMsg);
verifierTime += t.getElapsed();
doStatusLoop = false;
barManager.join();
std::cout << "(" << roundTimer.getElapsed() << " seconds)" << std::endl;
}
...
void printSpecs(const double proverTime, const double verifierTime,
const size_t proofGeneratedBytes, const size_t proofSentBytes,
const size_t queriedDataBytes) {
startSpecs();
specsPrinter specs("Protocol execution measurements");
specs.addLine("Prover time", secondsToString(proverTime));
specs.addLine("Verifier time", secondsToString(verifierTime));
specs.addLine("Total IOP length", numBytesToString(proofGeneratedBytes));
specs.addLine("Total communication complexity (STARK proof size)",
numBytesToString(proofSentBytes));
specs.addLine("Query complexity", numBytesToString(queriedDataBytes));
specs.print();
resetColor();
}
...
bool executeProtocol(const BairInstance& instance, const BairWitness& witness,
const unsigned short securityParameter, bool testBair,
bool testAcsp, bool testPCP) {
const bool noWitness = !(testBair || testAcsp || testPCP);
prn::printBairInstanceSpec(instance);
unique_ptr<AcspInstance> acspInstance = CBairToAcsp::reduceInstance(
instance,
vector<FieldElement>(instance.constraintsPermutation().numMappings(),
one()),
vector<FieldElement>(instance.constraintsAssignment().numMappings(),
one()));
prn::printAcspInstanceSpec(*acspInstance);
prn::printAprInstanceSpec(*acspInstance);
...
}
void simulateProtocol(const BairInstance& instance,
const unsigned short securityParameter) {
BairWitness* witness_dummy = nullptr;
Protocols::executeProtocol(instance, *witness_dummy, securityParameter,
false, false, false);
}
Circle STARKs enhance zero-knowledge proofs by efficiently handling polynomials in geometric structures through innovative techniques like quotienting and two-point proving. Their use of interpolation functions and specialized vanishing polynomials improves computational efficiency and verification accuracy. Furthermore, the adoption of bit-reversed order in polynomial evaluation optimizes the FRI process, allowing for effective value pairing and increased spatial efficiency. Overall, Circle STARKs significantly advance the security and applicability of zero-knowledge proofs in decentralized systems and privacy-preserving technologies.