感謝 Jochem Brouwer、Sina Mahmoodi、Thomas Thiery、Rahul Guha、Carlos Perez 和 Guillaume Ballet 的審閱和反饋。
TL;DR:
- 這篇文章從廣闊的視角介紹了涉及 L1 snarkification 的一個眾所周知的最壞情況的解決方案:字節碼分塊。
- 即將到來的 Layer-1 擴容和其他協議變更會加劇這種最壞情況。雖然這些變更必須繼續,但重要的是開始思考如何應對未來的 SNARKification 挑戰。
- 隨著時間的推移和以太坊的發展,核心開發人員和研究人員對於重大協議變更所帶來的風險的意見越來越多樣化,因此深入探索每個潛在解決方案的細微差別非常重要。
- 本文檔的目標是:
- 調查該問題的潛在解決方案,從最簡單到最複雜,突出每種解決方案的不同權衡。
- 提高認識,鼓勵更多人考慮它並提出更好的解決方案。
- 這並不需要立即採取行動(例如“Glamsterdam”)——即使是看似簡單的解決方案也需要徹底的分析來確定最佳的前進道路。
本文檔探討了區塊證明最壞情況的解決方案:執行軌跡中需要合約字節碼。這個問題由來已久,並提出了各種解決方案。隨著時間的推移,協議變更在用戶體驗影響和風險方面的評估會有所不同,因此更詳細地瞭解其利弊將大有裨益。
背景與動機
本部分為不熟悉此問題的讀者提供介紹。如果您已經熟悉此問題,請跳過此部分。
為什麼要進行區塊執行證明?
Vitalik最近發表的一篇文章解釋了為什麼以太坊需要更高的執行吞吐量。提高 Gas 上限會線性增加區塊驗證負載,需要證明者提供更多的計算和存儲資源,這與以太坊去中心化和無需信任等核心價值觀相沖突。
執行證明將這種關係從線性變為亞線性,通過將工作量轉移給區塊提議者,允許更安全地提高 Gas 限額,而不會影響證明者。這是朝著更安全地提高執行吞吐量邁出的重要一步,儘管它並不能解決所有擴展問題,例如狀態增長。
區塊證明硬件和證明效率
雖然區塊證明將工作分擔給了區塊構建者,但對證明者的最低硬件要求卻存在限制。區塊證明遵循“N 選 1”的假設(一個誠實的證明者即可確保區塊的活性和抗審查性)。與任何“N 選 1”的假設一樣,以太坊社區應該在某個時候決定需要多少誠實且獨立的證明者才能滿足這一假設。
此硬件預算限制了網絡可以設置的最大 Gas 限額,因此,最大限度地降低 Gas 限額在最壞情況下的使用率至關重要。縮小平均情況和最壞情況之間的差距可以提高網絡效率,從而在給定 Gas 限額的情況下降低證明者預算。
證明器面臨的兩個主要瓶頸是合約代碼(字節碼)證明和錯誤定價的操作碼/預編譯。本文檔重點討論前者。
為什麼合約代碼是瓶頸
目前,每個帳戶字節碼都在帳戶 Merkle Patricia Trie (MPT) 中的代碼哈希( keccak(bytecode) )字段中提交,因此驗證字節碼的片段需要提供全部內容,即只能從完整的字節碼中計算出哈希值。
這對於區塊證明來說效率低下,因為交易通常只使用合約代碼的一小部分。例如,ERC-20 代幣轉賬不涉及鑄造代碼。類似地,像EXTCODESIZE這樣返回合約大小的操作碼,需要完整的字節碼來計算長度,因為合約大小並不直接存儲在樹中。
例如,在當前 36M 的 Gas 限制下,最壞的情況是,一個區塊中包含一筆交易,該交易使用 36M Gas 反覆執行EXTCODESIZE指令,目標合約大小上限為 24KiB。在當前 36M 的 Gas 限制下,證明者將被迫哈希約 338MiB 的數據(使用EIP-2930計算,約等於36M/2500*24KiB )。
這並非一個非常複雜的理論攻擊——幾個月前,我在主網中進行了一次 9 倍降級攻擊,以檢查其對 Ethproofs 證明器的影響。我花費了 25 美元( 攻擊交易,攻擊區塊),而證明器在完成證明的過程中花費的時間比平均時間長 3 到 5 倍,或者在此過程中崩潰。
還要注意, EXTCODESIZE攻擊向量並非唯一——任何觸及合約單個字節碼的類似CALL的指令都會迫使證明者將整個字節碼包含在見證中。使用EXTCODESIZE只是觸發攻擊的最簡單方法。
不同 gas 限制的最壞情況如下:
- 36M gas = ~338MiB 數據到 keccak。
- 60M gas = ~563MiB 數據到 keccak。
- 100M gas = ~939MiB 數據到 keccak。
- 300M gas = ~2.8GiB 數據到 keccak。
請注意,如果協議將 24KiB 最大合約大小增加到 256KiB,則我們必須將每種情況乘以約 10 倍。
我正在與@kevaundray合作,讓所有 zkVM 都能夠重現這種(以及其他)最壞情況,並將其作為跨不同 Gas 限制和最大合約規模的區塊測試。這樣做可以讓 zkVM 團隊準確地瞭解這些極端情況如何影響他們的架構,從而確保工程和協議設計都以具體、可復現的指標為指導。
解決方案
在區塊證明的背景下,解決字節碼問題有兩種方法:協議外和協議內。
請注意,沒有完美的解決方案;每種解決方案都會增加複雜性,但同時也會帶來好處。本文檔探討了多種可能性,最終決定權留給未來的 ACD。解決方案按複雜性升序排列。
協議外解決方法
侵入性最小的方法是不對協議進行任何更改,但這不僅僅是一個解決方案,而是一種變通方法。Reth 的Ress 客戶端是部分無狀態的,只存儲字節碼,而不存儲其他狀態(例如隨機數、餘額、存儲槽)。假設驗證者擁有合約字節碼,區塊證明者只需證明代碼哈希的正確性,而無需證明字節碼數據本身。
該解決方案的優點:
- 沒有核心協議變化(這是一個很大的好處),僅支持網絡上的證明分發。
缺點:
- 客戶端並非無狀態,而是部分無狀態,需要約 10GiB 的數據。隨著區塊鏈的發展,這一需求將持續增長,並且隨著 Gas 限額或最大合約規模的增加,增長速度也將加快。對於大多數驗證者來說,這個空間仍然相對較低,但可能會排除其他功耗極低的設備(例如智能手機)。
- 區塊驗證者可以在加入區塊鏈之前下載所有合約字節碼,但如果沒有額外的信任假設,他們無法確定數據是否完整——他們可能需要從其他網絡節點獲取缺失的合約字節碼。此獲取過程不受時間限制,從而在The Block驗證流程中引入了網絡依賴性。
- 這使得該解決方案不適用於對區塊證明有嚴格截止期限的網絡證明者。對於非證明者(例如,僅驗證鏈的節點)來說,這不是問題。
- 證明和字節碼服務都依賴於網絡節點的誠實性——與完整的協議解決方案相比,區塊提議者必須在The Block中包含證明。
儘管上述列表有很多缺點,但考慮到它不需要任何協議更改,我們不能對該解決方案要求更多。
節點可以通過從可信來源下載所有合約字節碼來緩解這些缺陷,確保不會因為字節碼丟失而導致後續網絡調用。通過在協議層引入一個僅可追加的代碼哈希 Merkle 樹,可以消除這種信任假設。該樹的根節點允許客戶端驗證數據,並幫助離線節點同步丟失的數據。
協議內選項
解決這個問題的方法是將合約的字節碼拆分成多個塊,並對其進行默克爾化。這樣可以只證明執行軌跡中涉及的代碼塊,這比完整的合約代碼所需的數據要少。
任何關於代碼分塊的解決方案都有兩個正交的維度:
- 字節碼如何分成塊?
- 塊是如何進行默克爾化的?
字節碼如何分成塊?
主要有兩種代碼分塊策略:
- 31字節代碼分塊器:它使用1字節前綴將字節碼劃分為31字節的塊,以輔助
JUMPDEST分析。詳細說明可在此處找到。 - 32 字節代碼分塊器:它將字節碼劃分為 32 字節的塊,並在字節碼前面添加一個表,該表可以有效地編碼哪些代碼塊包含無效的跳轉目標
0x5B。例如,如果沒有PUSHN立即數包含0x5B,則任何跳轉都是有效的;因此,該表將為空——更詳細的解釋可以在這裡找到。 - 動態分塊: 先前的研究探索了動態大小分塊,其中代碼跳轉只能定位到塊的開頭。由於
JUMPDEST指令標記了有效的跳轉目標,因此它們可以有效地確定塊的起始位置。這可以避免使用額外的機制來檢測無效跳轉。然而,它存在一些潛在問題,需要通過其他機制來解決,例如,沒有任何JUMPDEST大型字節碼。
請注意,這些代碼分塊器解決了遺留合約可能存在的一個問題。即將推出的 EOF 合約不存在這個問題,因為只有包含有效跳轉的代碼才能被部署。不久前,我做了一個分析,比較了這兩種策略。
以上是兩個建議的分塊器,但還可以有更多變體。
- 使用效率:塊越大意味著在見證中提供的塊越少,但會增加浪費的字節,因為並非所有塊中的字節都在執行跟蹤中使用。
- 兩個分塊器都提出了 32 字節的代碼塊,這可能是因為它們的大小與存儲槽的大小相同。
- 存儲效率:分塊會產生多少存儲開銷?
- 31字節代碼分塊器:1/31~=3.2% 開銷
- 32 字節代碼分塊器:動態的,不久前進行的主網分析報告顯示約為 0.1%,最壞情況為 3.1%(即每個塊都包含無效跳轉)。
- 複雜性:規範和實現的複雜性
- 31字節代碼分塊器:非常簡單
- 32 字節代碼分塊器:定義表格式、位置ETC更加複雜。
塊如何進行默克爾化?
給定一個分塊的字節碼,我們需要確定如何對塊進行默克爾化,以便可以為執行跟蹤中訪問的部分字節碼創建證明。
為了高效地支持EXTCODE*操作碼,我們還必須包含現有的codeHash和新的codeSize字段。實現起來非常簡單,因此我將只關注主要難點:對代碼塊進行默克爾化。
在哪裡對代碼塊進行默克爾化?
以下是一些關於它們將被默克爾化的策略:
- 在當前 MPT 內。
- 新的統一狀態樹內部
- 在 EIP 中,提出了用於帳戶和存儲數據的新統一樹(例如二叉樹),合約代碼塊也將存儲在這裡。
- 優點:
- 引入了考慮新樹的自然設計。
- 現有代碼的代碼分塊是樹轉換過程的一部分。
- 缺點:
- 當前的提議不會對相同的塊進行重複數據刪除,但這種情況可能會改變。
- 與許多其他變化捆綁在一起,它可能會增加共識錯誤的風險。
- 在專門用於代碼塊的新樹內
- 引入了一個新的僅用於代碼塊的樹。現有的 MPT 保持不變。
- 優點:
- 或者,它允許靈活地設計新樹(例如,不同的元數或哈希函數)。
- 重複數據刪除是可選的,取決於密鑰定義。
- 缺點:
- The Block中有一個額外的樹根。
- 與 MPT 不同的樹設計意味著更多的規範複雜性和實施錯誤的風險。
如何處理新的和現有的字節碼的默克爾化?
創建新合約時對代碼進行默克爾化的過程很簡單,因為EIP-4762已經概述了按存儲的代碼塊收取 gas 費的結構,即按樹中插入的代碼塊向用戶收費。
主要挑戰在於對現有代碼進行默克爾化,至少需要 15GiB 的去重數據。這無法僅在分叉激活時進行操作來實現。雖然新的統一狀態樹方案可以在樹轉換過程中完成這項工作,但其他兩種策略必須決定如何執行——以下是一些思路:
- 按需轉換(感謝 Dankrad 提到這個變體 - 優點/缺點由我自己決定)
- 只有當交易與未分塊的合約交互時,代碼才會被分塊。交易發送者需要支付代碼分塊的費用。
- 優點:
- 對於協議來說非常容易,因為它在使用之前不必轉換現有的主網字節碼。
- 代碼被默克爾化的目標樹不會被可能永遠不會再使用的合約所汙染,因此它間接地進行了一些修剪。
- 缺點:
- 合約的代碼分塊由與其交互的第一個交易支付。這可能是一個用戶體驗問題,因為誰來支付是不可預測的。從用戶的角度來看,等待其他人先支付可能比發送自己的交易更好。
- 在分叉激活時以及後續區塊生成過程中,我們可能會看到 Gas 用量大幅飆升,這僅與代碼分塊有關。這也會影響基礎費用,直到系統穩定下來。
- 這種方法可能會留下永遠未轉換的字節碼。因此,EL 客戶端必須保留轉換後的代碼,因為最終可能還會用到它。這也意味著The Block證明者必須證明代碼分塊,因此轉換所需的 Gas 成本可能會很高。
- 多塊轉換
- 類似於新的樹轉換( EIP-7748 ),我們將定義一個流程,在每個塊上遷移待處理的代碼,直到所有代碼都被分塊。
- 優點:
- 由於用戶無需為代碼分塊付費,因此不存在用戶體驗 (UX) 問題。
- 沒有汽油價格飆升或基本費用干擾。
- 與單個區塊激活相比,The Block塊轉換可以在任何錯誤發生時立即檢測到共識級別上的錯誤。
- 缺點:
- 它增加了僅執行一次的任務的規範的複雜性,並且可能存在許多實現錯誤,從而增加了風險。
- 離線轉換
- 按需+(有界)線下混合
- 僅對過去一年內訪問過的合約請求離線轉換,以最大程度地減少工作量。對於剩餘未分塊的合約,第一筆交易即可覆蓋轉換成本。
- 優點:
- 用戶體驗問題極少,因為訪問過去一年的合同的用戶無需付費。只有訪問更早交易的用戶才需要付費。
- 最小的天然氣峰值或基本費用干擾。
- 減少 EL 客戶端在離線轉換中的工作量。
- 缺點:
- 儘管需要轉換的字節碼較少,但後臺轉換仍然不透明,因此共識失敗的風險仍然存在。
- 分叉激活時間戳估計仍然至關重要,或者可能需要手動進行第二次分叉激活。
- 它具有相同的情況,即總是有未轉換的代碼並且必須證明代碼分塊。
接下來會發生什麼?
解決了未分塊的字節碼問題之後,剩下的瓶頸就是錯誤定價的操作碼/預編譯(例如, MODEXP和配對——一些這方面的工作計劃在 Fusaka( EIP-7883 )中進行),以及狀態訪問效率。研究人員和 zkVM 工程師正在分析前者。
後者意味著使用所有可用的 gas 來訪問狀態。負載取決於底層樹的元數和哈希函數,這會影響見證所需的哈希量。假設保持當前 MPT 不變,如果我們儘可能多地訪問賬戶:
- 36M gas = ~407k keccak 排列。
- 60M gas = ~678k keccak 排列。
- 100M gas = ~1.13m keccak 排列。
- 3億 gas = ~339萬 keccak 排列。
( keccak perms = gas/2500*tree_depth*15*32/keccak_rate = gas/2500*8*15*32/136 )
請注意,上述計算使用了 MPT 等 16 元樹中主網的預期深度(即~8);然而,實際樹結構中的特定分支可以達到更大的深度,從而提供了利用的機會。
如果上述預測對於考慮的最小證明器硬件預算來說是一個問題,我們可能需要考慮狀態樹的改變或狀態訪問的 gas 模型。



