热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

zkECDSAPart2:底层原理

今年早些时候,我们介绍了 circom-ecdsa,一个用于 circom 中基于 ECDSA 算法的 zkSNARK 电路概念验证有效实现。这篇文章更深入地介绍了电路结构中的一些构建模块。在未来的文

早些时候,我们介绍了 circom-ecdsa,一个用于 circom 中基于 ECDSA 算法的 zkSNARK 电路概念验证有效实现。这篇文章更深入地介绍了电路结构中的一些构建模块。
在未来的文章中,还将讨论我们为 secp256k1 操作执行的一些更高级的优化。

ECDSA 是如何工作的?

zk-ECDSA 系列的第一部分,我们介绍了椭圆曲线数字签名算法(ECDSA:Elliptic Curve Digital Signature Algorithm )在包括比特币和以太坊的网络中发挥的作用。这篇文章中我们将深入了解实现细节,以了解如何将 ECDSA 操作转化为 circom 中的算术电路。这些电路反过来又指定了一个 zkSNARK ,该 zkSNARK 可以用来实现群组成员资格证明、私人空投、链上投票汇总等等的(关于这种 "电路指定 zkSNARK"转换的更多细节,请参阅 Vitalik 的帖子[1] 和 zCash 的文章[2]。
对于我们的目的,我们将专注于两个 ECDSA 操作:
(1)将私钥转换为公钥
(2)验证 ECDSA 签名
我们不需要实现签名生成本身,因为对签名的有效性进行的零知识验证与对签名的有效计算进行的零知识验证完成的是同样的事情。
ECDSA 协议需要几个协议参数:一个椭圆曲线方程,一个指定曲线上(x,y)坐标的素数,一个曲线上的生成元点,以及由生成元点生成的群阶。我们将对 secp256k1 曲线进行操作。具体来说,该曲线是由方程定义的:
其中256位素数 p 是 :
p=2256−232−29−28−27−26−24−1
secp256k1 标准还规定了生成元点 G 和相应群阶 n 的特定值。关于参数的更多细节,请查看文后链接 [3]。此外,以太坊依赖的加密安全哈希函数 keccak256,也是我们将要使用的,我们使用 Vocdoni 的 keccak circom 实现。

ECDSAPrivToPub
ECDSA 展示了如何基于一个给定的私钥来生成一个相应的可用于签名验证公钥。
私钥 d_A 是一个在 [1, n - 1] 范围内随机生成的整数,公钥被定义为 Q_A = d_A * G 。即,我们只需用椭圆曲线乘以标量,以及使基点 G 乘以私钥。这将组合使用椭圆曲线的点加和倍点来完成。

ECDSA 验证
用公钥 Q_A 签署一个信息 m , 会产生一个签名 (r, s)
现在假设我们得到了一个签名 (r, s) 和公钥 Q_A ,我们想检查这个签名是否有效。你可以在文后链接 [4] 中找到签名验证算法的完整描述。这个算法可被分解为以下操作:
  • 用 keccak256 加密信息 m
  • 模乘和模逆
  • 椭圆曲线乘以标量,以及椭圆曲线点加

电路和技术

现在我们对所涉及的运算有了很好的理解,我们进入了有趣的部分:把这些运算转化为算术电路。与使用程序化编程语言相比,在 circom 中编写电路既引入了额外的约束,也允许使用一些技巧来减少所需的计算量。我们将根据最终电路需要多少约束来衡量进展。
在算术电路中,约束条件只是一个 A * B + C = 0  形式的方程,其中 ABC  是信号的线性组合,在我们想要完成的事情中,它们可以被认为是变量。
产生一个满足约束的见证(中间值的变量赋值)是比较容易做到的,因为我们可以执行任何操作来产生见证,不仅仅是 +* 。然而,要确保约束是充分的就难得多了,因为我们的目标是构建电路,使输出信号成为输入信号的正确函数。换句话说,应该没有办法在不违反约束条件的情况下对数值进行操作。目前我们依靠审计代码和运行测试来验证这些电路的正确性。

BigInt 算术

BigInt 表示
首先,让我们找到一个合适的大整数的表示方法。回想一下,今天 zkSNARK 的证明系统通常使用素数为 254 位的椭圆曲线,这有效地将寄存器大小限制在 254-bit 。然而 secp256k1 的素数 p 是 256 位,这就造成了一点尴尬:
如果要做到原生的支持乘法,则应该将寄存器大小限制在最大 (254 - 1) / 2 = 126 -bit,因为两个 k 位数相乘的结果是 2k 位数,这将需要 ceil(256 / 126) = 3 个寄存器来表示 [0, p - 1] 中的整数。那么我们就浪费了 3 * 126 - 256 = 122 -bit,这基本上是一个完整的寄存器位数位。
鉴于我们已经使用 3 个寄存器,我们可以简单地选择位数为 ceil(256 / 3) = 86 -bit,这样则只是浪费了 3 * 86 - 256 = 2 -bit ,这是给定约束条件下我们能做的最好。

BigInt 比对/加法/减法
接下来,让我们假设 86-bit 寄存器继续向前,记住我们需要 3 个寄存器来表示我们关心的整数范围。我们将 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]  )
  • 等等... 
这些都可通过简单地合成 circom 中现有的 AND/OR 电路来完成。
对于加法,可以简单地将每一对寄存器 a[i] + b[i] 求和,然后用单独的信号模拟进位。
减法和加法类似,如果假设 a >= b ,则用一个借入信号计算 a[i] - b[i]

BigInt 乘法
通常,在有 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

BigInt 除法/模数
事实证明,在构建约束方面,除法只比乘法稍微复杂一点。
如果把 a / b 写成 (q, r) ,其中 q 是商, r 是余数,则只需约束 a = b * q + r0 <= 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)
再一次强调,我们可以在编写约束时把除法变成乘法。

ECC 操作

在这一节中,我们将讨论如何在一个非本机字段上将 BigInt 运算转变为椭圆曲线运算。下面的约束计数被应用到了我们的 v0.0.0 实现中 [5] 。在过去的几个月里,我们已经实现了进一步的优化,目前仍在整理和记录这些优化的过程中。

点加法和倍点
在传统的计算模型中,模逆比模乘要昂贵得多。因此,点加和倍点通常是以 Jacobian 形式进行的,它是以牺牲更多的模乘为代价来消除模逆运算。在我们的设置中,模逆和模乘消耗相同数量的约束,所以我们使用更有效的 Weierstrass 形式的标准公式。这个例子也能够说明 zkSNARKs 编程如何为性能优化创造一个新的机制。

标量乘法

实现 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

对于一个 256-bit 的私钥,在最坏的情况下,需要 256 个点加和点乘,以转化为 9037920 个约束。由于基点 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

这种缓存的倍点-加最多需要 256 个点加,无需倍点,将约束的数量减少到了 3733697 个。
在我们的实现中,我们使用缓存版的窗口法(Windowed Method) 来进一步减少点加的数量。也就是说,我们将私钥的二进制数字分成大小为 stride 的组,缓存 G 的倍数对应于一个组中二进制数为非零的数字,并将 G 表示为缓存的椭圆曲线点之和:

// Gpow[i][j] = j * (2 ** (i * stride)) * G for j = 1, ..., 2**stride - 1Gpow[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 和约束数量之间有如下关系。




当 stride 为 10 的时候,约束的数量似乎是最小的。我们在电路中使用的是 stride10 窗口法,有 416883 个约束条件,比我们的起始点减少了 20 多倍。

展望和未来的工作


进一步优化 R1CS 的表示大小
除了我们上面阐述的策略之外,还有一些机会可以进一步优化 R1CS 形式的电路。我们将简要地提到一种常见的技术:惰性约减(Lazy Reduction)。
在 ECC 运算期间,应用每个大整数算术运算之后,我们将大整数保持在 [0, p - 1] 范围内,并将所有寄存器保持在所需的范围内。
因此,我们有两种约减,它们对应于将这些值保持在范围内。但是我们可以跳过其中一些约减,并且仅在必要时进行约减,这通常称为惰性约减,因此实际上我们能够进一步减少约束的总数。在未来的博文中将更详细地介绍其中的一些优化措施。

新的证明系统 - PLONK
PLONK 的一个优点是它不需要为每个特定应用电路进行可信设置,这将大大减轻想要在现实世界中部署 ZK 应用程序的开发人员的负担。我们的 R1CS 形式的电路可以简单地转换为 PLONK 的形式,只需标准的加法门和乘法门。这种转换的一个限制是,加法门不像 Groth16 那样是免费提供的,因此电路可能次优的。
PLONKish 验证系统最近有许多发展,精心设计的定制门或查找表在实现 ECC 操作方面有非常有前途的结果(例如 Turbo Plonk)。我们还在考虑通过利用 PLONK 和 PLOOKUP 中的高效范围证明来优化我们的电路,并用更小的寄存器尺寸重新设计我们的电路。然而,我们希望的是提供一个在当前 circom 生态系统中实际有效的实现,因为它可以帮助开发人员更轻松地对新的 ZK 应用程序进行原型设计和开发。

基于配对的加密和其它原语
除了我们在这里使用的 secp256k1 曲线之外,还可以构建一个具有配对友好曲线的 ECC 模块,因为我们的大多数技术对于其它曲线也足够通用。另一个 0xPARC 团队将很快在 zkSNARK 中共享基于配对的加密实现,这可能会通向其他应用程序,如 BLS 签名。

电路审计和验证
正如我们在本系列的第一部分中所指出的,这些电路未经审计,目前不建议将它们用于生产。我们尽最大努力让见证测试覆盖大多数电路模块,但一般来说,仅靠见证测试是不够的。我们可以采取一些方法来解决此问题:
  • 开发更好的测试方法:目前我们只测试见证生成是否正确、是否满足电路约束,最好有一个能够捕获“未经约束的”漏洞的测试,例如文后链接 [6] 呈现的报告。
  • 应用形式验证:除了测试之外,形式方法也适用于我们的案例,因为我们电路的规格很容易用数学方法确定。
  • 提交专业审计:如果需求量大,我们也可以提交专业审计公司进行安全审计。

结语

我们邀请您查看我们的 repo,并希望听到任何反馈、改进/优化的想法或您想在此基础上构建的应用程序实验!
该项目是在 0xPARC 的 Applied ZK Learning Group #1 期间构建的,并在 0xPARC ZK ID 工作组期间进一步优化。
我们使用了来自 Vocdoni 的 keccak circom 实现;使用了来自 xJsnark 的大整数乘法优化,也使用了一些 circom 实用程序将 ECDSA 公钥转换为由 lsankar4033、jefflau 和 veronicaz41 实现的以太坊地址。


推荐阅读
[1] Vitalik 的帖子
https://vitalik.ca/general/2016/12/10/qap.html
[2] zCash 的文章
https://z.cash/technology/zksnarks/
[3] secp256k1 参数的更多细节
https://github.com/ethereum/py_ecc/blob/master/py_ecc/secp256k1/secp256k1.py
[4] 签名验证算法的完整描述
https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm#Signature_verification_algorithm
[5] circom-ecdsa v0.0.0
https://github.com/0xPARC/circom-ecdsa/releases/tag/v0.0.0
[6] 捕获“约束不足under-contrained”漏洞的测试
https://hackmd.io/@aztec-network/disclosure-of-recent-vulnerabilities




END





上一篇:ZK 机器学习
zCloak Network 是基于零知识证明技术的隐私计算服务平台,使用 zk-STARK 虚拟机为通用计算进行零知识证明的生成与验证。基于独创的自主权数据自证明计算技术,可以让用户在无需对外发送数据的情况下,实现对数据的分析和计算。项目采用“零知识证明即服务”的商业模式,打造一站式的多链隐私计算基础设施。zCloak 的首个产品 zkID.app 是一个链上数字身份与数据隐私保护解决方案。无论是企业、个人还是 IoT 设备,都可以借由 zkID 获得符合 W3C 国际标准的数字身份,并完成相关数字凭证的制备、公证、传递和验证等相关工作,整个过程由符合工业标准的密码技术保障其安全可靠。


原文出自 OxPARC,原文链见“阅读原文”
转载请注明原文与本文出处及翻译团队 zCloak Network

推荐阅读
  • des算法php,Des算法属于加密技术中的
    本文目录一览:1、des是什么算法2、80分求 ... [详细]
  • 【原创】利用Python进行河流遥感处理的PyRIS软件开发
    今天开始着手改造pyris1.0.文章地址:https:doi.org10.1016J.ENVSOFT.2018.03.028Monegaglia,2 ... [详细]
  • 本文介绍了闭包的定义和运转机制,重点解释了闭包如何能够接触外部函数的作用域中的变量。通过词法作用域的查找规则,闭包可以访问外部函数的作用域。同时还提到了闭包的作用和影响。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 如何用Matlab快速画出带有3D渲染效果的复杂曲面
    简要地介绍了一下如何用Matlab快速画出带有3D渲染效果的复杂曲面图,包括三维曲面绘制、光线、材质、着色等等控制,以及如何 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • 开发笔记:计网局域网:NAT 是如何工作的?
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了计网-局域网:NAT是如何工作的?相关的知识,希望对你有一定的参考价值。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 一句话解决高并发的核心原则
    本文介绍了解决高并发的核心原则,即将用户访问请求尽量往前推,避免访问CDN、静态服务器、动态服务器、数据库和存储,从而实现高性能、高并发、高可扩展的网站架构。同时提到了Google的成功案例,以及适用于千万级别PV站和亿级PV网站的架构层次。 ... [详细]
  • LVS实现负载均衡的原理LVS负载均衡负载均衡集群是LoadBalance集群。是一种将网络上的访问流量分布于各个节点,以降低服务器压力,更好的向客户端 ... [详细]
  • 假设我有两个数组A和B,其中A和B都是mxn.我现在的目标是,对于A和B的每一行,找到我应该在B的相应行中插入A的第i行元素的位置.也就是说,我希望将np.digitize或np. ... [详细]
  • 1print过程procprint<data数据集名><选项>;*label指定打印输出标签noobs制定不显示观测序号*by变量名1< ... [详细]
  • Aptos 生态最全盘点:哪些 DeFi 项目值得关注?
    本文将从Aptos生态挑选头部DeFi项目,详解其运行机制、创新点、完成度等。撰文:Mabrary ... [详细]
  • NN,NearestNeighbor,最近邻KNN,K-NearestNeighbor,K最近邻KNN分类的思路:分类的过程其实是直接将测试集的每一个图片和训练集中的所有图片进行比 ... [详细]
author-avatar
单色设计
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有