作者: @mmjahanara , @pierre
感謝@b-wagn 、 @soispoke和@levs57的有益討論和評論,促成了本文的撰寫。
簡而言之,我們探討了蟲洞的“似是而非的可否認性”為何似乎與以太坊的 160 位地址不兼容。此外,我們還闡明瞭 EIP-7503 聲稱的匿名集遠低於實際預期。雖然後一個問題應該可以解決,但我們認為前一個問題可能需要重新思考蟲洞的整體架構。文章最後簡要介紹了一種使用信標鏈存款的潛在後續設計方案。
關於蟲洞
以太坊的隱私解決方案歷來依賴於特定應用的匿名集。在諸如Tornado Cash之類的協議中,存款行為是與特定智能合約的顯式交互。這造成了一個根本性的缺陷:雖然存款人和取款人之間的聯繫被切斷,但參與隱私協議的行為卻是公開的。這些設計使得鏈上觀察者可以標記並可能審查所有匿名集中的存款人。
EIP-7503(零知識蟲洞)提出了一種不同的範式,依賴於可信的否認。
該機制很簡單:用戶通過將資金髮送到一個由加密技術確定的不可花費地址來“銷燬”資金。之後,他們提供一個(非交互式)零知識證明(NIZK),證明他們知道該地址的原像,以便在新的賬戶中重新鑄造這些資金。關鍵在於,“存款”(或“銷燬”)與向新地址的標準以太坊轉賬沒有任何區別。
然而,我們認為可信的否認性和安全性之間可能存在根本性的權衡,這可能會阻礙蟲洞目前在L1層中的應用。此外,我們還指出EIP-7503的有效匿名集可能低於預期,並對此問題進行了探討。
符號和假設
我們將使用:
- \mathsf{H}(\cdot) H ( ⋅ )表示通用的 256 位哈希函數(例如,SHA3、SHA-256 等)。
- \operatorname{trunc}_{160}(x) trunc 160 ( x )將 256 位值x x截斷為 160 位以太坊地址。
- 域分隔哈希為\mathsf{H}(\text{“TAG”} \parallel \cdots) H ( “TAG” ∥ ⋯ ) ,其中\text{“TAG”} “TAG”是一個固定的 ASCII 前綴(例如\text{“worm”} “worm” , \text{“null”} “null” )。
EIP-7503
在最初的 EIP-7503 規範中,銷燬地址是由單個密鑰s派生而來的:
第一步,用戶通過隨機選擇一個 s並將其發送到Addr_{burn}(s) A d d r b u r n ( s )來銷燬其資金。之後,用戶可以通過向智能合約提供一個無效化符ν = \mathsf{H}(\text{“null”} \parallel s) ν = H ( “null” ∥ s )和一個 NIZK 證明來增發資金,該證明需證明ν與某個現有交易A\to B A → B一致(即,使用s s st B = Addr_{burn}(s) B = A d d r b u r n ( s ) )。合約驗證該證明並檢查ν ν是否已被提交過。如果這些檢查都通過,則增發資金。
這種銷燬機制的安全特性並不明確,所以讓我們非正式地定義一下。
正確性/完整性
如果用戶行為誠實,那麼燃燒後可能會產生收益。
隱私
- 不可鏈接性
直觀地說,這裡的隱私意味著除了用戶本人之外,任何人都無法將銷燬交易與鑄幣過程(即提交給鑄幣合約的鑄幣交易)關聯起來。更準確地說,我們假設用戶在這種情況下是誠實的,並且希望鏈上的任何觀察者都無法分辨出哪個交易是與鑄幣交易對應的銷燬交易。
- 合理否認
我們希望實現的第二個隱私屬性是,銷燬交易應該看起來像一筆“常規”交易。具體來說,銷燬地址B應該看起來像一個隨機地址。這種可否認性非常重要,也是它與需要顯式合約交互的 Tornado Cash 等協議的主要區別之一。使用蟲洞,參與銷燬協議的過程是私密的——但請注意,提現操作仍然是公開的。
非通貨膨脹
我們不希望以太幣憑空產生,這意味著只有在銷燬之後才能再次鑄造:任何成功的鑄造交易到銷燬交易的映射都應該是單射的。乍一看,這意味著要防止攻擊者為同一筆銷燬交易生成兩個不同的無效化符。事實上,如果攻擊者能夠為同一筆銷燬交易生成兩個不同的無效化符,我們就會導致通貨膨脹漏洞。
既然\mathsf{H}(\cdot) H ( ⋅ )隱藏起來了,那豈不是很簡單嗎?嗯,正如Scroll 團隊很早就注意到的那樣,事情的難點就在這裡。
生日悖論
生日悖論意味著,找到任意兩個秘密s_1, s_2,使得它們的160位地址衝突
大約需要2^{80} 2 80 次運算。雖然數量龐大,但對於國家級黑客組織或大型礦池來說,這樣的運算量仍在可控範圍內。當攻擊者發現此類碰撞時,協議就會失效:
- 一次性向衝突地址存入 100 ETH。
- 使用無效化符\nu_1=\mathsf{H}(\text{"null"} \parallel s_1) ν 1 = H ( "null" ∥ s 1 )和\nu_2=\mathsf{H}(\text{"null"} \parallel s_2) ν 2 = H ( "null" ∥ s 2 )進行鑄造。這是兩個不同的無效化符,因此 100 ETH 被鑄造了兩次。
- 提取 200 ETH。
由於驗證器僅檢查每個無效化符是否被使用過一次,因此它無法判斷這兩次提款是否由同一筆鏈上存款支持。這是一個無限膨脹漏洞:在2^{80}生日邊界處發生的 160 位地址衝突會直接導致雙倍鑄幣。
然而,需要注意的是,擁有更大地址空間的鏈不會面臨這種安全缺陷:使用完整的 320 位地址,該方案的安全性將令人滿意。以太坊的 160 位截斷是一種優化,理論上它可以擺脫這種限制。
暫定方案:移除衝突搜索,並使用 dlog 使方案更難被破壞?
讓我們提出一個想法:我們能否繞過生日悖論,不要求攻擊者找到碰撞,而是要求他破壞 dlog?
假設我們對語句(\mathcal{R}_{root}, \nu) ( R r o o t , ν )和見證(sk,s_{worm},tx = (A,B,pk),\text{salt},\pi_{merkle}) ( s k , s w o r m , t x = ( A , B , p k ) , salt , π m e r k l e )進行如下 NIZK 運算,並滿足以下約束:
(1) \pi_{merkle} π m e r k l e驗證tx t x在\mathcal{R}_{root} R r o o t 中;
(2) pk == \mathsf{SkToPk}(sk) p k = = S k T o P k ( s k ) ,即, pk = sk \cdot G p k = s k ⋅ G對於 ECDSA 密鑰;
(4) B == \operatorname{trunc}_{160}\!\big(\mathsf{H}(\text{"worm"} \parallel sk \parallel \text{salt})\big) B == trunc 160 ( H ( "worm" ∥ s k ∥ salt ) ) ;
(5) \nu == \mathsf{H}(\text{"null"} \parallel sk \parallel tx). ν = = H ( "null" ∥ s k ∥ t x ) 。
這樣的設置就太好了:用戶甚至可以在兩個不同的銷燬交易中使用相同的鹽值時重複使用其銷燬地址!
安全性論證也應該很直接:雙重鑄幣現在需要破解 NIZK、ECDSA 或用於ν ν的哈希,而不是利用2^{80} 2 80成本的地址碰撞。事實上,對於固定的銷燬交易(因此A、pk 、 B也是固定的) ,只有一個有效的sk s k ,否則你就可以找到兩個不同的私鑰映射到發起該交易的同一個公鑰。因此,所有碰撞的見證都映射到同一個無效化符。
這就是解決方案嗎?
其實不然,因為防止無效化碰撞並不是唯一的問題!
蟲洞機制也應防止銷燬地址被花費。正如最初的 EIP-7503 所述,攻擊者可以找到一對密鑰(s_1, s_2) ,使得create2_address ( ... , s1 create2_address(..., s1) == sha256("wormhole" || s2) 。在這種情況下,攻擊者可以將資金銷燬到sha256("wormhole" || s2) ,然後從create2_address(..., s1)提取這些資金。雖然這意味著攻擊者只能提取兩次以太幣,但這仍然應被視為攻擊失敗。
但考慮到我們上面更新的電路,我們真的需要擔心嗎?既然我們的設計需要攻擊者破壞 dlog,我們難道不能簡單地要求在銷燬資金時EXTCODESIZE不為0嗎?這樣就能防止攻擊者從create2_address(..., s1)中取出銷燬的資金。
問題在於,首先就應該意識到上述方案需要攻擊者破解 dlog。事實上,無論是否掌握私鑰,攻擊者控制銷燬地址的策略與使用CREATE2操作碼的策略幾乎相同。
在我們的設置中,為了使銷燬地址可以花費,遊戲規則如下:如果攻擊者能夠找到兩個秘密(s_1, s_2) ( s 1 , s 2 ) ,使得trunc160(H(\text{“worm”} || s_1)) == trunc160(H(skToPk(s_2))) tr u n c 160 ( H ( “ worm ” | | s 1 ) ) = = tr u n c 160 ( H ( sk To P k ( s 2 ) ) ) ,則他獲勝。事實上,在這種情況下,攻擊者最終會得到一個可花費的銷燬地址,因為找到這樣的對可以讓攻擊者發起從trunc160(H(skToPk(s_1))) t r u n c 160 ( H ( sk To P k ( s 1 ) ) )到trunc160 (H(\text{“worm”} || s1)) t r u n c 160 ( H ( “worm” | | s 1 ) )的交易,該地址由s_2 s 2控制!
我們又回到了原點:這仍然是一個碰撞搜索問題,只是複雜度只有 80 位。
如今那些看似可信但實際上不可否認的蟲洞需要具備一定的抗碰撞能力。
忽略檢測啟發式方法,蟲洞的合理否認性要求意味著,任何觀察L1層的人都無法區分蟲洞交易和常規交易。這一特性自然要求銷燬地址的計算過程對發起銷燬的用戶保密。但是,由於抗碰撞機制的存在,用戶無法以超過80比特的安全級別證明,他們用於生成160比特私有銷燬地址的密鑰不會同樣控制該地址。
一種解決辦法是將銷燬地址直接寫入協議本身(例如,將資金髮送到trunc160(H ( " worm " ) ) ) ,從而將問題轉化為原像搜索任務。但這種設置會讓我們失去所尋求的合理否認性。最終,我們不得不在安全性較弱但合理否認的設計和幾乎等同於 Tornado Cash 的另一種設計之間做出選擇。
我們在此列出三個我們認為對改善蟲洞現狀有價值的問題:
- 我們是否可以利用其他原始技術,在L1層中構建看似可信卻又難以否認的蟲洞?我們認為答案是肯定的,並正在著手研究如何利用信標存金來實現這一目標。以下附錄將簡要闡述這一想法。
- 是否有可能利用原像搜索問題構建蟲洞,並實現合理的否認?我們對此並不確定,使用確定性但私密的銷燬地址似乎相當困難。
- 是否存在一些容易實現的 L1 更改,可以讓我們肯定地回答上述兩個問題中的一個?
附加組件
EIP-7503 有效匿名集
我們有一個實際問題,不確定是否已經得到充分討論。因此,我們在此說明,以求清晰明瞭。
EIP-7503 將beacon_block_root作為鑄幣證明的公開輸入。但此舉將聲稱的匿名集(所有零交易記錄的以太坊賬戶)縮小到該區塊內發生的交易,因為相應的銷燬交易必須位於被引用的區塊中。如果蟲洞協議決定使用交易收據根作為用戶鑄幣證明的輸入,則要保持足夠大的匿名集,就需要訪問一個能夠獲取所有先前交易認證路徑的累加器。
雖然理論上這應該可行,因為每個區塊根哈希都是對該區塊之前所有交易的承諾,但此證明要麼需要高效的客戶端遞歸 snark,要麼需要特定的 L1 設置——例如用易於算術運算的哈希替換 keccak,或者用一個字段擴展區塊頭,該字段承諾到包含所有先前區塊哈希的 merklized 區塊根。
後續工作:蟲洞與信標沉積物相遇
信標存款合約是執行層與信標鏈之間的單向橋樑。目前部署的方案是,每個驗證器都可以公開鏈接到其存款和取款目的地:該合約公開了一個deposit函數,該函數接受存款以及驗證器pubkey和取款憑證( withdrawal_credentials )。
或許可以設計一個私有版本的合約,用相應的隱藏承諾替換pubkey和withdrawal_credentials 。存款隨後可以通過在信標鏈上提供零知識證明來領取。在這種架構中,驗證者只有在存款在信標鏈上被領取後才會被添加到激活隊列。需要注意的是,未領取的存款仍然可以像任何常規驗證者一樣添加到取款隊列中。
這需要對協議進行一些架構重構,但它可以顯著提升驗證者的隱私性,同時還能作為一種協議內類似蟲洞的機制,提供合理的否認能力。此外,考慮到大約三分之一的以太坊已被質押,且每日提現額高達數千萬美元(甚至更多),由此產生的匿名集規模可能相當龐大。
