Proving Single Polynomial
Before dealing with all of \(\ell(x)\), \(r(x)\), and \(o(x)\) at once, we design a protocol that allows the Prover \(\mathcal{A}\) to convince the Verifier \(\mathcal{B}\) that \(\mathcal{A}\) knows a specific polynomial. Let's denote this polynomial of degree \(n\) with coefficients in a finite field as:
\begin{equation} P(x) = c_0 + c_1 x + c_2 x^{2} + \cdots c_n x^{n} \end{equation}
Assume \(P(x)\) has \(n\) roots, \(a_1, a_2, \ldots, a_n \in \mathbb{F}\), such that \(P(x) = (x - a_1)(x - a_2)\cdots(x - a_n)\). The Verifier \(\mathcal{B}\) knows \(m < n\) roots of \(P(x)\), namely \(a_1, a_2, \ldots, a_m\). Let \(T(x) = (x - a_1)(x - a_2)\cdots(x - a_m)\). Note that the Prover also knows \(T(x)\).
The Prover's objective is to convince the Verifier that \(\mathcal{A}\) knows a polynomial \(H(x) = \frac{P(x)}{T(x)}\).
First Protocol: Naive Approach
The simplest approach to prove that \(\mathcal{A}\) knows \(H(x)\) is as follows:
Protocol:
- \(\mathcal{B}\) sends all possible values in \(\mathbb{F}\) to \(\mathcal{A}\).
- \(\mathcal{A}\) computes and sends all possible outputs of \(H(x)\) and \(P(x)\).
- \(\mathcal{B}\) checks whether \(H(a)T(a) = P(a)\) holds for any \(a\) in \(\mathbb{F}\).
This protocol is highly inefficient, requiring \(\mathcal{O}(|\mathbb{F}|)\) evaluations and communications.
Implementation:
#![allow(unused)] fn main() { pub struct Prover1<F: Field> { pub p: Polynomial<F>, pub t: Polynomial<F>, pub h: Polynomial<F>, } pub struct Verifier1<F: Field> { pub t: Polynomial<F>, pub known_roots: Vec<F>, } impl<F: Field> Prover1<F> { pub fn new(p: Polynomial<F>, t: Polynomial<F>) -> Self { let h = p.clone() / t.clone(); Prover1 { p, t, h } } pub fn compute_all_values(&self, modulus: i128) -> (HashMap<F, F>, HashMap<F, F>) { let mut h_values = HashMap::new(); let mut p_values = HashMap::new(); for i in 0..modulus { let x = F::from_value(i); h_values.insert(x.clone(), self.h.eval(&x)); p_values.insert(x.clone(), self.p.eval(&x)); } (h_values, p_values) } } impl<F: Field> Verifier1<F> { pub fn new(known_roots: Vec<F>) -> Self { let t = Polynomial::from_monomials(&known_roots); Verifier1 { t, known_roots } } pub fn verify(&self, h_values: &HashMap<F, F>, p_values: &HashMap<F, F>) -> bool { for (x, h_x) in h_values { let t_x = self.t.eval(x); let p_x = p_values.get(x).unwrap(); if h_x.clone() * t_x != *p_x { return false; } } true } } pub fn naive_protocol<F: Field>( prover: &Prover1<F>, verifier: &Verifier1<F>, modulus: i128, ) -> bool { // Step 1: Verifier1 sends all possible values (implicitly done by Prover1 computing all values) // Step 2: Prover1 computes and sends all possible outputs let (h_values, p_values) = prover.compute_all_values(modulus); // Step 3: Verifier1 checks whether H(a)T(a) = P(a) holds for any a in F verifier.verify(&h_values, &p_values) } }
Second Protocol: Schwartz-Zippel Lemma
Instead of evaluating polynomials at all values in \(\mathbb{F}\), we can leverage the Schwartz-Zippel Lemma: if \(H(s) = \frac{P(s)}{T(s)}\) or equivalently \(H(s)T(s) = P(s)\) for a random element \(s\), we can conclude that \(H(x) = \frac{P(x)}{T(x)}\) with high probability. Thus, the Prover \(\mathcal{A}\) only needs to send evaluations of \(P(s)\) and \(H(s)\) for a random input \(s\) received from \(\mathcal{B}\).
Protocol:
- \(\mathcal{B}\) draws random \(s\) from \(\mathbb{F}\) and sends it to \(\mathcal{A}\).
- \(\mathcal{A}\) computes \(h = H(s)\) and \(p = P(s)\) and send them to \(\mathcal{B}\).
- \(\mathcal{B}\) checks whether \(p = t h\), where \(t\) denotes \(T(s)\).
This protocol is efficient, requiring only a constant number of evaluations and communications.
Implementation:
#![allow(unused)] fn main() { pub struct Prover2<F: Field> { pub p: Polynomial<F>, pub t: Polynomial<F>, pub h: Polynomial<F>, } pub struct Verifier2<F: Field> { pub t: Polynomial<F>, } impl<F: Field> Prover2<F> { pub fn new(p: Polynomial<F>, t: Polynomial<F>) -> Self { let h = p.clone() / t.clone(); Prover2 { p, t, h } } pub fn compute_values(&self, s: &F) -> (F, F) { let h_s = self.h.eval(s); let p_s = self.p.eval(s); (h_s, p_s) } } impl<F: Field> Verifier2<F> { pub fn new(t: Polynomial<F>) -> Self { Verifier2 { t } } pub fn generate_challenge(&self) -> F { F::random_element(&[]) } pub fn verify(&self, s: &F, h: &F, p: &F) -> bool { let t_s = self.t.eval(s); h.clone() * t_s == *p } } pub fn schwartz_zippel_protocol<F: Field>(prover: &Prover2<F>, verifier: &Verifier2<F>) -> bool { // Step 1: Verifier2 generates a random challenge let s = verifier.generate_challenge(); // Step 2: Prover2 computes and sends h and p let (h, p) = prover.compute_values(&s); // Step 3: Verifier2 checks whether p = t * h verifier.verify(&s, &h, &p) } }
Vulnerability:
However, it is vulnerable to a malicious prover who could send an arbitrary value \(h'\) and the corresponding \(p'\) such that \(p' = h't\).
#![allow(unused)] fn main() { // Simulating a malicious prover pub struct MaliciousProver2<F: Field> { t: Polynomial<F>, } impl<F: Field> MaliciousProver2<F> { pub fn new(t: Polynomial<F>) -> Self { MaliciousProver2 { t } } pub fn compute_malicious_values(&self, s: &F) -> (F, F) { let h_prime = F::random_element(&[]); let t_s = self.t.eval(s); let p_prime = h_prime.clone() * t_s; (h_prime, p_prime) } } pub fn malicious_schwartz_zippel_protocol<F: Field>( prover: &MaliciousProver2<F>, verifier: &Verifier2<F>, ) -> bool { // Step 1: Verifier2 generates a random challenge let s = verifier.generate_challenge(); // Step 2: Malicious Prover2 computes and sends h' and p' let (h_prime, p_prime) = prover.compute_malicious_values(&s); // Step 3: Verifier2 checks whether p' = t * h' verifier.verify(&s, &h_prime, &p_prime) } }
Third Protocol: Discrete Logarithm Assumption
To address this vulnerability, the Verifier must hide the randomly chosen input \(s\) from the Prover. This can be achieved using the discrete logarithm assumption: it is computationally hard to determine \(s\) from \(\gamma\), where \(\gamma = g^s \bmod q\). Thus, it's safe for the Verifier to send \(\gamma\), as the Prover cannot easily derive \(s\) from it.
An interesting property of polynomial exponentiation is:
\begin{align} g^{P(x)} &= g^{c_0 + c_1 x + c_2 x^{2} + \cdots c_n x^{n}} = g^{c_0} (g^{x})^{c_1} (g^{(x^2)})^{c_2} \cdots (g^{(x^n)})^{c_n} \end{align}
Instead of sending \(s\), the Verifier can send \(g\) and \(\gamma_{i} = g^{(s^i)}\) for \(i = 1, \cdots n\). BE CAREFUL THAT \(g^{(s^i)} \neq (g^s)^i\). The Prover can still evaluate \(g^p = g^{P(s)}\) using these powers of \(g\):
\begin{equation} g^{p} = g^{P(s)} = g^{c_0} \gamma_{1}^{c_1} \gamma_{2}^{c_2} \cdots \gamma_{n}^{c_n} \end{equation}
Similarly, the Prover can evaluate \(g^h = g^{H(s)}\). The Verifier can then check \(p = ht \iff g^p = (g^h)^t\).
Protocol:
- \(\mathcal{B}\) randomly draw \(s\) from \(\mathbb{F}\).
- \(\mathcal{B}\) computes and sends \(\{\gamma_1, \gamma_2, ..., \gamma_{n}\}\), where \(\gamma_i= g^{(s^{i})}\).
- \(\mathcal{A}\) computes and sends \(u = g^{p}\) and \(v = g^{h}\).
- \(\mathcal{B}\) checks whether \(u = v^{t}\).
This approach prevents the Prover from obtaining \(s\) or \(t = T(s)\), making it impossible to send fake \((h', p')\) such that \(p' = h't\).
Implementation:
Suppose we are working in a finite field (\mathbb{F}_q) derived from a prime number (q). It’s important to note that
\[g^{c_0} \cdot (g^{s^{1} \bmod q})^{c_1} \cdots (g^{s^{n} \bmod q})^{c_n} \bmod q \]
is not equal to
\[g^{c_0 + c_1 s^{1} + c_2 s^{2} + \cdots c_n s^{n}} \bmod q\]
However, directly calculating \(s^i\) without taking modulo results in too large values to handle. To address this, we leverage Fermat's Little Theorem, which states that for a prime \(q\):
\[a^{b \bmod q - 1} \bmod q\]
Following this principle, our implementation computes \(s^i\) modulo \(q - 1\) to keep the values manageable.
#![allow(unused)] fn main() { pub struct Prover3<F: Field> { pub p: Polynomial<F>, pub t: Polynomial<F>, pub h: Polynomial<F>, } pub struct Verifier3<F: Field> { t: Polynomial<F>, s: F, g: F, } impl<F: Field> Prover3<F> { pub fn new(p: Polynomial<F>, t: Polynomial<F>) -> Self { let h = p.clone() / t.clone(); Prover3 { p, t, h } } pub fn compute_values(&self, s_powers: &[F]) -> (F, F) { let g_p = self.p.eval_with_powers(s_powers); let g_h = self.h.eval_with_powers(s_powers); (g_p, g_h) } } impl<F: Field> Verifier3<F> { pub fn new(t: Polynomial<F>, generator: i128) -> Self { let s = F::random_element(&[]); let g = F::from_value(generator); Verifier3 { t, s, g } } pub fn generate_challenge(&self, max_degree: usize) -> Vec<F> { let mut s_powers = vec![]; for i in 0..(max_degree + 1) { s_powers.push( self.g .pow(self.s.clone().pow_m1(i.to_bigint().unwrap()).get_value()), ); } s_powers } pub fn verify(&self, u: &F, v: &F) -> bool { let t_s = self.t.eval_m1(&self.s); u == &v.pow(t_s.get_value()) } } pub fn discrete_log_protocol<F: Field>(prover: &Prover3<F>, verifier: &Verifier3<F>) -> bool { // Step 1 & 2: Verifier3 generates a challenge let max_degree = prover.p.degree(); let s_powers = verifier.generate_challenge(max_degree as usize); // Step 3: Prover3 computes and sends u = g^p and v = g^h let (u, v) = prover.compute_values(&s_powers); // Step 4: Verifier3 checks whether u = v^t verifier.verify(&u, &v) } }
Vulnerability:
However, this protocol still has a flaw. Since the Prover can compute \(g^t\) from \(\gamma _1, \cdots \gamma _m\), they could send fake values \(((g^{t})^{z}, g^{z})\) instead of \((g^p, g^h)\) for an arbitrary value \(z\). The verifier's check would still pass, and they could not detect this deception.
#![allow(unused)] fn main() { // Simulating a malicious prover pub struct MaliciousProver3<F: Field> { t: Polynomial<F>, } impl<F: Field> MaliciousProver3<F> { pub fn new(t: Polynomial<F>) -> Self { MaliciousProver3 { t } } pub fn compute_malicious_values(&self, s_powers: &[F]) -> (F, F) { let g_t = self.t.eval_with_powers(s_powers); let z = F::random_element(&[]); let g = &s_powers[0]; let fake_v = g.pow(z.get_value()); let fake_u = g_t.pow(z.get_value()); (fake_u, fake_v) } } pub fn malicious_discrete_log_protocol<F: Field>( prover: &MaliciousProver3<F>, verifier: &Verifier3<F>, ) -> bool { // Step 1 & 2: Verifier3 generates a challenge let max_degree = prover.t.degree() as usize; let s_powers = verifier.generate_challenge(max_degree as usize); // Step 3: Malicious Prover3 computes and sends fake u and v let (fake_u, fake_v) = prover.compute_malicious_values(&s_powers); // Step 4: Verifier3 checks whether u = v^t (which will pass for the fake values) verifier.verify(&fake_u, &fake_v) } }
Forth Protocol: Knowledge of Exponent Assumption
To address the vulnerability where the verifier \(\mathcal{B}\) cannot distinguish if \(v (= g^h)\) from the prover is a power of \(\gamma_i = g^{(s^i)}\), we can employ the Knowledge of Exponent Assumption. This approach involves the following steps:
- \(\mathcal{B}\) sends both \(\gamma_i\) and \(\gamma'_i = \gamma_i^r\) for a new random value \(r\).
- The prover returns \(a = (\gamma_i)^{c_i}\) and \(a' = (\gamma'_i)^{c_i}\) for \(i = 1, ..., n\).
- \(\mathcal{B}\) can conclude that \(a\) is a power of \(\gamma_i\) if \(a^r = a'\).
Based on this assumption, we can design an improved protocol:
Protocol:
- \(\mathcal{B}\) randomly selects \(s\) and \(r\) from field \(\mathbb{F}\).
- \(\mathcal{B}\) computes and sends \(\{\gamma_1, \gamma_2, ..., \gamma_{n}\}\) and \(\{\gamma'_1, \gamma'_2, ..., \gamma'_{n}\}\), where \(\gamma_i = g^{(s^i)}\) and \(\gamma' = \gamma_{r} = g^{(s^{i})r}\).
- \(\mathcal{A}\) computes and sends \(u = g^{p}\), \(v = g^{h}\), and \(w = g^{p'}\), where \(g^{p'} = g^{rP(s)}\).
- \(\mathcal{B}\) checks whether \(u^{r} = w\).
- \(\mathcal{B}\) checks whether \(u = v^{t}\).
The prover can compute \(g^{p'} = g^{rP(s)} = g^{c_0} (\gamma_{1})'^{c_1} (\gamma'_{2})^{c_2} \cdots (\gamma_{n}')^{c_n}\). This protocol now satisfies the properties of a SNARK: completeness, soundness, and efficiency.
Implementation:
#![allow(unused)] fn main() { pub struct Prover4<F: Field> { pub p: Polynomial<F>, pub t: Polynomial<F>, pub h: Polynomial<F>, } pub struct Verifier4<F: Field> { t: Polynomial<F>, s: F, r: F, g: F, } impl<F: Field> Prover4<F> { pub fn new(p: Polynomial<F>, t: Polynomial<F>) -> Self { let h = p.clone() / t.clone(); Prover4 { p, t, h } } pub fn compute_values(&self, s_powers: &[F], s_prime_powers: &[F]) -> (F, F, F) { let g_p = self.p.eval_with_powers(s_powers); let g_h = self.h.eval_with_powers(s_powers); let g_p_prime = self.p.eval_with_powers(s_prime_powers); (g_p, g_h, g_p_prime) } } impl<F: Field> Verifier4<F> { pub fn new(t: Polynomial<F>, generator: i128) -> Self { let s = F::random_element(&[]); let r = F::random_element(&[]); let g = F::from_value(generator); Verifier4 { t, s, r, g } } pub fn generate_challenge(&self, max_degree: usize) -> (Vec<F>, Vec<F>) { let mut s_powers = vec![]; let mut s_prime_powers = vec![]; for i in 0..(max_degree + 1) { s_powers.push( self.g .pow(self.s.clone().pow_m1(i.to_bigint().unwrap()).get_value()), ); s_prime_powers.push(s_powers.last().unwrap().pow(self.r.get_value())); } (s_powers, s_prime_powers) } pub fn verify(&self, u: &F, v: &F, w: &F) -> bool { let t_s = self.t.eval_m1(&self.s); let u_r = u.pow(self.r.clone().get_value()); // Check 1: u^r = w let check1 = u_r == *w; // Check 2: u = v^t let check2 = *u == v.pow(t_s.get_value()); check1 && check2 } } pub fn knowledge_of_exponent_protocol<F: Field>( prover: &Prover4<F>, verifier: &Verifier4<F>, ) -> bool { // Step 1 & 2: Verifier4 generates a challenge let max_degree = std::cmp::max(prover.p.degree(), prover.h.degree()) as usize; let (s_powers, s_prime_powers) = verifier.generate_challenge(max_degree + 1); // Step 3: Prover4 computes and sends u = g^p, v = g^h, and w = g^p' let (u, v, w) = prover.compute_values(&s_powers, &s_prime_powers); // Step 4 & 5: Verifier4 checks whether u^r = w and u = v^t verifier.verify(&u, &v, &w) } }
Fifth Protocol: Zero Knowledge
To transform the above protocol into a zk-SNARK, we need to ensure that the verifier cannot learn anything about \(P(x)\) from the prover's information. This is achieved by having the prover obfuscate all information with a random secret value \(\delta\):
Protocol:
- \(\mathcal{B}\) randomly selects \(s\) and \(r\) from field \(\mathbb{F}\).
- \(\mathcal{B}\) computes and sends \(\{\gamma_1, \gamma_2, ..., \gamma_{n}\}\) and \(\{\gamma_1', \gamma'_2, ..., \gamma'_{n}\}\), where \(\gamma_i = g^{(s^{i})}\) and \(\gamma_i' = \gamma_i^{r} = g^{(s^{i})r}\).
- \(\mathcal{A}\) randomly selects \(\delta\) from field \(\mathbb{F}\).
- \(\mathcal{A}\) computes and sends \(u' = (g^{p})^{\delta}\), \(v' = (g^{h})^{\delta}\), and \(w' = (g^{p'})^{\delta}\).
- \(\mathcal{B}\) checks whether \(u'^{r} = w'\).
- \(\mathcal{B}\) checks whether \(u' = v'^{t}\).
By introducing the random value \(\delta\), the verifier can no longer learn anything about \(p\), \(h\), or \(w\), thus achieving zero knowledge.
Implementation:
#![allow(unused)] fn main() { pub struct Prover5<F: Field> { pub p: Polynomial<F>, pub t: Polynomial<F>, pub h: Polynomial<F>, } pub struct Verifier5<F: Field> { t: Polynomial<F>, s: F, r: F, g: F, } impl<F: Field> Prover5<F> { pub fn new(p: Polynomial<F>, t: Polynomial<F>) -> Self { let h = p.clone() / t.clone(); Prover5 { p, t, h } } pub fn compute_values(&self, s_powers: &[F], s_prime_powers: &[F]) -> (F, F, F) { let delta = F::random_element(&[]); let g_p = self.p.eval_with_powers(s_powers); let g_h = self.h.eval_with_powers(s_powers); let g_p_prime = self.p.eval_with_powers(s_prime_powers); let u_prime = g_p.pow(delta.get_value()); let v_prime = g_h.pow(delta.get_value()); let w_prime = g_p_prime.pow(delta.get_value()); (u_prime, v_prime, w_prime) } } impl<F: Field> Verifier5<F> { pub fn new(t: Polynomial<F>, generator: i128) -> Self { let s = F::random_element(&[]); let r = F::random_element(&[]); let g = F::from_value(generator); Verifier5 { t, s, r, g } } pub fn generate_challenge(&self, max_degree: usize) -> (Vec<F>, Vec<F>) { let mut s_powers = vec![]; let mut s_prime_powers = vec![]; for i in 0..(max_degree + 1) { s_powers.push( self.g .pow(self.s.clone().pow_m1(i.to_bigint().unwrap()).get_value()), ); s_prime_powers.push(s_powers.last().unwrap().pow(self.r.get_value())); } (s_powers, s_prime_powers) } pub fn verify(&self, u_prime: &F, v_prime: &F, w_prime: &F) -> bool { let t_s = self.t.eval_m1(&self.s); let u_prime_r = u_prime.pow(self.r.clone().get_value()); // Check 1: u'^r = w' let check1 = u_prime_r == *w_prime; // Check 2: u' = v'^t let check2 = *u_prime == v_prime.pow(t_s.get_value()); check1 && check2 } } pub fn zk_protocol<F: Field>(prover: &Prover5<F>, verifier: &Verifier5<F>) -> bool { // Step 1 & 2: Verifier5 generates a challenge let max_degree = std::cmp::max(prover.p.degree(), prover.h.degree()) as usize; let (s_powers, s_prime_powers) = verifier.generate_challenge(max_degree + 1); // Step 3 & 4: Prover5 computes and sends u' = (g^p)^δ, v' = (g^h)^δ, and w' = (g^p')^δ let (u_prime, v_prime, w_prime) = prover.compute_values(&s_powers, &s_prime_powers); // Step 5 & 6: Verifier5 checks whether u'^r = w' and u' = v'^t verifier.verify(&u_prime, &v_prime, &w_prime) } }
Sixth Protocol: Non-interactivity
The previously described protocol requires each verifier to generate unique random values, which becomes inefficient when a prover needs to demonstrate knowledge to multiple verifiers. To address this, we aim to eliminate the interaction between the prover and verifier. One effective solution is the use of a trusted setup.
Specifically, we let a thrid trusted party generate the secret seeds, which neither the prover nor the verifier can access. However, this prevents the verifier \(\mathcal{B}\) from calculating \(u'^{r}\) to check \(u'^{r} = w'\). Instead, the verifier can use a paring with bilinear mapping; \(u'^{r} = w'\) is equivalent to \(e(u' = (g^{p})^{\delta}, g^{r}) = e(w'=(g^{p'})^{\delta}, g)\).
In this tutorial, we adopt the optimal ate pairing, which is an asynmetric pairing and uses two different cyclic groups derived from elliptic curves; \(e(g_1^a, g_2^b) = e(g_1^{ab}, g_2) = e(b_1, g_2^{ab})\), where \(g_1\) and \(g_2\) are the generators of two different cyclic groups.
Protocol (Trusted Setup):
- Secret Seed: A trusted third party generates the random values \(s\) and \(r\)
- Proof Key: Provided to the prover
- \(\{\gamma_1, \gamma_2, ..., \gamma_{n}\}\), where \(\gamma_{i} = g_1^{(s^i)}\)
- \(\{\gamma'_1, \gamma'_2, ..., \gamma'_{n}\}\), where \(\gamma_i' = g_1^{(s^{i})r}\)
- Verification Key: Distributed to verifiers
- \(g_2^r\)
- \(g_2^t := g_2^{T(s)}\)
- After distribution, the original secret seeds are securely destroyed.
Then, the non-interactive protocol consists of two main parts: proof generation and verification.
Protocol (Proof):
- \(\mathcal{A}\) receives the proof key
- \(\mathcal{A}\) randomly selects \(\delta\) from field \(\mathbb{F}\).
- \(\mathcal{A}\) broadcast the proof \(\pi = (u' = (g_1^{p})^{\delta}, v' = (g_1^{h})^{\delta}, w' = (g_1^{p'})^{\delta})\)
Protocol (Verification):
- \(\mathcal{B}\) receives the verification key.
- \(\mathcal{B}\) receives the proof \(\pi\).
- \(\mathcal{B}\) checks whether \(e(u', g_2^r) = e(w', g_2)\).
- \(\mathcal{B}\) checks whether \(e(u', g_2) = e (v', g_2^t)\).
Implementation:
This tutorial utilize elliptic curves called BN128
for pairing:
-
First Group:
- Curve: \(Y^2 = x^3 + 3\) over the finite field \(\mathbb{F}_{q}\), where \(q\) is defined as 21888242871839275222246405745257275088696311157297823662689037894645226208583.
- Generator \(\mathscr{g}_1 := (1, 2)\)
-
Second Group:
- Curve: \(Y^2 = x^3 + 3\) over the finite field \(\mathbb{F} _{q^2}\)
- Modulus polynomial for \(\mathbb{F} _{q^2}\): \(x^2 + 1\)
- Generator \(\mathscr{g}_2\) := (11559732032986387107991004021392285783925812861821192530917403151452391805634x + 10857046999023057135944570762232829481370756359578518086990519993285655852781, 4082367875863433681332203403145435568316851327593401208105741076214120093531x + 8495653923123431417604973247489272438418190587263600148770280649306958101930)
The orders of two groups are \(\omega := \) 21888242871839275222246405745257275088548364400416034343698204186575808495617. Thus, we evaluate the polynomials over the finite field \(\mathbb{F}_{\omega}\).
In the context of elliptic curves, the power of a generator corresponds to the multiplication of an elliptic curve point. This property is reflected in the ate pairing on BN128, which satisfies:
\[
e(a \mathscr{g}_1, b \mathscr{g}_2) = e(ab \mathscr{g}_1, \mathscr{g}_2) = e(\mathscr{g}_1, ab\mathscr{g}_2)
\]
#![allow(unused)] fn main() { pub struct ProofKey { alpha: Vec<G1Point>, alpha_prime: Vec<G1Point>, } pub struct VerificationKey { g_r: G2Point, g_t_s: G2Point, } pub struct Proof { u_prime: G1Point, v_prime: G1Point, w_prime: G1Point, } pub fn setup( g1: &G1Point, g2: &G2Point, t: &Polynomial<FqOrder>, n: usize, ) -> (ProofKey, VerificationKey) { let s = FqOrder::random_element(&[]); let r = FqOrder::random_element(&[]); let mut alpha = Vec::with_capacity(n); let mut alpha_prime = Vec::with_capacity(n); let mut s_power = FqOrder::one(); for _ in 0..1 + n { alpha.push(g1.mul_ref(s_power.clone().get_value())); alpha_prime.push(g1.mul_ref((s_power.clone() * r.clone()).get_value())); s_power = s_power * s.clone(); } let g_r = g2.mul_ref(r.clone().get_value()); let g_t_s = g2.mul_ref(t.eval(&s).get_value()); ( ProofKey { alpha, alpha_prime }, VerificationKey { g_r, g_t_s }, ) } pub fn prove(p: &Polynomial<FqOrder>, t: &Polynomial<FqOrder>, proof_key: &ProofKey) -> Proof { let h = p.clone() / t.clone(); let delta = FqOrder::random_element(&[]); let g_p = p.eval_with_powers_on_curve(&proof_key.alpha); let g_h = h.eval_with_powers_on_curve(&proof_key.alpha); let g_p_prime = p.eval_with_powers_on_curve(&proof_key.alpha_prime); Proof { u_prime: g_p * delta.get_value(), v_prime: g_h * delta.get_value(), w_prime: g_p_prime * delta.get_value(), } } pub fn verify(g2: &G2Point, proof: &Proof, vk: &VerificationKey) -> bool { // Check e(u', rG_2) = e(w', G_2) let pairing1 = optimal_ate_pairing(&proof.u_prime, &vk.g_r); let pairing2 = optimal_ate_pairing(&proof.w_prime, g2); let check1 = pairing1 == pairing2; // Check e(u', G_2) = e(v', tG_2) let pairing3 = optimal_ate_pairing(&proof.u_prime, g2); let pairing4 = optimal_ate_pairing(&proof.v_prime, &vk.g_t_s); let check2 = pairing3 == pairing4; check1 && check2 } }