本文是关于 Eli Ben-Sasson、Dan Carmon、Swastik Kopparty 和 David Levit 最近发表的一篇论文的。
作者在这篇论文中提出了经典 FFT 算法的适用于所有有限域的令人惊叹的新推广。本文将概述该算法及其在 Sage 中的简单实现。我强烈建议阅读该论文以了解更多详细信息和背景。
令$p$为质数, 且$n = 2^k$是大小为$\mid n \mid p-1,\ \langle w \rangle = H < \mathbb{F}p^*$的子群 。经典FFT算法可用于在$O(n \log n)$中对次数$< n$的多项式$P(X) = \sum{i=0}^{n} a_i X^i$在上$H$求值。注意, 朴素的对$P$在$H$上每一点的求值算法需要$O(n^2)$次操作。
FFT 的工作原理是将$P$写作 $P(X) = P_0(X^2) + X P_1(X^2)$
其中$P_0$,$P_1$是$< n/2$次多项式$P$的偶数和奇数项系数 。
因此, 给定$P_0$和$P_1$在$H^2$,上的求值, 我们可以用$O(n)$次操作恢复出$P$在$H$上的求值。
需要注意的关键在于$H^2$的大小是$H$的一半,因为$H = -H$。因此,如果我们用$F$表示 FFT 的运行时间, 我们有以下的递推关系 $F(n) = 2 F\left(\frac{n}{2}\right) + O(n)$
由此我们可以推断出$F(n) = O(n \log n)$。
ECFFT 算法的目标是将 FFT 算法推广到不具有大小为$n$的乘法子群的有限域上 , 即$n\nmid p-1$的域$\mathbb{F}_p$。
这个想法既聪明又简单。概述如下:
我现在将更详细地解释这些步骤。用 Sage
给出 Secp256k1
曲线的基域的实现, 即$\mathbb{F}_p$
$p = 115792089237316195423570985008687907853269984665640564039457584007908834671663$
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F F = GF(p)
我们需要找到一条曲线$E(\mathbb{F}_p)$使得$n | #E(\mathbb{F}_p)$。论文里给出了这样做的算法。然而,暴力搜索对于较小的值$n$来说也足够有效了。
这是一个通过暴力搜索寻找曲线的脚本
def find_curve(n):
while True:
b = F.random_element()
E = EllipticCurve(F, [1, b])
if E.order() % n == 0:
return E
n = 2^12
E = find_curve(n)
对$n = 2^{12}$,这可以在大约一个小时内找到一条曲线 。这是我找到的曲线$E : y^2 = x^3 + x + 641632526086088189863104799840287255425991771106608433941469413117059106896$
令$G < E$是大小为$n$的子群 。我们选择一个$G$的随机陪集$H$。
gen = E.gens()[0]
G = (gen.order()//n) * gen
R = E.random_element()
H = [R + i*G for i in range(n)]
最后, 令$L$是$H$在$\mathbb{F}_p$上的投影 。$L$将是我们将在其上执行 ECFFT的大小为的$n$集合。
L = [h.xy()[0] for h in H]
我们需要一个将大小减半的同源。这用 Sage 很容易找到:
def find_isogeny(E, H):
for phi in E.isogenies_prime_degree(2):
if len(set([phi(h) for h in H])) == len(H)//2:
return phi
phi = find_isogeny(E, H)
使用$E$的$x$-坐标,$\phi$由2次有理函数$\psi(X) = \mu(X)/\nu(X)$出。
psi = phi.x_rational_map() u, v = psi.numerator(), psi.denominator()
由此我们可以得到一条新的椭圆曲线$E'$, 这是$\phi$的到达域,一个新的陪集$\phi(H)$, 以及一个新的$\psi(L)$给出的$\mathbb{F}_p$的子集。
在经典 FFT 中, 我们能根据两个较小多项式$P_0,P_1$在较小的集合$H^2$上的求值写出$P$在$H$上的求值 。
我们现在想要通过替换来执行相同的操作,将$H$替换成$L$,$H^2$替换成$\psi(L)$, 以及平方操作替换成$\psi$。
令$P$是一个次数$< n$的多项式 。那么存在次数$< n/2$的多项式 ,使得 $P({X}) = (P{0}(\psi({X})) + {X}P{1}(\psi({X})))v({X})^{n/2-1}$ 证明见论文附录 A。证明的思想是,从$< n/2$次多项式对到次数$< n$多项式的线性映射$(P_0,P_1) \mapsto P$是单射, 同时也是双射, 因为它的定义域和到达域具有与$\mathbb{F}_p$-向量空间同样的维度。
计算多项式$P_0,P_1$不像经典 FFT 那样简单。然而, 可以很容易从$P$在$L$上的求值到$P_0$和$P_1$在$\psi(L)$上的求值,反之亦然。
实际上, 给定$s_0,s_1\in L$使得$\psi(s_0) = \psi(s_1) = t \in \psi(L)$, 通过上面的公式写出$P(X)$在$X = s_0,s_1$处的求值,我们得到以下线性关系(令$q = n/2 - 1$) $\begin{bmatrix} P(s_0) \ P(s_1) \end{bmatrix} = \begin{bmatrix} v(s_0)^q & s_0v(s_0)^q \ v(s_1)^q & s_1v(s_1)^q \end{bmatrix} \begin{bmatrix} P_0(t) \ P_1(t) \end{bmatrix}$ 可以看出矩阵是可逆的, 因此我们可以很容易地从$(P(s_0),P(s_1))$到$(P_0(t),P_1(t))$,反之亦然。这给出了我们稍后将使用的以下有效的一一对应关系:
$P$在$L$上的求值$\longleftrightarrow$$P_0,P_1$在$\psi(L)$上的求值
更重要的是, 矩阵不依赖于$P$, 因此我们可以预先计算它,并将其重用于 ECFFT 的所有实例。
inverse_matrices = []
nn = n // 2
q = nn - 1
for j in range(nn):
s0, s1 = L[j], L[j + nn]
assert psi(s0) == psi(s1)
M = Matrix(F, [[v(s0)^q,s0*v(s0)^q],[v(s1)^q, s1*v(s1)^q]])
inverse_matrices.append(M.inverse())
最后一个难题是 EXTEND 操作。令 是 分别在偶数和奇数索引处的元素 , 。
S = [L[i] for i in range(0, n, 2)] S_prime = [L[i] for i in range(1, n, 2)]
给定次数$< n/2$的多项式$Q$在$S$上的求值,EXTEND 操作计算$Q$在$S'$上的求值。该论文的主要结果是, 有一个$O(n\log n)$的EXTEND算法。请注意, 一个朴素的算法要通过在$S$上拉格朗日插值恢复$Q$的系数 ,然后在$S'$上求值,需要$O(n^2)$。
该算法的工作原理如下。 如果$#S = #S' = 1,Q$是常数且$S$在$S$和$S'$上的求值是一样的。
否则, 从$Q$在$S$上的求值导出$Q_0,Q_1$在$\psi(S)$上的求值, 如上节所示。然后应用 EXTEND操作两次以获得$Q_0,Q_1$在$\psi(S')$上的求值 。最后,恢复$Q$在$S'$上的求值,如上节所示。
def extend(Q_evals, S, S_prime, matrices, inverse_matrices):
n = len(Q_evals)
nn = n // 2
if n == 1:
return Q_evals
Q0_evals = []
Q1_evals = []
q = nn - 1
for j in range(nn):
s0, s1 = S[j], S[j + nn]
y0, y1 = Q_evals[j], Q_evals[j + nn]
Mi = inverse_matrices[n][j]
q0, q1 = Mi * vector([y0, y1])
Q0_evals.append(q0)
Q1_evals.append(q1)
Q0_evals_prime = extend(Q0_evals, [psi(s) for s in S], [psi(s) for s in S_prime], matrices, inverse_matrices) Q1_evals_prime = extend(Q1_evals, [psi(s) for s in S], [psi(s) for s in S_prime], matrices, inverse_matrices)
return [
M * vector([q0, q1])
for M, q0, q1 in zip(matrices[n], Q0_evals_prime, Q1_evals_prime) ]
运行时间的递推关系与经典 FFT 中的相同, 为$O(n\log n)$。
上一节中给出的 EXTEND 操作的算法会是论文中给出的许多高效算法的构建块。
其中之一是 ENTER 操作的算法, 该算法计算次数$< n$的多项式$P$在$L$上的求值。这与我们在经典 FFT 中所做的类似。
算法非常简单。按其低系数和高系数分解$P(X)$为$U(X) + X^{n/2}V(X)$。对$U$和$V$在$S$上调用 ENTER 得到$U,V$在$S$上的求值。然后调用 EXTEND 两次以获得$U,V$在$S'$上的求值。因为$L = S\cup S'$, 我们得到$U,V$在$L$上的求值,并且可以导出$P$在$L$上的求值 。
运行时间的递推关系现在为 $F(n) = 2 F(n/2) + O(n \log n)$
因为我们必须在递归步骤中调用 EXTEND。因此运行时间 为$F(n) = O\left( n \log^2 n \right)$,比经典 FFT 稍差。
正如这篇文章 (希望) 展示的, 这些算法在像 Sage 这样的计算机代数系统中实现起来非常简单。然而, Sage 是极其慢的, 这些实现远谈不上速度。
这些算法最酷的一点是对于给定的域$\mathbb{F}_p$, 我们可以在 Sage 中预先计算所有必要的数据:所有的集合$L,S,S'$以及 EXTEND 算法中所使用的(逆)矩阵。
一旦我们有了这些预计算数据, 算法就只使用$\mathbb{F}_p$上的基本操作 , 即加法和乘法。
因此, 实际的算法可以很容易地用 C++ 或 Rust 等快速语言实现, 同时不必用这些语言实现所有的椭圆曲线和同源机制。
#p 是 Secp256k1 曲线基域的大小
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
F = GF(p)
# 有关如何找到 a 和 b 使得 2^12 能整除 E 的阶,请看文章。
a, b = 1, 641632526086088189863104799840287255425991771106608433941469413117059106896
E = EllipticCurve(F, [a,b])
log_n = 12
n = 2^log_n
assert E.order() % n == 0
g = E.gens()[0]
G = (g.order()//n) * g
assert G.order() == n
R = E.random_element()
H = [R + i*G for i in range(2^log_n)]
L = [h.xy()[0] for h in H]
S = [L[i] for i in range(0, n, 2)]
S_prime = [L[i] for i in range(1, n, 2)]
def precompute(log_n, S, S_prime, E):
Ss = {}
Ss_prime = {}
matrices = {}
inverse_matrices = {}
for i in range(log_n, -1, -1):
n = 1 << i
nn = n // 2
Ss[n] = S
Ss_prime[n] = S_prime
matrices[n] = []
inverse_matrices[n] = []
for iso in E.isogenies_prime_degree(2):
psi = iso.x_rational_map()
if len(set([psi(x) for x in S]))==nn:
break
v = psi.denominator()
q = nn - 1
for j in range(nn):
s0, s1 = S[j], S[j + nn]
assert psi(s0) == psi(s1)
M = Matrix(F, [[v(s0)^q,s0*v(s0)^q],[v(s1)^q, s1*v(s1)^q]]) inverse_matrices[n].append(M.inverse()) s0, s1 = S_prime[j], S_prime[j + nn] assert psi(s0) == psi(s1) M = Matrix(F, [[v(s0)^q,s0*v(s0)^q],[v(s1)^q, s1*v(s1)^q]]) matrices[n].append(M)
S = [psi(x) for x in S[:nn]]
S_prime = [psi(x) for x in S_prime[:nn]]
E = iso.codomain()
return Ss, Ss_prime, matrices, inverse_matrices
# 预计算 EXTEND_S,S' 所需的数据
Ss, Ss_prime, matrices, inverse_matrices = precompute(log_n-1, S, S_prime, E)
def extend(P_evals):
n = len(P_evals)
nn = n // 2
if n == 1:
return P_evals
S = Ss[n]
S_prime = Ss_prime[n]
P0_evals = []
P1_evals = []
for j in range(nn):
s0, s1 = S[j], S[j + nn]
y0, y1 = P_evals[j], P_evals[j + nn]
Mi = inverse_matrices[n][j]
p0, p1 = Mi * vector([y0, y1])
P0_evals.append(p0)
P1_evals.append(p1)
P0_evals_prime = extend(P0_evals)
P1_evals_prime = extend(P1_evals)
ansL = []
ansR = []
for M, p0, p1 in zip(matrices[n], P0_evals_prime, P1_evals_prime): v = M * vector([p0, p1])
ansL.append(v[0])
ansR.append(v[1])
return ansL + ansR
# 生成一个随机多项式进行测试
R.<X> = F[]
P = sum(F.random_element() * X^i for i in range(1<<(log_n - 1)))
# P 在 S 上求值
P_evals = [P(x) for x in S]
# 结果包括P在S'上的求值
result = extend(P_evals)
assert result == [P(x) for x in S_prime]`
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!