缺失的零:Payy 跨鏈橋如何接受偽造的 ZK 證明,損失 192 萬美元)
一個(gè)缺失的零Payy 跨鏈橋如何接受偽造的 ZK 證明損失 192 萬美元2026 年 9 月 24 日Payy 在攻擊者從其以太坊跨鏈橋合約中提走 1,918,793.198148 USDC 之后暫停了整個(gè)網(wǎng)絡(luò)。最容易講的故事是私鑰被盜。但公開證據(jù)指向別處。Payy 已經(jīng)排除了私鑰泄露、社會(huì)工程攻擊以及鏈下基礎(chǔ)設(shè)施被利用的可能。攻擊者提交了一筆背后沒有任何資金支撐的提現(xiàn)并附上一份偽造的零知識(shí)證明。Payy 的驗(yàn)證器接受了這份證明。隨后又發(fā)生了兩筆金額較小的偽造提現(xiàn)。根因是 Barretenberg 小群 IPA 檢查中的一個(gè)可靠性漏洞。所謂可靠性漏洞就是讓驗(yàn)證器接受一個(gè)虛假陳述的證明。這個(gè)檢查沒有強(qiáng)制某個(gè)累加和從零開始。用軟件的話說驗(yàn)證器約束不足。這才是真正的問題所在。不是“ZK 壞了”而是一條約束缺失、部署的驗(yàn)證器版本過舊、升級(jí)路徑又太慢。攻擊路徑一步一步看Payy 的余額是隱私 note。一筆交易最多花費(fèi)兩個(gè)輸入 note創(chuàng)建最多兩個(gè)輸出 note鏈上只公開它們的承諾也就是代表隱藏 note 的指紋。burn 是 Payy 對(duì)“提現(xiàn)到以太坊”的術(shù)語(yǔ)。它還會(huì)公開金額、以太坊收款地址和 burn hash而電路會(huì)把 burn hash 設(shè)為第一個(gè)輸入承諾。第一筆偽造 burn 公開了四個(gè)零承諾、一個(gè)零 burn hash、金額 1,828,589,378,132 個(gè)基礎(chǔ)單位以及攻擊者地址。USDC 有六位小數(shù)所以這是 1,828,589.378132 USDC。四個(gè)零承諾就是填充 note價(jià)值為零。交易前后 note 樹的根完全一樣。如果直接拿這筆交易去跑 Payy 的 burn 電路它會(huì)失敗。價(jià)值守恒斷言會(huì)拒絕一筆由零價(jià)值 note 支撐的正數(shù)提現(xiàn)。note 類型斷言也會(huì)失敗因?yàn)樘畛?note 的類型是 0而消息里寫的是 USDC note 類型。Payy 不會(huì)對(duì)每筆提現(xiàn)重跑電路它依賴的是“這筆交易遵守了電路規(guī)則”的密碼學(xué)證明。攻擊者的偽造證明通過了 Payy 的驗(yàn)證器。Payy 的事后報(bào)告把這一點(diǎn)歸因于 Barretenberg 的可靠性漏洞。Payy 節(jié)點(diǎn)內(nèi)部的驗(yàn)證器首先接受了這份證明。Payy 的第一層聚合電路會(huì)用編譯進(jìn)電路的同一個(gè) Barretenberg 驗(yàn)證器再驗(yàn)證每筆交易證明而上層只驗(yàn)證下一層。出于隱私考慮沒有任何一層能看到 note 的價(jià)值所以沒有任何一層能獨(dú)立重做余額檢查。Payy 自己的 prover 提交了這批交易Payy 的驗(yàn)證者簽了名rollup 合約隨后支付了 burn。rollup 合約沒有“burn hash 必須非零”這條規(guī)則。這條規(guī)則本可以攔下這三條消息但它治不了驗(yàn)證器的病。時(shí)間線與損失第一筆偽造 burn 在 UTC 03:25:41 被記錄在 Payy 自己的鏈上高度 33,195,439。UTC 04:21:23Payy 的 prover 在以太坊區(qū)塊 26,044,909 中結(jié)算了這批交易。跨鏈橋把 1,828,589.378132 USDC 轉(zhuǎn)給 0xAa4985dBDaBfACa344237D40F7E06C4a0BB57E70。八分鐘內(nèi)資金進(jìn)入第二個(gè)錢包并賣出換成約 683 ETH。UTC 04:21:47一個(gè) burn 墊付方支付了攻擊者的第二筆 burn金額 1 USDC。墊付方會(huì)提前支付提現(xiàn)等提現(xiàn)結(jié)算后再收回款項(xiàng)??珂湗虼蠹s五小時(shí)后償還了它。這兩筆轉(zhuǎn)賬里只有一筆是損失。UTC 09:30:35第二筆結(jié)算在區(qū)塊 26,046,439 中向同一地址支付了 90,202.820016 USDC并償還了墊付方??倱p失1,828,589.378132 1 90,202.820016 1,918,793.198148 USDC。UTC 14:04Payy 宣布遭到攻擊并暫停所有網(wǎng)絡(luò)交易。Payy 追蹤到第一筆付款中的 682.3 ETH 在 9 月 26 日通過 19 筆存款進(jìn)入 Tornado Cash。截至 10 月 11 日最后那筆 90,202.82 USDC 仍在收款錢包里。截至 10 月 11 日沒有任何公開信息表明 Payy 已恢復(fù)運(yùn)行。其公開倉(cāng)庫(kù)默認(rèn)分支自 6 月 24 日以來沒有新提交。代碼問題一個(gè)方程兩個(gè)未知數(shù)公開的重構(gòu)資料描述了 Payy 在提交 2f95947f 中 burn 分支的四條斷言。關(guān)鍵邏輯簡(jiǎn)化后大致如下。這不是生產(chǎn)環(huán)境的精確代碼但能看出檢查的形狀。// Payy burn 分支的簡(jiǎn)化模型非生產(chǎn)代碼。fnburn_circuit(inputs:[Note;2],outputs:[Note;2],msg:BurnMsg){letinput_valueinputs[0].valueinputs[1].value;letoutput_valueoutputs[0].valueoutputs[1].value;assert_eq!(input_value,output_valuemsg.value);// 價(jià)值守恒assert_eq!(msg.burn_hash,inputs[0].commitment);// burn 綁定第一個(gè)輸入assert!(msg.value0);// 提現(xiàn)金額為正assert_eq!(inputs[0].kind,msg.kind);// note 類型匹配消息}用四個(gè)填充 note 時(shí)input_value和output_value都是零。正數(shù)的msg.value會(huì)通不過價(jià)值守恒。類型斷言同樣會(huì)失敗。在這個(gè)故事里電路本身不是弱點(diǎn)驗(yàn)證器才是。Barretenberg 的小群 IPA 檢查會(huì)對(duì)一個(gè)累加和做記賬。prover 聲稱一組數(shù)字的總和驗(yàn)證器要在看不到這些數(shù)字的情況下確認(rèn)總和。規(guī)則很簡(jiǎn)單累加和必須從零開始。每個(gè)值必須等于前一個(gè)值加上列表中的下一個(gè)數(shù)。最后一個(gè)值必須等于聲稱的總和。舊檢查把前兩條規(guī)則揉進(jìn)了一個(gè)方程。一個(gè)方程無法釘死兩個(gè)未知數(shù)。惡意 prover 可以讓累加和從錯(cuò)誤的值開始并讓第一步錯(cuò)出恰好抵消的量。之后每一步都是誠(chéng)實(shí)的之后每個(gè)值都偏離同一個(gè)常數(shù)總和也一樣。這個(gè)常數(shù)由 prover 選總和也就由 prover 選。簡(jiǎn)化模型能看出這個(gè)漏洞。// 簡(jiǎn)化模型不是 Barretenberg 的實(shí)際代碼。// 舊檢查把兩個(gè)義務(wù)揉進(jìn)了一個(gè)等式。fnold_check(a:[Fr],t:[Fr],s:Fr,g:Fr)-bool{a[0]g*(a[1]-a[0]-t[0])Fr::ZERO(1..t.len()).all(|i|a[i1]a[i]t[i])a[t.len()]s}// 正確的檢查起始條件單獨(dú)約束。fnfixed_check(a:[Fr],t:[Fr],s:Fr)-bool{a[0]Fr::ZEROa[1]a[0]t[0](1..t.len()).all(|i|a[i1]a[i]t[i])a[t.len()]s}舊驗(yàn)證器在這個(gè)步驟里檢查 prover 承諾值的四條斷言。修復(fù)后的代碼檢查五條多出來的就是a[0] 0。效果不是表面功夫。一個(gè) Lean 4 模型用 Lean 4.34.1 和 Mathlib 驗(yàn)證了兩條性質(zhì)。文件里沒有sorry除 Lean 三條標(biāo)準(zhǔn)公理外不依賴其他公理。定理和證明一共 57 行。第一條定理表明當(dāng)位置數(shù)至少為二且g ! 0時(shí)舊檢查對(duì)任意數(shù)字列表和任意聲稱的總和都會(huì)接受錯(cuò)誤的總和。第二條表明當(dāng)g ! 1時(shí)修復(fù)后的檢查會(huì)強(qiáng)制被接受的總和等于真實(shí)總和。這并不能證明整個(gè) Barretenberg 是可靠的。它只證明了一個(gè)檢查在兩條假設(shè)下的性質(zhì)承諾把 prover 綁定到一個(gè)累加和上且抽查都得到誠(chéng)實(shí)回答在隨機(jī)點(diǎn)上通過一個(gè)組合方程意味著除可忽略概率外三條等式在每個(gè)位置都成立。這兩條假設(shè)把模型和部署的驗(yàn)證器連起來但它們不覆蓋 C 代碼、Solidity 驗(yàn)證器、Payy 的電路或 Payy 的合約。不過它們說明按模型來看舊檢查確實(shí)會(huì)接受錯(cuò)誤總和而修復(fù)移除了那一個(gè)自由度。Payy 的復(fù)現(xiàn)包包含那份 16,256 字節(jié)的證明和公開輸入。根據(jù)事后報(bào)告這份證明在 Payy 當(dāng)時(shí)運(yùn)行的構(gòu)建下能通過驗(yàn)證。把公開的 burn hash 從 0 改成 1同一份證明就會(huì)失敗。這確認(rèn)了驗(yàn)證器會(huì)拒絕被修改的輸入但并不能說明攻擊者最初是怎么構(gòu)造出那份證明的。修復(fù)為什么沒進(jìn)生產(chǎn)Payy 把服務(wù)鎖定在標(biāo)記為3.0.0-manual.20251030的 Barretenberg 構(gòu)建上配套 Noir1.0.0-beta.14。Cargo.toml里有一行、六個(gè) Dockerfile 都指向這個(gè)構(gòu)建。Aztec 在 2026 年 5 月 14 日提交了修復(fù)。提交號(hào)58bf73a標(biāo)題fix!: small subgroup ipa bug (#44)。它改了 34 個(gè)文件包括原生驗(yàn)證器、遞歸驗(yàn)證器和 Solidity 驗(yàn)證器模板。Aztec 在 2026 年 7 月 13 日的 v5.0.0 中發(fā)布了修復(fù)。6 月的兩個(gè) v5 候選版本不含此修復(fù)6 月 27 日發(fā)布的 v4.4.0 也不含。Payy 被攻擊發(fā)生在 2026 年 9 月 24 日距離含修復(fù)的公開版本發(fā)布已經(jīng) 73 天。Payy 在 2026 年 5 月 15 日部署了一個(gè)新的鏈上驗(yàn)證器用來修復(fù)自己某個(gè)電路里的另一個(gè)嚴(yán)重漏洞。合約創(chuàng)建時(shí)間比 Aztec 提交記錄的時(shí)間晚 20 小時(shí) 49 分鐘。事件報(bào)告說這個(gè)合約仍由 2025 年 10 月的構(gòu)建生成。一次全新的部署用的還是舊驗(yàn)證器。Payy 最后一次公開倉(cāng)庫(kù)同步在 6 月 24 日仍指向 2025 年 10 月的構(gòu)建。5 月 15 日部署的驗(yàn)證器到 9 月 24 日仍在使用。對(duì) Payy 這樣的 rollup 來說升級(jí) Barretenberg 不是改一行依賴那么簡(jiǎn)單。修復(fù)落在一個(gè)新的大版本里。驗(yàn)證器被編譯進(jìn) Payy 的每一層聚合電路電路、驗(yàn)證密鑰和鏈上驗(yàn)證器合約必須一起重新生成、一起重新部署。Aztec 3 月 10 日的安全公告說修復(fù)會(huì)打包進(jìn) v5 發(fā)布對(duì)于需要新驗(yàn)證密鑰的嚴(yán)重漏洞會(huì)先通知其網(wǎng)絡(luò)門戶portal的運(yùn)營(yíng)者再在論壇發(fā)公告。Payy 不在那個(gè)網(wǎng)絡(luò)上它把 Noir 和 Barretenberg 當(dāng)作庫(kù)用在自己的 rollup 上。一個(gè)庫(kù)如果有廠商網(wǎng)絡(luò)之外的用戶就需要一條能觸達(dá)這些用戶的渠道。到 7 月 13 日修復(fù)已經(jīng)公開提交還給協(xié)議文檔加了一節(jié)明確寫出這種偽造方式。但 Aztec 倉(cāng)庫(kù)里沒有任何安全公告點(diǎn)名這個(gè)漏洞。10 月 8 日Aztec 針對(duì)另一個(gè) Barretenberg 驗(yàn)證器漏洞發(fā)布了兩份安全公告并建議“直接使用 Barretenberg 的用戶”打補(bǔ)丁。直接用戶需要的是事前通知不是事后通知。該改什么升級(jí)到包含58bf73a或之后提交的 Barretenberg 版本。重新生成電路、驗(yàn)證密鑰和鏈上驗(yàn)證器合約把它們作為一次協(xié)調(diào)好的發(fā)布一起重新部署。效果補(bǔ)上缺失的約束恢復(fù)“被接受的總和必須等于真實(shí)總和”這條性質(zhì)。不要把合約層的burnHash ! 0檢查當(dāng)成修復(fù)。它能攔下這三條消息但驗(yàn)證器仍然會(huì)接受偽造證明。把它當(dāng)縱深防御加上去而不是根因治理。給單筆提現(xiàn)加上限對(duì)大額提現(xiàn)加延遲。效果一份偽造證明無法在一次結(jié)算里掏空整個(gè)跨鏈橋團(tuán)隊(duì)也有時(shí)間暫停和調(diào)查。Aztec 3 月 10 日的公告給其網(wǎng)絡(luò)門戶建設(shè)者的也是這個(gè)建議。給依賴項(xiàng)建立驗(yàn)證器變更監(jiān)控。對(duì)你編譯進(jìn)生產(chǎn)的每一套證明棧都要讀上游的安全提交和發(fā)布說明。鎖版本可以但別鎖完就不管。效果可靠性修復(fù)會(huì)變成一張可執(zhí)行的工程工單而不是出事之后才看到的博客文章。保持一條能在幾天內(nèi)完成的電路、密鑰和驗(yàn)證器合約重建路徑。如果某個(gè)嚴(yán)重驗(yàn)證器修復(fù)需要新大版本和協(xié)調(diào)重部署那就在需要之前先演練一遍。效果上游發(fā)布到生產(chǎn)部署之間的窗口會(huì)縮短。對(duì)關(guān)鍵驗(yàn)證器檢查使用形式化模型。57 行的 Lean 模型本身發(fā)現(xiàn)不了這個(gè)漏洞但它把檢查必須滿足的性質(zhì)寫清楚了。對(duì)舊檢查來說這條性質(zhì)是假的對(duì)修復(fù)后的檢查來說在給定假設(shè)下它成立。效果代碼評(píng)審有了一個(gè)精確的問題要回答而不是“證明系統(tǒng)大概沒問題”的模糊感覺。在隱私允許的地方加獨(dú)立不變量。非零 burn hash、每批最大 burn 額度、驗(yàn)證器版本白名單這些都不是可靠性的替代品而是爆炸半徑控制。效果它們限制單次驗(yàn)證器失效能造成的后果??偨Y(jié)Payy 這件事的核心不是私鑰被盜。它是一個(gè)驗(yàn)證器因?yàn)樯倭艘粭l約束而接受了虛假陳述也是一個(gè)生產(chǎn)系統(tǒng)在修復(fù)公開 73 天后仍在運(yùn)行 2025 年 10 月的構(gòu)建。缺的那一行很小流程上的失敗很大。如果你的系統(tǒng)接受證明你跑的不是電路你信的是驗(yàn)證器。把驗(yàn)證器當(dāng)生產(chǎn)安全代碼對(duì)待盯住上游每一次變更保持重部署路徑隨時(shí)可用。還要記住在證明系統(tǒng)里一個(gè)缺失的零可以值 192 萬美元。