p
是 :G
和相应群阶 n
的特定值。关于参数的更多细节,请查看文后链接 [3]。此外,以太坊依赖的加密安全哈希函数 keccak256,也是我们将要使用的,我们使用 Vocdoni 的 keccak circom 实现。d_A
是一个在 [1, n - 1]
范围内随机生成的整数,公钥被定义为 Q_A = d_A * G
。即,我们只需用椭圆曲线乘以标量,以及使基点 G
乘以私钥。这将组合使用椭圆曲线的点加和倍点来完成。Q_A
签署一个信息 m
, 会产生一个签名 (r, s)
。(r, s)
和公钥 Q_A
,我们想检查这个签名是否有效。你可以在文后链接 [4] 中找到签名验证算法的完整描述。这个算法可被分解为以下操作:m
A * B + C = 0
形式的方程,其中 A
,B
,C
是信号的线性组合,在我们想要完成的事情中,它们可以被认为是变量。+
和 *
。然而,要确保约束是充分的就难得多了,因为我们的目标是构建电路,使输出信号成为输入信号的正确函数。换句话说,应该没有办法在不违反约束条件的情况下对数值进行操作。目前我们依靠审计代码和运行测试来验证这些电路的正确性。secp256k1
的素数 p
是 256 位,这就造成了一点尴尬:(254 - 1) / 2 = 126
-bit,因为两个 k
位数相乘的结果是 2k
位数,这将需要 ceil(256 / 126) = 3
个寄存器来表示 [0, p - 1]
中的整数。那么我们就浪费了 3 * 126 - 256 = 122
-bit,这基本上是一个完整的寄存器位数位。ceil(256 / 3) = 86
-bit,这样则只是浪费了 3 * 86 - 256 = 2
-bit ,这是给定约束条件下我们能做的最好。a[k]
和 b[k]
表示为两个整数,每个都使用 k
个寄存器。a
实际上是检查:a[k - 1]
a[k - 1] == b[k - 1]
和 a[k - 2]
)a[k - 1] == b[k - 1]
和 a[k - 2] = b[k - 2]
和 a[k - 3]
)a[i] + b[i]
求和,然后用单独的信号模拟进位。a >= b
,则用一个借入信号计算 a[i] - b[i]
。k
个寄存器的情况下,一个简单的乘法实现会使用两个 for-loops 来计算中间乘积,对于 O(k^2)
个约束, 是一组接受 O(nk)
个约束的进位(主要成本是一组范围检查,即每 2k
个寄存器中最多只有 n
-bit)。我们最初的 Bigint 乘法实现,需要几千个约束来验证两个 256 位数字的乘法,我们想知道能在这个基础上做得更好吗?x
为基数的数字 n
的真表示为一个数组 a
,这个数组中的每个值 a[i]
是一个小于 x
的非负整数,且满足 n = a[0] + a[1] * x + ... + a[k - 1] * x ** (k - 1)
。请注意,对于任何非负整数 n
和任何基数 x
, n
只有一个真表示。n
的 m-溢出表达是一个数组 a
, 其中每个值 a[i]
是一个小 于 m * x
的非负整数,且满足 n = a[0] + a[1] * x + ... + a[k - 1] * x ** (k - 1)
。请注意,一个非负整数 n
对于同一个基数 x
可能有许多不同的 m-溢出表达。k * 2**n
-溢出表示 (c[2k]
) , 表示为 a[k]
和 b[k]
。这里的重点是我们选择了一个特定的可以被有效约束地 k * 2**n
-溢出表示,该灵感来自 xJsnark 的一个技巧。在方法的第二部分,我们建立了一个可以约束数字的溢出表示的电路,该过程我们称之为归约,即,建立一个电路,限制一个数字的溢出表示以使之等于其完整表示。2k
个寄存器,所以我们能期望的最好结果大概是 2k
个约束,即每个寄存器一个约束。A(x) = a[0] + a[1] * x + ... + a[k - 1] * x ** (k - 1)
B(x) = b[0] + b[1] * x + ... + b[k - 1] * x ** (k - 1)
C(x) = c[0] + c[1] * x + ... + c[2 * k - 2] * x ** (2 * k - 2)
a * b = c
与验证多项式恒等式 A(x) * B(x) = C(x)
是相同的,则可使用 x = 2 ** 86
,其中系数 c[j]
可以达到 k * (2 ** 86) ** 2
。两个 for-loop 的实现相当于分别计算每个系数 c[j] = sum a[i] * b[j - i]
。但是如果只是在 2k - 1
个点上评估这两个多项式呢?m
阶多项式在 m + 1
个不同的点上达成一致,则它们一定是相等的,例如两个点决定了一条直线(1阶 ),三个点决定了一条抛物线(2阶),以此类推。由于我们的多项式有 2 * k - 2
阶,那么对于 y = 0, ..., 2 * k - 2
来说, 2 * k - 1
约束 C(y) = A(y) * B(y)
足以检查多项式是否相等,因为 A(y), B(y), C(y)
中的每一个都只是信号 a[i], b[i], c[i]
的加权和,以及在我们的计算模型中,每个等式只需要花费一个约束。K * (2 ** n)
-溢出表示,将约束集从 O(k ** 2)
减少到 O(k)
!c[2k]
和完整表示 d[2k]
的等价性。我们的策略是初始化一组中间信号 carry[2k]
,然后寄存器逐个验证 c[i]
和 d[i]
(包括来自先前寄存器的进位)之间的差异总是 2 ** n
的整数倍。我们使用以下约束:carry[2k]
carry[0] = c[0] - d[0] * (2 ** -n)
i > 0
进行约束 carry[i] = (c[i] - d[i] + carry[i-1]) * (2 ** -n)
。可通过约束 2 ** n * carry[i] = c[i] - d[i] + carry[i-1]
来做加法和乘法d[i]
最多只有 n
位carry[i]
最多只有 n + log(k)
位carry[2k-1] = 0
a / b
写成 (q, r)
,其中 q
是商, r
是余数,则只需约束 a = b * q + r
和 0 <= r
。虽然仍然需要用长除法来计算这些值以产生一个见证,但约束这个见证要简单得多,因为可以再次使用 BigInt 乘法约束。a mod p
操作(也就是除以 p
后的余数),同时也可以用这个电路来建立加法、减法和乘法 mod p
!剩下的唯一操作是模逆运算,我们可以将其限制为 a_inv * a = 1 (mod p)
并重新使用乘法电路,此处可以用费马小定律 (Fermat&#39;s Little Theorem )来计算见证的 a_inv
,即 a ** (p - 2) = a_inv (mod p)
。实现 ECDSAPrivToPub
需要将 secp256k1
上的基点 G
与私钥 d_A
(一个标量)进行标量乘法 d_A * G
。这方面的标准算法是倍点-加,它使用 d_A
的二进制表示:
d_A = bits[0] + bits[1] * 2 + ... + bits[255] * (2 ** 255).
def double_and_add(bits):
res = O // O is the additive identity on secp256k1
for i in [0, ..., 255]:
res = res + res // doubling on secp256k1
if bits[i] == 1:
res = res + G // addition of unequal points on secp256k1
return res
G
在我们的设定中是已知和固定的,我们可以通过预先计算和缓存 G
的 2 次方的倍数来减少点操作的数量,然后再通过与私钥二进制数相对应的 G
的缓存倍数之和来计算出公钥。Gpow[256] = precompute_G_powers() // Gpow[i] = (2 ** i) * G
def cached_double_and_add(bits):
let res = O // O is the additive identity on secp256k1
for i in [0, ..., 255]:
if bits[i] == 1:
res = res + Gpow[i] // addition of unequal points on secp256k1
return res
stride
的组,缓存 G
的倍数对应于一个组中二进制数为非零的数字,并将 G
表示为缓存的椭圆曲线点之和:// Gpow[i][j] = j * (2 ** (i * stride)) * G for j = 1, ..., 2**stride - 1
Gpow[ceil(256 / stride)][2 ** stride] = precompute_G_powers(stride)
def cached_windowed(bits, stride):
let res = O // O is the additive identity on secp256k1
for i in [0, ..., ceil(256 / stride) - 1]:
j = bits[i * stride] + ... + bits[(i + 1) * stride - 1] * 2**(stride - 1)
if j != 0:
res = res + Gpow[i][j] // addition of unequal points on secp256k1
return res
ceil(256 / stride)
,并且随着 stride
的增加而减少。然而,随着 stride
的增加,我们必须维持一个大小为 ceil(256 / stride) * 2**stride
点的不断增长的缓存 ,并使用一个具有 2 ** stride
种选择可能的多路复用器从 Gpow[i]
中选择第 jth
个元素。令人惊讶的是,在 zkSNARK 中这是一个昂贵的操作,每次选择需要 O(2 ** stride)
个约束。stride
进行了穷举搜索,发现 stride
和约束数量之间有如下关系。[0, p - 1]
范围内,并将所有寄存器保持在所需的范围内。