這篇文章的目的是正式公開一個對以太坊平臺的嚴重威脅,其危險性清晰而明確,直到 “柏林” 硬分叉才解除。
狀態
我們先來了解一些以太坊和 “狀態” 的背景知識。
以太坊狀態是一棵 帕特里夏-默克爾樹(particia-merkle trie,一種兼有字首樹規則的默克爾樹)。本文不會深入過多細節,你只要知道,隨著狀態數量的增長,這個樹結構的分支會變得越來越密。以太坊區塊鏈上每多一個賬戶,這棵樹就多一個葉子節點。在樹的根節點與葉子節點,是許多所謂的 “中間” 節點。
為了查詢某個賬戶,或者說在這棵龐大的樹上找到某片 “葉子”,需要解析 6 ~ 9 個雜湊值,從根節點開始,經由中間節點,最終解析到一個能夠給予我們所需資料的雜湊值。
用大白話來說:無論什麼時候要在這棵樹上查詢某個賬戶,都要經過 8 ~ 9 次解析操作。每次解析操作都是一次資料庫查詢,而每一次資料庫查詢都意味著不確定數量的多次硬碟操作。硬碟操作的次數很難估計,但是因為狀態樹的 “鍵(key)” 是密碼學雜湊值(抗碰撞的),所以這些鍵都是隨機的,這對所有資料庫來說都屬於最壞的情況。
隨著以太坊狀態的增加,就有必要提高訪問狀態樹的操作的 Gas 消耗量。早在 2016 年 10 月,我們就曾用 “橘子口哨(Tangerine Whistle)” 分叉(納入 EIP 150,在區塊高度246 3000啟用)做過這樣的事。EIP 150 大幅提高了特定操作的 Gas 消耗量,並引入了一系列的措施來保護網路免於 DoS 攻擊;這是在所謂的 “上海攻擊” 之後推出的。
另一次這樣的 Gas 消耗量提升是在 “伊斯坦布林” 分叉的時候,在區塊高度906 9000(2019 年 12 月)啟用,引入了 EIP 1884。1884 的內容包括:
SLOAD操作碼的 Gas 消耗量從200提高到800gas
BALANCE消耗量從400提高到700gas (還加入了一個更便宜的SELFBALANCE操作碼)
EXTCODEHASH消耗量從400提升到700gas
問題(們)
在 2019 年 3 月,Martin Swende 測量了 EVM 操作碼的效能。這一研究後來導致了 EIP-1884 的建立。在 1884 啟用的幾個月前,這篇以 “Broken Metre” 為名的論文發表(2019 年 9 月)。
兩位以太坊安全研究員 —— Hubert Ritzdorf 和 Matthias Egli —— 與這篇論文的作者之一 Daniel Perez 展開了合作,並 “武器化” 了一個漏洞,並提交給了以太坊的 bug 懸賞專案。那是在 2019 年 10 月 4 日。
我們建議你完整地閱讀他們提交的報告,寫得非常好。
在一個專門討論跨客戶端安全性的頻道里,來自 Geth 客戶端、Parity 客戶端和 Aleth 客戶端的開發者被告知了這份報告,就在同一天。
該漏洞的本質是觸發隨機的樹查詢。一個非常簡單的變體是:
jumpdest ; jump label, start of loop gas ; get a 'random' value on the stack extcodesize ; trigger trie lookup pop ; ignore the extcodesize result push2 0x00 ; jump label dest jump ; jump back to start
在他們的報告裡,研究員透過eth_callRPC 端點對同步到主網的節點執行了這一負載,下面是它們消耗 1000 萬 gas 所需的時間。
使用EXTCOEHASH(名義 Gas 消耗量是 400)耗盡 1000 萬 gas
Parity:約 90 秒
Geth:約 70 秒
使用EXTCODESIZE(名義 Gas 消耗量是 700)消耗 1000 萬 gas
Parity:約 50 秒
Geth:約 38 秒
(譯者注:此處的意思是,如果一個 1000 萬 gas 的區塊全用這兩個操作碼填滿,則節點需要這麼長時間才能處理完這個區塊)
顯而易見的是,EIP-1884 確實減少了攻擊的效果,但還是遠遠不夠的。
那時候離大阪 Devcon 已經很近了。在 Devcon 期間,關於這一問題的知識在主網的客戶端開發者之間傳開來。我們也會晤了 Hubert 和 Mathias,還有 Greg Markou(他來自 Chainsafe 團隊,一直在做 ETC 的工作)。ETC 區塊鏈的開發者們也收到了這份報告。
隨著 2019 年接近尾聲,我們發現,這問題比我們之前以為的還要棘手,惡意的事務可能導致出塊時間延長到以分鐘計。更難辦的是,開發者社羣已經對 EIP-1884 感到不滿,它打破了一些合約,而且使用者和礦工都希望提高區塊的 Gas Limit。
此外,僅僅兩個月之後,到了 2019 年 12 月,Partiy Ethereum 就宣佈要退出了,OpenEthereum 專案接管了 Parity 客戶端的程式碼維護工作。
於是大家建立了一個新的客戶端協作頻道,Geth、Netheremind、OpenEthereum 和 Besu 的開發者繼續合作。
解決方案
我們意識到,只有雙管齊下才能解決這個問題。一方面,我們要改進以太坊協議,也就是在協議層解決這個問題;最好是不要打破合約,也不要懲罰 “善意” 的行為,但又能防止攻擊。
另一方面,我們可以依靠軟體工程,改變客戶端內的資料模式和結構。協議層工作
處理此類攻擊的第一個思路是這個。在 2020 年2 月,其正式版本作為 EIP 2583 釋出。該提案背後的觀念是增加一個懲罰措施,每次樹查詢導致 miss (“未找到”)時就觸發。
不過,Peter 找出了一個繞過它的辦法 ——“shielded relay” 攻擊 —— 使得本質上懲罰有了一個上限(約為 800)(譯者注:此處沒有單位,疑為 gas)。
懲罰 miss方法的問題在於,必須先有查詢的過程,然後才能確定要不要實施懲罰。但如果剩餘的 gas 已不足於實施懲罰,則(從協議的角度看)一個沒有得到充分支付的消耗流程又已經執行了。即使這會導致丟擲錯誤,這些狀態讀取也可以封裝到巢狀呼叫中,使得外部呼叫者可以重複執行攻擊而不必支付(完整的)懲罰。
因此,這個 EIP 也被拋棄了,我們要尋找更好的替代方案。
Alexey Akhunov 研究了 Oil 的概念 —— 一種次級的 “Gas”,但與 Gas 完全不同的是,它對執行層是不可見的,而且可能導致事務全域性回滾(transaction-global revert)。
Martin 提了一個類似的提案,稱為 “Karma”,在 2020 年5 月。
雖然這許多方案都有進展,Vitalik Buterin 提議僅僅提高 Gas 消耗量,並維護一個 “訪問清單”。在 2020 年 8 月,Martin 和 Vitalik 開始迭代後來成為 EIP-2929 及其同伴 EIP-2930的想法。
EIP-2929 在根本上解決了許多上面提到的問題。
與 EIP-1884 相反;1884 是無條件提高 Gas 消耗量,但 2929 僅提高訪問新物件的 Gas 消耗量。這使得淨成本僅增加了不到一個百分點。
同樣地,與 EIP-2930 配合後,就不會打破任何合約。
它還可以透過提高 Gas 消耗量來進一步調整(也不會打破合約)
在 2021 年 4 月 14 日,這兩個 EIP 都在 “柏林” 分叉時啟用。開發工作
Peter 嘗試用動態的狀態快照解決這個問題,時值 2019 年 10 月。
快照是一個次級的資料結構,用來以扁平格式(flat format)儲存以太坊狀態。快照可在 Geth 節點正常執行期間建立,無需下線專門執行。快照的好處是,它可以作為狀態訪問的一種加速結構:
不再是執行O(log N)次硬碟讀取(還要乘以 LevelDB 的開銷)來訪問一個賬戶/儲存項,快照可以提供直接的,O(1)級別的訪問時間(再乘以 LevelDB 的開銷)。
快照還支援以每個條目O(1)的複雜度迭代賬戶和儲存項,這使得遠端節點可以檢索連續的狀態資料,比以往便宜非常多。
快照的存在還支援其它更奇怪的用途,比如離線修剪狀態樹,以及遷移到另一種資料格式。
弊端是,快照等於是完全複製了賬戶和儲存項的未經處理(raw)的資料。若在主網環境中使用,這意味著需要額外 25 GB 的固態硬碟空間。
動態快照的想法從 2019 年中就有了,當時的主要目標是啟用 “快照同步”。那時候 Geth 團隊還在開發許多 “大專案”:
離線的狀態修剪
同態快照 + 快照同步
透過共享狀態實現 LES 狀態分散
不過,後來他們決定一心一意做快照功能,推遲了其他專案。這些工作為後來的snap/1同步演算法打下了基礎。這一演算法已在 2020 年 3 月合併到了程式碼庫中。
有了 “動態快照” 功能,我們就能喘口氣了。如果以太坊網路遭到攻擊,那會是很痛苦的,但至少,我們能通知使用者開啟快照功能。生成快照需要花一些時間,而且還沒有辦法同步快照,但網路至少能繼續執行了。
結合
在 2021 年 3 月/4 月,snap/1協議已經在 geth 客戶端推出,節點能夠使用新的、基於快照的演算法來同步區塊鏈了。雖然還不是預設的同步模式,這是使快照能不僅作為攻擊保護措施,也能顯著提高使用者體驗的一部。
在協議層,“柏林” 升級已於 2021 年 4 月啟用。
在我們的 AWS 監控環境中,我們的基準測試結果如下:
“柏林” 前,沒有快照,處理 2500 萬 gas:14.3 秒
“柏林” 前,有快照,處理 2500 萬 gas:1.5 秒
“柏林” 後,沒有快照,處理 2500 萬 gas:約 3.1 秒
“柏林” 後,有快照,處理 2500 萬 gas:約 0.3 秒
這個(粗糙)的數字表明,“柏林” 升級使攻擊的效率降低了 5 倍,而快照使之降低了 10 倍,最終使其影響降低了 50 倍。
我們估計,在當前的主網上(區塊為 1500 萬 gas),不使用快照的 geth 節點可能可以做到只需 2.5 ~ 3 秒就能執行一個區塊。隨著狀態的增長,這個數字會繼續惡化(對於不使用快照的節點來說是如此)。
如果 gas 返還機制被用來造成單個區塊的實際 gas 使用量提升,這個惡化的倍數(最大)是 2 倍。在 EIP-1559 實施後,區塊的 Gas Limit 會有更高的彈性,在短時間內可爆發出最大 2 倍的惡化乘數。
至於實施這種攻擊的可行性,攻擊者買斷一個區塊的成本大概在幾個 ETH 這樣的級別(1500 萬 gas,100 Gwei 的價格,乘出來就是 1.5 ETH)。
為何要在此時公開
這一威脅在很長時間裡都是 “公開的秘密” —— 因為疏忽,它至少被公開披露過一次;而且在核心開發者會議中也多次提到它,雖然沒有公開細節。
因為我們已經啟用了 “柏林” 升級,也因為 geth 客戶端已經預設使用快照功能,我們認為,威脅已經足夠低,而透明化才是更重要的了。所以是時候把幕後的工作都公開了。
重要的是,社羣得到了一次理解和思考這些影響使用者體驗(這些 EIP 會提高 Gas 消耗量,也會限制返還機制的效果)的變更的機會。
(完)
本文由 Martin Holst Swende 和 Peter Szilagyi 在 2021 年 4 月 23 日撰寫完成。在 4 月 26 日,該文被分享給了其他基於以太坊的專案,並在 5 月 18 日完全公開。
(文內有許多超連結,可點選左下 ”閱讀原文“ 從 EthFans 網站上獲取)