这篇文章指出了 Circom 中 assert() 语句的一个常见误解:它只在电路编译和 witness 生成时起作用,而不会在电路中添加实际的约束。作者通过一个防止双重花费的例子说明了这一问题,并提供了正确的约束实现方式,强调了理解零知识电路基础概念的重要性。

本文旨在让程序员意识到 Circom 的 assert() 语句的误用,而不是解释零知识或 zk-SNARKs 的概念,网上有很多精彩的学习材料可供参考。
TL;DR
断言不添加任何约束
最近,我们偶然发现了 Circom 的 assert() 语句,它们在一个实现基于 UTXO 的乐观隐私保护 L2 项目中被用来强制执行某些约束。遗憾的是,它并非一个神奇的工具,让程序员可以绕过表达某些非平凡约束的复杂性。
零知识证明和 ZK 电路
简短版本:零知识证明允许在不泄露除了声明为真之外的任何信息的情况下,证明一个声明是真实的。
ZK 电路,用于 zk-SNARKs 和 zk-STARKs,编码定义了电路输入和输出之间某些关系并保持这些关系的约束。并非所有关系都可以通过 zk-SNARKs(我们正在查看的项目使用的证明系统)来证明,它们必须具有二次算术程序 (QAP) 表示,关于 QAP 的更多信息请参见 Vitalik 的这篇文章。
什么是 Circom?
Circom 是一个电路编译器,提供了一种领域特定语言 (DSL),它抽象了构建零知识证明所需的复杂多项式。它是目前 ZK 电路最广泛使用的 DSL 之一。
Circom 允许程序员定义所谓的 templates,它们是电路的构建块。程序员为电路定义 input 和 output signals,并在 template 中编写约束,例如,将电路输入 x 约束为零表示为 x === 0。在 Circom 中,约束表示为 a === b,而带赋值的约束表示为 a <== b。
编写可以被证明 (QAP) 的约束可能很棘手,尤其是在想要证明“某物不是”的情况下。让我们以一个不希望有任何重复项的数组为例,在我们的乐观 L2 中,这需要防止在同一事务中发生双重支付。想法是:
signal input array[N];
for (var i = 0; i < N; i++) {
for (var j=i+1; j < N; j++) {
// 我们希望约束 array[j] == 0 或者 array [i] != array[j]
}
}
为该约束实现的解决方案是 assert(array[j] == 0 || array[i] != array[j]),乍一看似乎是正确的。但是如果我们仔细研究一下这个有吸引力的 assert() 语句的作用,它就会变成一个怪物。根据 Circom 的文档,断言只在电路编译和 witness 生成过程中起作用。
断言不添加任何约束到电路中,它们只充当输入清理。让我们以电路 A 和 B 为例,这两个电路是相同的,都将 signal x 作为输入,唯一的区别是电路 A 的 circom 代码包含 assert(x != 0)。可以从 B 生成一个证明并提供 x = 0,这个证明在由 A 构建的验证器验证时将是有效的。
在我们的乐观 L2 中,后果是双重支付在事务内部成为可能,并且系统不允许挑战自身的事务。
必须添加一个适当的约束。这里的一个可能解决方案是使用 circomlib 中的模板:
var res = OR() (IsZero() (array[j]), NOT() (IsEqual() ([array[i], array[j]])));
1 === res
虽然在新领域(如零知识电路)中很容易错误地假设某些行为,但这个问题强调了理解基本概念的重要性。
汲取智能合约早期阶段的教训,我们必须努力不重蹈覆辙,并从一开始就认真对待安全问题。
circom 文档可能会更清楚地传达这一关键警告。广泛的测试可能能够捕获这种意外行为。
ChainSecurity 是历史最悠久、最受信任的智能合约审计公司之一。我们的团队自 2017 年以来一直进行智能合约审计,并受到 许多长期合作伙伴 的信任,例如蓝筹 DeFi 协议、有前景的 Web3 项目和中央银行部门。
你是否有兴趣对你的电路进行审计并提高你的 ZK 项目的安全性?我们很乐意通过 contact@ChainSecurity.com 收到你的来信。
本文涵盖的主题
Circom, Circom assert, 零知识电路安全, zk circuits audit, 网络安全, 区块链安全, 漏洞缓解
- 原文链接: chainsecurity.com/blog/c...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!