title | tags | ||||
---|---|---|---|---|---|
41. LDE & Constraints |
|
Authors: Eta, looking forward to your joining
Low Degree Extension (LDE) is a technique used in cryptographic protocols to efficiently represent and manipulate data. It involves transforming a sequence of data points into a polynomial and then evaluating this polynomial over a larger domain. This process is akin to fitting a curve to a set of points and then extending that curve beyond the original data points.
Key Steps in LDE:
-
Generate Inputs: Start with a sequence of inputs
$y_0, y_1, y_2, ...$ (e.g., a Fibonacci sequence$a_0, a_1, a_2, ..., a_{1022}$ ). To make calculations easier, choose another sequence$x_0, x_1, x_2, ...$ (e.g.,$g^0, g^1, g^2 ..., g^{1022}$ ), and pair$x$ and$y$ accordingly (like pairing$a$ with$g$ ). -
Interpolation: Interpolate these values into a polynomial
$f(x_i) = y_i$ (e.g.,$f(g^i) = a_i$ ). -
Extension: LDE expands the domain of a vector by interpolating its values into a polynomial, similar to creating a Reed-Solomon error correction code by extending polynomial evaluations over a larger field. Starting with a set of data points (red dots), a polynomial is fitted to pass through them. Extending this polynomial to a larger domain (e.g., from K to 8K) generates new data points (green dots), representing the extended vector. Interpolation is key in LDE, enabling the transition from a smaller to a larger domain, allowing for vector evaluation over a much-expanded space.
Reed-Solomon Code
Reed-Solomon codes are a cornerstone of coding theory, a field focused on efficiently encoding information. The encoding process maps a message of length
The code rate
A larger Hamming distance enables more efficient error detection and correction for Reed-Solomon codes, reducing the number of checks required to verify an execution trace. However, excessively increasing the distance can negatively impact proof efficiency and security by imposing a heavier computational burden on the prover.
Coset: Expanding the Domain and Preventing Collisions
When transitioning from a smaller to a larger domain (often referred to as "blowup"), it's crucial to avoid simple expansion. Instead, a specific subset or coset within the larger domain is chosen to mitigate the risk of collisions and redundant data. This is essential for maintaining security and efficiency.
For instance, in the PLONK protocol, carefully selected vectors are used to optimize the placement of data points. To construct a specific domain, two elements,
Commit on LDE
In the second step, commitment is made where the leaves of the Merkle tree are the results of the computation on the larger domain
🌰 Example & Code
Fibonacci Sequence
Consider the FibonacciSq (Fibonacci Square) sequence, where each element is the sum of the squares of the two preceding elements,
from field import FieldElement
a = [FieldElement(1), FieldElement(3141592)]
while len(a) < 1023:
a.append(a[-2] * a[-2] + a[-1] * a[-1])
Recall that
g = FieldElement.generator() ** (3 * 2 ** 20)
G = [g ** i for i in range(1024)]
polynomial module provides a Lagrange interpolation function by interpolate_poly(x_values, y_values) function, who returns the unique Polynomial of degree < len(x_values) instance that evaluates to y_values[i] on x_values[i] for all i.
from polynomial import interpolate_poly
f = interpolate_poly(G[:-1], a)
Evaluating on a Larger Domain
To extend the evaluation of the polynomial f from its trace on the domain G to a larger domain and thereby create a Reed-Solomon error correction code, we choose a new domain that is 8 times larger than G. This larger domain is constructed by selecting a group H of size 8192 (which is possible because 8192 divides the order of the multiplicative group eval_domain
, represented as
w = FieldElement.generator()
h = w ** ((2 ** 30 * 3) // 8192)
H = [h ** i for i in range(8192)]
eval_domain = [w * x for x in H]
Time to use interpolate_poly and Polynomial.poly to evaluate over the coset - similarly to the way we did a few minutes ago.
f = interpolate_poly(G[:-1], a)
f_eval = [f(d) for d in eval_domain]
Comments
We will use Sha256-based Merkle Trees as our commitment scheme.
from merkle import MerkleTree
f_merkle = MerkleTree(f_eval)
Channel
The Channel class in a STARK proof system replaces the verifier by transforming the interactive protocol into a non-interactive one using the Fiat-Shamir Heuristic. You'll use the Channel to send data, like the root of your Merkle Tree, and receive random numbers or FieldElement instances as if interacting with a verifier. Additionally, you can access the accumulated proof-so-far by printing the Channel.proof, which contains all the data passed through the channel up to that point.
from channel import Channel
channel = Channel()
channel.send(f_merkle.root)
print(channel.proof)
In a zero-knowledge proof, the prover aims to convince the verifier of their knowledge of a secret value
-
Polynomial Representation of Constraints: The initial step involves transforming constraints defined over the Fibonacci sequence into polynomial equations. This means expressing the sequence’s properties in terms of polynomial relationships.
-
Roots as Proof Verifiers: A polynomial’s roots are values that produce zero when substituted into the polynomial. If a specific set of values (e.g.,
$g^0, g^1, g^2, ..., g^{1022}$ ) are roots of a constructed polynomial, it validates the original constraints. -
Interpolation and Rational Functions: To demonstrate that a value (g) is a root of a polynomial
$f(x)$ , it's necessary to prove that dividing$f(x)$ by$(x-g)$ yields another polynomial$\frac{f(x)}{x - g}$ . In essence, if g is a root of the dividend polynomial and$x-g$ is a divisor, then$g$ must also be a root of the quotient polynomial. This process involves converting the identified roots into rational functions that represent polynomials if and only if the initial constraints are satisfied.
To simplify our proof, we combine the three polynomials
🌰 Example & Code
Constraints
The first constraint involves ensuring that f(x) - 1 is divisible by (x - 1) to construct the polynomial
numer0 = f - 1
denom0 = X - 1
p0 = numer0 / denom0
Construct the polynomial p1
representing the second constraint,
numer1 = f - 2338775057
denom1 = X - g**1022
p1 = numer1 / denom1
Construct the third constraint p2
in a similar manner to the construction of p0
and p1
, using polynomial composition. Along the way, verify that
numer2 = f(g**2 * X) - f(g * X)**2 - f**2
print("Numerator at g^1020 is", numer2(g**1020))
print("Numerator at g^1021 is", numer2(g**1021))
denom2 = (X**1024 - 1) / ((X - g**1021) * (X - g**1022) * (X - g**1023))
p2 = numer2 / denom2
To simplify the proof, we create a composition polynomial
def get_CP(channel):
alpha0 = channel.receive_random_field_element()
alpha1 = channel.receive_random_field_element()
alpha2 = channel.receive_random_field_element()
return alpha0*p0 + alpha1*p1 + alpha2*p2
Commit on the Composition Polynomial
Finally, evaluate the composition polynomial over the eval_domain
, build a Merkle tree from the results, and send the root over the channel, just like the LDE trace commitment in part 1.
def CP_eval(channel):
CP = get_CP(channel)
return [CP(d) for d in eval_domain]
channel = Channel()
CP_merkle = MerkleTree(CP_eval(channel))
channel.send(CP_merkle.root)
🐿️ Source
from channel import Channel
from field import FieldElement
from merkle import MerkleTree
from polynomial import interpolate_poly, Polynomial
def part1():
t = [FieldElement(1), FieldElement(3141592)]
while len(t) < 1023:
t.append(t[-2] * t[-2] + t[-1] * t[-1])
g = FieldElement.generator() ** (3 * 2 ** 20)
points = [g ** i for i in range(1024)]
h_gen = FieldElement.generator() ** ((2 ** 30 * 3) // 8192)
h = [h_gen ** i for i in range(8192)]
domain = [FieldElement.generator() * x for x in h]
p = interpolate_poly(points[:-1], t)
ev = [p.eval(d) for d in domain]
mt = MerkleTree(ev)
ch = Channel()
ch.send(mt.root)
return t, g, points, h_gen, h, domain, p, ev, mt, ch
def part2():
t, g, points, h_gen, h, domain, p, ev, mt, ch = part1()
numer0 = p - Polynomial([FieldElement(1)])
denom0 = Polynomial.gen_linear_term(FieldElement(1))
q0, r0 = numer0.qdiv(denom0)
numer1 = p - Polynomial([FieldElement(2338775057)])
denom1 = Polynomial.gen_linear_term(points[1022])
q1, r1 = numer1.qdiv(denom1)
inner_poly0 = Polynomial([FieldElement(0), points[2]])
final0 = p.compose(inner_poly0)
inner_poly1 = Polynomial([FieldElement(0), points[1]])
composition = p.compose(inner_poly1)
final1 = composition * composition
final2 = p * p
numer2 = final0 - final1 - final2
coef = [FieldElement(1)] + [FieldElement(0)] * 1023 + [FieldElement(-1)]
numerator_of_denom2 = Polynomial(coef)
factor0 = Polynomial.gen_linear_term(points[1021])
factor1 = Polynomial.gen_linear_term(points[1022])
factor2 = Polynomial.gen_linear_term(points[1023])
denom_of_denom2 = factor0 * factor1 * factor2
denom2, r_denom2 = numerator_of_denom2.qdiv(denom_of_denom2)
q2, r2 = numer2.qdiv(denom2)
cp0 = q0.scalar_mul(ch.receive_random_field_element())
cp1 = q1.scalar_mul(ch.receive_random_field_element())
cp2 = q2.scalar_mul(ch.receive_random_field_element())
cp = cp0 + cp1 + cp2
cp_ev = [cp.eval(d) for d in domain]
cp_mt = MerkleTree(cp_ev)
ch.send(cp_mt.root)
return cp, cp_ev, cp_mt, ch, domain