ECFFT算法

文摘   2024-10-16 14:38   泰国  

原文:https://solvable.group/posts/ecfft/

作者:William Borgeaud

译者:Kurt Pan

本文是关于 Eli Ben-Sasson、Dan Carmon、Swastik Kopparty 和 David Levit 最近发表的一篇论文的。作者在这篇论文中提出了经典 FFT 算法的适用于所有有限域的令人惊叹的新推广。本文将概述该算法及其在 Sage 中的简单实现。我强烈建议阅读该论文以了解更多详细信息和背景。

  • https://arxiv.org/abs/2107.08473

经典FFT算法

为质数, 是大小为 的子群 。经典FFT 算法可用于在 中对次数 的多项式上求值。注意, 朴素的对 上每一点的求值算法需要 次操作。

FFT 的工作原理是将 写作

其中 次多项式 的偶数和奇数项系数 。

因此, 给定 ,上的求值, 我们可以用次操作恢复出 上的求值。

需要注意的关键在于 的大小是 的一半,因为 。因此,如果我们用表示 FFT 的运行时间, 我们有以下的递推关系

由此我们可以推断出

ECFFT算法

ECFFT 算法的目标是将 FFT 算法推广到不具有大小为的乘法子群的有限域上 , 即的域

这个想法既聪明又简单。概述如下:

  1. 找到一条椭圆曲线 使得 。令 是大小为的子群, 的一个陪集 。
  2. 是一个次数为2的同源 ,使得 的大小是 的一半。
  3. 使用 分解为两个较小的多项式 ,使用椭圆曲线 和陪集 ,将 ECFFT 应用于 上。

我现在将更详细地解释这些步骤。用 Sage 给出 Secp256k1 曲线的基域的实现, 即

p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
F = GF(p)

寻找曲线

我们需要找到一条曲线 使得 。论文里给出了这样做的算法。然而,暴力搜索对于较小的值 来说也足够有效了。

这是一个通过暴力搜索寻找曲线的脚本

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)

,这可以在大约一个小时内找到一条曲线 。这是我找到的曲线

选择陪集

是大小为 的子群 。我们选择一个 的随机陪集

gen = E.gens()[0]
G = (gen.order()//n) * gen
R = E.random_element()
H = [R + i*G for i in range(n)]

最后, 令 上的投影 。 将是我们将在其上执行 ECFFT的大小为的集合。

L = [h.xy()[0for 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)

使用-坐标, 由2次有理函数 给出。

psi = phi.x_rational_map()
u, v = psi.numerator(), psi.denominator()

由此我们可以得到一条新的椭圆曲线 , 这是 的到达域,一个新的陪集 , 以及一个新的给出的 的子集。

上分解多项式

在经典 FFT 中, 我们能根据两个较小多项式 在较小的集合上的求值写出 上的求值 。

我们现在想要通过替换来执行相同的操作,将 替换成 替换成 , 以及平方操作替换成

是一个次数 的多项式 。那么存在次数 的多项式 ,使得

证明见论文附录 A。证明的思想是,从 次多项式对到次数 多项式的线性映射 是单射, 同时也是双射, 因为它的定义域和到达域具有与-向量空间同样的维度。

计算多项式 不像经典 FFT 那样简单。然而, 可以很容易从 上的求值到 上的求值,反之亦然。

实际上, 给定 使得 , 通过上面的公式写出 处的求值,我们得到以下线性关系(令

可以看出矩阵是可逆的, 因此我们可以很容易地从 ,反之亦然。这给出了我们稍后将使用的以下有效的一一对应关系:

更重要的是, 矩阵不依赖于 , 因此我们可以预先计算它,并将其重用于 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操作

最后一个难题是 EXTEND 操作。令 分别在偶数和奇数索引处的元素 ,  

S = [L[i] for i in range(0, n, 2)]
S_prime = [L[i] for i in range(1, n, 2)]

给定次数的多项式上的求值,EXTEND 操作计算 上的求值。该论文的主要结果是, 有一个 的EXTEND算法。请注意, 一个朴素的算法要通过在上拉格朗日插值恢复的系数 ,然后在 上求值,需要

该算法的工作原理如下。 如果 是常数且 上的求值是一样的。

否则, 从 上的求值导出 上的求值, 如上节所示。然后应用 EXTEND操作两次以获得 上的求值 。最后,恢复 上的求值,如上节所示。

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 中的相同, 为

上求值多项式

上一节中给出的 EXTEND 操作的算法会是论文中给出的许多高效算法的构建块。

其中之一是 ENTER 操作的算法, 该算法计算次数 的多项式 上的求值。这与我们在经典 FFT 中所做的类似。

算法非常简单。按其低系数和高系数分解  为 。对 上调用 ENTER 得到 上的求值。然后调用 EXTEND 两次以获得 上的求值。因为 , 我们得到 上的求值,并且可以导出 上的求值 。

运行时间的递推关系现在为

因为我们必须在递归步骤中调用 EXTEND。因此运行时间 为 ,比经典 FFT 稍差。

在 Sage 之外运行

正如这篇文章 (希望) 展示的, 这些算法在像 Sage 这样的计算机代数系统中实现起来非常简单。然而, Sage 是极其慢的, 这些实现远谈不上速度。

这些算法最酷的一点是对于给定的域 , 我们可以在 Sage 中预先计算所有必要的数据:所有的集合 以及 EXTEND 算法中所使用的(逆)矩阵。

一旦我们有了这些预计算数据, 算法就只使用上的基本操作 , 即加法和乘法。

因此, 实际的算法可以很容易地用 C++ 或 Rust 等快速语言实现, 同时不必用这些语言实现所有的椭圆曲线和同源机制。

最终代码

#p 是 Secp256k1 曲线基域的大小
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
F = GF(p)
# 有关如何找到 a 和 b 使得 2^12 能整除 E 的阶,请看文章。
a, b = 1641632526086088189863104799840287255425991771106608433941469413117059106896
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()[0for 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]


XPTY
寓形宇内复几时,曷不委心任去留?胡为乎遑遑欲何之?富贵非吾愿,帝乡不可期。怀良辰以孤往,或植杖而耘耔。登东皋以舒啸,临清流而赋诗。