7.2. Plonk Protocol¶
[TODO] Viết lại.
7.2.1. Elliptic curve và pairing¶
7.2.1.1. Elliptic curve¶
Đường cong elliptic trên trường \(\mathbb{F}\) có dạng \(y^2 = x^3 + ax + b\) với \(x\), \(y\), \(a\), \(b\) thuộc \(\mathbb{F}\) và \(a, b\) là các phần tử cho trước.
Khi đó đường cong elliptic là tập hợp các điểm \((x, y)\) thỏa mãn phương trình trên và điểm vô cực \(\mathcal{O}\).
Để lấy ví dụ, xét \(\mathbb{F}_{101}\) là trường modulo \(101\) và đường cong elliptic với phương trình \(y^2 = x^3 + 3 \pmod{101}\).
7.2.1.2. Pairing¶
Gọi \(\mathbb{G}_1, \mathbb{G}_2, \mathbb{G}_t\) là các nhóm có cùng order bằng \(r\) và \(e\) là một pairing. Khi đó \(e\) là ánh xạ sao cho
7.2.1.3. Embedding degree¶
Order của đường cong elliptic trên là \(102 = 2 \cdot 3 \cdot 17\). Như vậy tồn tại các điểm trên đường cong có order là \(17\). Tuy nhiên, chung ta cần nhiều điểm khác cũng có order là \(17\) nhưng không nằm trên đường cong \(\mathbb{F}_{101}\). Lúc này chúng ta sẽ mở rộng trường lên \(\mathbb{F}_{101^k}\).
Bậc thấp nhất của mở rộng trường (với characteristic \(p\)) chứa tất cả điểm với order \(r\) được gọi là embedding degree. Nói đơn giản, embedding degree là số \(k\) nhỏ nhất mà \(r \mid (p^k - 1)\).
Trong ví dụ này thì \(r = 17\) là order của điểm và \(k = 2\).
Khi đó trường mở rộng \(\mathbb{F}_{101^2}\) với đa thức tối giản \(x^2 + 2\) cho đường cong với các điểm order \(17\).
Mỗi phần tử trong \(\mathbb{F}_{101^2}\) có dạng \(a + bx\) với \(a, b \in \mathbb{F}_{101}\).
7.2.2. Mạch Plonk¶
7.2.2.1. Trusted setup¶
Chúng ta cần một trusted setup gọi là structured reference string (SRS, chuỗi liên kết cấu trúc).
SRS là một danh sách các điểm trên đường cong elliptic được tính toán từ một số bí mật sinh ngẫu nhiên \(s\). Theo paper PLONK thì một mạch \(n\) cổng sẽ cần \(n+5\) điểm sau:
Trong ví dụ này, \(s\) giúp sinh ra các điểm trong subgroup với generator là \(G_1\). Do \(G_1\) có order là \(17\), \(s\) không được vượt quá \(17\).
Chọn \(G_1 = (1, 2)\) và \(G_2 = (36, 31x)\) là hai generator cho hai subgroup đều có order là \(17\).
Xét \(s = 2\) (cho dễ tính) và \(n = 4\) (có \(4\) cổng). Khi đó \(1 \cdot G_1, s \cdot G_1, \cdots, s^{n+2} \cdot G_1\) là các điểm sau:
Tương tự, \(1 \cdot G_2\) và \(s \cdot G_2\) là các điểm \((36, 31x)\) và \((90, 82x)\).
Như vậy,
7.2.2.2. Mạch PLONK¶
Plonk circuit là mạch được biểu diễn ở dạng đa thức (gồm phép cộng và nhân). Trong đó mỗi cổng, hay constrain, thực hiện một thao tác (cộng hoặc nhân).
7.2.2.3. Ví dụ với định lý Pythagoras¶
Giả sử chúng ta có bộ ba \((3, 4, 5)\) và chúng ta muốn kiểm tra xem chúng có thỏa mãn phương trình Pythagoras \(a^2 + b^2 = c^2\) hay không.
Giả sử bộ ba \((3, 4, 5)\) sẽ đi vào ba đầu là \(x_1, x_3, x_5\). Khi đó mạch tính
Phương trình cuối cho kết quả \(x_1^2 + x_3^2 = x_5^2\) là điều kiện thỏa mãn phương trình Pythagoras. Mỗi \(x_i\) được gọi là "wire" (dây). Mạch này đi qua ba cổng nhân và một cổng cộng. Ta có thể viết tất cả \(x_i\) thành vector \(\mathbf{x} = (x_1, x_2, x_3, x_4, x_5, x_6)\). Đối với ví dụ bộ ba \((3, 4, 5)\) thì
Tương tự, đối với bộ ba \((5, 12, 13)\) thì
Mỗi cổng plonk sẽ gồm hai dây input trái và phải, và một dây output. Ta gọi dây input trái và phải lần lượt là \(a\) và \(b\), còn dây output là \(c\).
Cổng plonk phức tạp hơn cổng cộng hoặc nhân để kiểm tra bộ ba Pythagoras ở trên. Cổng plonk đầy đủ có dạng
Trong đó \(a, b\) là hai dây input trái và phải, \(c\) là dây output, còn lại là các hệ số cho trước.
Ví dụ. Đối với đẳng thức \(a + b = c\) thì ta cho \(q_L = q_R = 1\), \(q_O = -1\) và \(q_M = q_C = 0\).
Mạch kiểm tra bộ ba Pythagoras bên trên có thể được viết lại dưới dạng mạch với các cổng plonk như sau
Bốn cổng plonk này tương đương bốn cổng kiểm tra bên trên. Tương tự bên trên, nếu gọi \(\mathbf{a}\) là vector các dây input trái, \(\mathbf{b}\) là vector các dây input phải và \(\mathbf{c}\) là các dây output thì
Ta cũng viết hệ số cổng plonk dưới dạng vector, khi đó
Lúc này, các vector \(\mathbf{q}\) được gọi là selector và sẽ giúp xây dựng nên cấu trúc của mạch. Các vector \(\mathbf{a}, \mathbf{b}, \mathbf{c}\) thể hiện các biến của mạch và là private input của Prover (bên chứng minh ZKP).
7.2.3. Proof¶
7.2.3.1. Roots of unity¶
Trong trường \(\mathbb{F}\) có phần tử đơn vị là \(1\). Nghiệm bậc \(n\) của \(1\) là các nghiệm \(x\) thỏa mãn phương trình \(x^n = 1\).
Ở ví dụ trên, mỗi subgroup đều có order là \(17\). Bây giờ các tính toán của chúng ta sẽ nằm trong \(\mathbb{F}_{17}\).
Nhắc lại, các vector \(\mathbf{a}, \mathbf{b}, \mathbf{c}\) cũng như các vector hệ số \(\mathbf{q_L}, \mathbf{q_R}, \mathbf{q_O}, \mathbf{q_M}, \mathbf{q_C}\) đều có bốn phần tử. Chúng ta có cách xây dựng đa thức \(f(x)\) từ các cặp \((x_i, f(x_i))\) là đa thức nội suy.
Khi đó, mỗi vector ở trên sẽ tương ứng \(f(x_i)\) với \(1 \leqslant i \leqslant 4\). Đa thức sinh ra là đa thức bậc \(3\).
Ứng với mỗi vector \(\mathbf{a}, \mathbf{b}, \mathbf{c}\) ta xây dựng một đa thức bậc \(3\). Do có \(3 \cdot 4 = 12\) giá trị \(x_i\) nên ta cần một "cơ chế" để tách trường \(\mathbb{F}_{17}\) thành \(3\) tập rời nhau, mỗi tập \(4\) phần tử.
Đầu tiên là cần một tập con của \(\mathbb{F}_{17}\) có \(4\) phần tử. Tập con này là tập nghiệm của \(x^4 = 1 \pmod{17}\). Phương trình này có nghiệm và may thay là có đủ \(4\) nghiệm. Gọi \(H\) là tập các nghiệm của phương trình \(x^4 = 1 \pmod{17}\) thì \(H = \{ 1, 4, 16, 13 \}\).
Ta cần tìm thêm hai tập con khác của \(\mathbb{F}_{17}\), cũng chứa \(4\) phần tử và không giao nhau với \(H\). Chúng ta sẽ dùng coset vì hai coset bất kì của nhóm hoặc là không giao nhau, hoặc là trùng nhau.
Chọn \(k_1 = 2\) và \(k_2 = 3\) thì ta có các coset
Với vector \(\mathbf{a} = \{ a_1, a_2, a_3, a_4 \}\), ta mong muốn \(f_{\mathbf{a}}(h_i) = a_i\) với \(h_i \in H\), nghĩa là \(f_{\mathbf{a}}(1) = 3\), \(f_{\mathbf{a}}(4) = 4\), \(f_{\mathbf{a}}(16) = 5\) và \(f_{\mathbf{a}}(13) = 9\). Sử dụng đa thức nội suy Lagrange có thể tìm được hàm \(f_{\mathbf{a}}(x) = 1 + 13x + 3x^2 + 3x^3\).
Tương tự ta cũng tìm được đa thức nội suy cho các vector còn lại (tất cả đều dùng \(H\)).
Một vấn đề xảy ra là, bốn cổng plonk ở trên là rời nhau và không liên quan gì nhau. Do đó chúng ta cần một phương án để "nối" output từ cổng này thành input của cổng kia.
Để làm việc này, ta đánh nhãn vector \(\mathbf{a}\) bởi \(H\), vector \(\mathbf{b}\) bởi \(k_1 H\) và vector \(\mathbf{c}\) bởi \(k_2 H\). Cụ thể ta có ánh xạ
Gọi \(\sigma_1, \sigma_2, \sigma_3\) lần lượt là output khi ánh xạ tác động lên \(H, k_1 H, k_2 H\). Theo bảng trên thì \(\sigma_1 = (2, 8, 15, 3), \sigma_2 = (1, 4, 16, 12), \sigma_3 = (13, 8, 5, 14)\).
Mỗi tác động như vậy thực chất tương ứng với một đa thức bậc \(3\).
Gọi \(S_{\sigma_1}\) là đa thức từ \(H\) tới \(\sigma_1\). Sử dụng nội suy Lagrange ta tính được \(S_{\sigma_1} = 7 + 13 x + 10 x^2 + 6x^3\).
Tương tự, gọi \(S_{\sigma_2}\) là đa thức từ \(k_1 H\) tới \(\sigma_2\) và \(S_{\sigma_3}\) là đa thức từ \(k_2 H\) tới \(\sigma_3\). Ta tính được \(S_{\sigma_2} = 4 + 13x^2 + x^3\) và \(S_{\sigma_3} = 6 + 7x + 3x^2 + 14x^3\).
Đến đây ta có đủ thành phần để tạo proof, gồm \(5\) vòng.
7.2.3.2. Round 1¶
Round 1 bao gồm các bước:
tạo random các phần tử \(b_1, \ldots, b_6 \in \mathbb{F}_{17}\);
tính các đa thức \(a(x), b(x), c(x)\) theo công thức
Output là \([a(s)], [b(s)], [c(s)]\), trong đó \(Z_H\) là đa thức sao cho mọi phần tử của subgroup \(H\) là nghiệm của \(Z_H\). Do các phần tử trong \(H\) là nghiệm của \(x^4 = 1 \pmod{17}\) nên \(Z_H = x^4 - 1\).
Giả sử ta chọn (ngẫu nhiên) các phần tử \(b_1 = 7, b_2 = 4, b_3 = 11, b_4 = 12, b_5 = 16, b_6 = 2\). Khi đó
Tiếp theo ta tính \([a(s)], [b(s)], [c(s)]\) là các điểm trên đường cong sử dụng SRS ban đầu và hệ số của các đa thức \(a(x), b(x), c(x)\). Cụ thể prover sẽ tính
Tương tự cho \([b(s)]\) và \([c(s)]\). Như vậy ta có
7.2.3.3. Round 2¶
Round 2 được thực hiện qua các bước:
random các phần tử \(b_7, b_8, b_9 \in \mathbb{F}_{17}\);
nhận challenge từ verifier là \(\beta, \gamma \in \mathbb{F}_{17}\);
tính \(z(x)\) theo công thức:
Ở round này, chúng ta commit một đa thức \(z(x)\) để encode cho việc copy contrains (nối các dây của cổng) bên trên. Đa thức \(\mathrm{acc}(x)\) sẽ được đề cập sau.
Các giá trị challenge \(\beta, \gamma\) phụ thuộc vào protocol là interactive (tương tác) hay non-interactive.
trong protocol interactive, verifier chọn các giá trị này và gửi cho prover để prover tạo ra proof;
trong protocol non-interactive, các giá trị này sinh ra từ quá trình tính toán của prover và đi qua một hàm hash mật mã.
Trong cả hai trường hợp, giá trị này là không thể đoán trước và cần được tính giống nhau (về hàm hash, về trường) ở cả hai phía. Do đó quá trình này còn được gọi là biến đổi Fiat-Shamir có thể biến interactive thành non-interactive protocol.
Trước tiên ta cần tìm hiểu cơ chế interactive, ở đó verifier gửi các challenge cho prover.
Giả sử \(\beta = 12\), \(\gamma = 13\).
Hàm \(z(x)\) ở trên được xây dựng từ các accumulator vector, định nghĩa như sau:
Do cách định nghĩa \(S_{\sigma}\) ở trên mà sẽ có những phần ở tử và mẫu giản lược nhau, và tất nhiên là một số thì không.
Bằng tính toán trực tiếp thu được
Khi đó đa thức \(\mathrm{acc}(x)\) tương ứng với đa thức nội suy từ nguồn là subgroup \(H = \{ 1, 4, 16, 13 \}\) và ảnh tương ứng là vector \(\mathrm{acc} = (1, 3, 9, 4)\).
Nói cách khác là \(\mathrm{acc}(1) = 1\), \(\mathrm{acc}(4) = 3\), \(\mathrm{acc}(16) = 9\) và \(\mathrm{acc}(13) = 4\). Từ đó ta có
Thay vào công thức tính \(z(x)\) ta có
Cuối cùng thay giá trị secret \(s\) vào \(z(x)\) ta có \(z(s) = z(2) = 11\) và tính \([z(s)] = 11 (1, 2) = (32, 59)\).
7.2.3.4. Round 3¶
Round này chứa một lượng lớn tính toán. Chúng ta sẽ tính đa thức \(t(x)\) bậc \(3n + 5\) với \(n\) là số lượng cổng. Đa thức \(t(x)\) sẽ chứa mạch và tất cả assignment cùng lúc.
Quá trình ở round 3 gồm:
tính quotient challenge \(\alpha \in \mathbb{F}_{17}\);
tính quotient polynomial \(t(x)\)
tách \(t(x)\) thành các đa thức bậc nhỏ hơn \(n+2\) là \(t_{10}(x)\), \(t_{\text{mid}}(x)\) và \(t_{\text{ni}}(x)\) sao cho
Đầu ra của round \(3\) là \([t_{10}(s)]\), \([t_{\text{mid}}(x)]\) và \([t_{\text{ni}}(x)]\).
Dòng cuối có \(L_1(x)\) sẽ được định nghĩa dưới đây.
\(L_1(x)\) là cơ sở của đa thức nội suy từ \(H\). Với \(L_1(1) = 1\), các giá trị còn lại cho ảnh bằng \(0\). Khi đó \(L_1(x)\) tương ứng vector \((1, 0, 0, 0)\) nên \(L_1(x) = 13 + 13 x + 13 x^3 + 13 x^3\).
Verifier chọn số \(\alpha\) (ngẫu nhiên) và gửi cho prover.
7.2.3.5. Round 4¶
verifier chọn một số \(\mathfrak{z}\) và gửi cho prover;
prover khi đó cần tính
tính đa thức linearization
tính linearization evaluation \(\bar{r} = r(\mathfrak{z})\).
Output của round 5 là các giá trị
7.2.3.6. Round 5¶
Verifier chọn số ngẫu nhiên \(v \in \mathbb{F}_{17}\) và gửi cho prover.
tính đa thức đầu cho proof là \(W_{\mathfrak{z}}(x)\) như sau
tính đa thức tiếp theo cho proof là \(W_{\mathfrak{z} w}(x)\):
Output của round 5 là \([w_\mathfrak{z}], [w_{\mathfrak{z} w}]\).
7.2.3.7. Proof¶
Proof thu được là vector \(\pi_{\text{SNARK}}\) như sau
7.2.4. Code mẫu với SageMath¶
from sage.all import *
F101 = GF(101)['xn']; xn = F101.gen()
F101_2 = GF(101**2, name='yn', modulus=xn**2 + 2)
Ec = EllipticCurve(F101_2, [0, 3])
G1 = Ec(1, 2)
G2 = Ec(36, 31*xn)
s = 2
n = 4
SRS = []
for i in range(n+3):
SRS.append(s**i * G1)
for i in range(2):
SRS.append(s**i * G2)
print([srs.xy() for srs in SRS])
F17 = GF(17)
Pol = PolynomialRing(F17, 'x')
x = Pol.gen()
Z_H = x**4 - 1
k1, k2 = 2, 3
H = [F17(i) for i in [1, 4, 16, 13]]
assert set(H) == set([z[0] for z in Z_H.roots()])
k1_H = [F17(k1 * h) for h in H]
k2_H = [F17(k2 * h) for h in H]
print(f"k1 H = {k1_H}, k2 H = {k2_H}")
ai = [3, 4, 5, 9]
bi = [3, 4, 5, 16]
ci = [9, 16, 25, 25]
qLi = [0, 0, 0, 1]
qRi = [0, 0, 0, 1]
qOi = [-1, -1, -1, -1]
qMi = [1, 1, 1, 0]
qCi = [0, 0, 0, 0]
fa = Pol.lagrange_polynomial(list(zip(H, ai)))
fb = Pol.lagrange_polynomial(list(zip(H, bi)))
fc = Pol.lagrange_polynomial(list(zip(H, ci)))
fqL = Pol.lagrange_polynomial(list(zip(H, qLi)))
fqR = Pol.lagrange_polynomial(list(zip(H, qRi)))
fqO = Pol.lagrange_polynomial(list(zip(H, qOi)))
fqM = Pol.lagrange_polynomial(list(zip(H, qMi)))
fqC = Pol.lagrange_polynomial(list(zip(H, qCi)))
sigma_1 = list(map(F17, [2, 8, 15, 3]))
sigma_2 = list(map(F17, [1, 4, 16, 12]))
sigma_3 = list(map(F17, [13, 8, 5, 14]))
S_sig_1 = Pol.lagrange_polynomial(list(zip(H, sigma_1)))
S_sig_2 = Pol.lagrange_polynomial(list(zip(k1_H, sigma_2)))
S_sig_3 = Pol.lagrange_polynomial(list(zip(k2_H, sigma_3)))
# Dưới đây là kết quả tính theo ví dụ, khác phần trên
S_sig_2 = x**3 + 13*x**2 + 4
S_sig_3 = 14*x**3 + 3*x**2 + 7*x + 6
# Round 1
b_coeffs = [7, 4, 11, 12, 16, 2]
ax = (b_coeffs[0] * x + b_coeffs[1]) * Z_H + fa
bx = (b_coeffs[2] * x + b_coeffs[3]) * Z_H + fb
cx = (b_coeffs[4] * x + b_coeffs[5]) * Z_H + fc
ass = sum(i * j for i, j in zip(ax.coefficients(), SRS))
bss = sum(i * j for i, j in zip(bx.coefficients(), SRS))
css = sum(i * j for i, j in zip(cx.coefficients(), SRS))
print(ass.xy(), bss.xy(), css.xy())
# Round 2
b_coeffs.extend([14, 11, 7])
beta_, gamma_ = 12, 13
w = 4 # Choose from H
acc = [1]
for i in range(1, len(H)):
numerator_ = (ai[i-1] + beta_ * w**(i-1) + gamma_)
numerator_ *= (bi[i-1] + beta_ * k1 * w**(i-1) + gamma_)
numerator_ *= (ci[i-1] + beta_ * k2 * w**(i-1) + gamma_)
denominator_ = (ai[i-1] + beta_ * S_sig_1(w**(i-1)) + gamma_)
denominator_ *= (bi[i-1] + beta_ * S_sig_2(w**(i-1)) + gamma_)
denominator_ *= (ci[i-1] + beta_ * S_sig_3(w**(i-1)) + gamma_)
acc.append(acc[-1] * F17(numerator_) / F17(denominator_))
accx = Pol.lagrange_polynomial(list(zip(H, acc)))
print(accx)
zx = (b_coeffs[6] * x**2 + b_coeffs[7] * x + b_coeffs[8]) * Z_H + accx
zs = zx(s) * G1
print(f"[z(s)] = {zs.xy()}")
alpha = 15
L1x = Pol.lagrange_polynomial(list(zip(H, [1, 0, 0, 0])))
tx = (ax * bx * fqM + ax * fqL + bx * fqR + cx * fqO + 0 + fqC)
tx += (ax + beta_ * x + gamma_) * (bx + beta_ * k1 * x + gamma_) * (cx + beta_ * k2 * x + gamma_) * zx * alpha
tx -= (ax + beta_ * S_sig_1 + gamma_) * (bx + beta_ * S_sig_2 + gamma_) * (cx + beta_ * S_sig_3 + gamma_) * zx(w * x) * alpha
tx += (zx - 1) * L1x * alpha * alpha
#print(tx.coefficients(sparse=False))
print(tx % Z_H)
tx //= (x**4 - 1)
tx_c = tx.coefficients(sparse=False)
#print(tx_c)
t10 = Pol(tx_c[0:6])
tmid = Pol(tx_c[6:12])
tni = Pol(tx_c[12:18])
t10s = t10(s) * G1
tmids = tmid(s) * G1
tnis = tni(s) * G1
zz = 5
abar = ax(zz)
bbar = bx(zz)
cbar = cx(zz)
S_sig_1_bar = S_sig_1(zz)
S_sig_2_bar = S_sig_2(zz)
tbar = tx(zz)
zwbar = zx(w * zz)
rx = abar * bbar * fqM + abar * fqL + bbar * fqR + cbar * fqO + fqC
rx += (abar + beta_ * zz + gamma_) * (bbar + beta_ * k1 * zz + gamma_) * (cbar + beta_ * k2 * zz + gamma_) * zx * alpha
rx -= (abar + beta_ * S_sig_1_bar + gamma_) * (bbar + beta_ * S_sig_2_bar + gamma_) * beta_ * zwbar * S_sig_3 * alpha
rx += zx * L1x(zz) * alpha**2
rbar = rx(zz)
print(abar, bbar, cbar, S_sig_1_bar, S_sig_2_bar, zwbar, tbar, rbar)
v = F17(12)
wzx = t10 + zz**(n+2) * tmid + zz**(2*n+4) * tni - tbar
wzx += v * (rx - rbar)
wzx += v**2 * (ax - abar)
wzx += v**3 * (bx - bbar)
wzx += v**4 * (cx - cbar)
wzx += v**5 * (S_sig_1 - S_sig_1_bar)
wzx += v**6 * (S_sig_2 - S_sig_2_bar)
wzx //= (x - zz)
wzwx = (zx - zwbar) // (x - zz * w)
f__ = lambda fx, srs: sum(i * j for i, j in zip(fx.coefficients(sparse=False), srs))
a__ = f__(ax, SRS)
b__ = f__(bx, SRS)
c__ = f__(cx, SRS)
z__ = f__(zx, SRS)
t10__ = f__(t10, SRS)
tmid__ = f__(tmid, SRS)
tni__ = f__(tni, SRS)
wz__ = f__(wzx, SRS)
wzw__ = f__(wzwx, SRS)
pi_snark = [a__, b__, c__, z__, t10__, tmid__, tni__, wz__, wzw__, abar, bbar, cbar, S_sig_1_bar, S_sig_2_bar, rbar, zwbar]
print(pi_snark)
7.2.5. Tài liệu tham khảo¶
[1] Under the hood of zkSNARKs — PLONK protocol: Part 1, URL - https://medium.com/coinmonks/under-the-hood-of-zksnarks-plonk-protocol-part-1-34bc406d8303
[2] Under the hood of zkSNARKs — PLONK protocol: Part 2, URL - https://medium.com/coinmonks/under-the-hood-of-zksnarks-plonk-protocol-part-2-ee00d6accb4d
[3] Under the hood of zkSNARKs — PLONK protocol: Part 3, URL - https://medium.com/@cryptofairy/under-the-hood-of-zksnarks-plonk-protocol-part3-821855e49ce6