Alex Stokes@ralexstokes:

你可能已經聽說了,@OpenEthereum 客戶端的一個錯誤導致了一些支撐以太坊網絡的重要服務宕機。

我們來琢磨一下那筆造成事故的交易。

首先,我想感謝所有快速反應到事故並解決了問題的工程師:https://twitter.com/OpenEthereumOrg/status/1382719444833726470?s=20…

另外,我沒有自己跟踪所有的細節,下文中的重要事實都由用戶 eb 在Eth R&D discord 服務器裡提出。

先從那筆觸發了錯誤的交易開始:https://etherscan.io/tx/0x7006f38fa2e6654fae1a781aefc5885fe0cb8f778b1add10636eaf7e34279247

這是一筆合約調用交易,從KuCoin 交易所發出,向許多地址分發ETH。對該筆交易的call data 的ABI 編碼錯誤,最終導致了鏈分裂。你可以在Etherscan 上看看這筆交易的“Input Data”。

1/ 在合約中調用sendEths 時,需要提供兩個參數:一個是關於目標地址的不定長數組(dynamically sized array);一個是關於轉賬數額(以wei 為單位)的不定長數組;兩者相搭配才知道要轉移多少錢給哪個地址。

2/ 我們可以解析call data 來看看到底哪裡出了錯:第一行(在Etherscan 上標記為“[0]”)表示地址列表從字節64(行“[2]”)開始。第二行表示轉移數額的列表從字節416(行“[13]”)開始。

3/ 因此,大體上,我們是希望成對成對地、從上往下、向某個地址發送一定數量的ETH —— 看起來很直接嘛。

4/ 然而,當我們開始遍歷這個列表,我們先跳轉到call data 的正確字節,而Solidity ABI 聲明了數據的第一個字是整個不定長數組的長度。

5/ 這就是最終bug 的根源:因為call dada 中的值是“0x10”(注意,這可是16 進制!),但是call data 只給出了10 個地址-數值對。對這個call data 的正確ABI 編碼(填在行[2] 和行[13] 的)應該是“0xA” —— 不是“0x10”!

6/ 你可能已經猜到了那時候會發生什麼事,我們可以通過執行情況跟踪器(execution trace)來看看:https://etherscan.io/vmtrace?txhash=0x7006f38fa2e6654fae1a781aefc5885fe0cb8f778b1add10636eaf7e34279247&type=parity

7/ 合約成功地遍歷了前10 個地址。本來合約應該在此時停止執行,但根據call data 的聲明,還有很多個地址!那就繼續執行吧。

但是,根據call data 的結構,“第11 個地址” 是用於編碼列表長度的 0x10,所以合約就嘗試發送0 ETH 到地址 0x10。

8/ 此外,似乎,當合約嘗試讀取並不存在的call data 時,會返回0 ETH —— 你可以想像成合約在這裡跑出了一個錯誤,但它卻繼續發送0 ETH 到它從call data中讀取的另外6 個“地址”。

此時,你可能會注意到,0x10 有可能是我們所謂的“特殊地址” 之一,它完全在EVM 預編譯合約的範圍內(所謂“預編譯合約”,就是一類特殊合約,在EVM 之外有最優的實現,但是編譯起來與大多數合約一樣)。

而我們也並不期望預編譯合約 0x10 能夠返回ETH 。如此,它就成了一個ETH 黑洞。但是,這也並不必然造成任何問題。到底是什麼導致了整個客戶端崩潰?

原因在於,0x10 實際上是一個由EIP-2537 斷言的預編譯合約,是為BLS 配對密碼學程序而設的,但這個EIP 還未部署到主網上。所以雖然你能夠跟這個地址互動,但主網上的這個地址裡沒有任何合約,不會有任何進一步的動作。

此外,我們還需要一個事實來解釋這次分裂,你可能也猜到了,就是“柏林” 硬分叉(也正是這次硬分叉使這個問題浮出水面):它改變了EVM 中Gas 消耗量的計量方法。

在EIP-2929 實施後,如果你在一筆交易中對同一個存儲槽多次執行狀態存儲操作,第一次執行會消耗更多Gas,後續執行的消耗會更少。這種重定價理論上能更準確地反映當前的客戶端訪問存儲項的成本……

而且,要知道,在所有客戶端的執行中,這些數據通常都換存在更便宜的硬件層中。

現在我們終於找到了OpenEthereum 在區塊#12244294 處發生的Bug:該客戶端包含了 所有 已實現的預編譯,作為EIP-2929 訪問清單的一部分。 (譯者註:此處應為“EIP-2930”)

因為EIP-2537 在大部分客戶端中都已經實現就緒了(而且一度有人提議要把它包含在“柏林” 升級裡面!),OpenEthereum 對所有訪問了 0x10 的交易都給了gas 折扣。

但網絡的絕大部分活躍客戶端都不是這樣實現EIP-2929 的,它們只會給訪問了已激活預編譯合約的交易提供gas 折扣—— 而EIP-2537 屬於還未激活的預編譯合約!所以,OpenEthereum 客戶端對該交易消耗了多少Gas 的計算與網絡中其他客戶端發生了分歧。

所幸,@mhswende 很快找出了該bug,而 @sorpaas 出力修復了該bug:https://github.com/openethereum/openethereum/pull/364

還有很多東西可說,我也預期會有比我更能觀察到全貌人來撰寫更好的時候報告。

我能說的只是,這個bug 彰顯了硬分叉的內在風險,以及持續致力於建設更有彈性的基礎設施的重要性。

依賴於OpenEthereum 客戶端的單客戶端系統在今天停機了一段時間,因為客戶端無法在問題區塊出現後與網絡保持同步。 Etherscan 自身也因此停機。

慶幸的是,這個bug 沒有嚴重到導致重大的鏈分叉,但這樣的可能性並不是不存在。我們可以利用多客戶端實現來提升抗性—— 多客戶端本身就是我們以太坊生態的一大長處—— 並推動您的基礎設施提供商也這樣做。

我們已經看到,2021 年的普及速度已經前所未有地快,而且前景非常光明。我們要從這個事故中吸取教訓,一起打造更好的以太坊。

原文鏈接:

https://twitter.com/ralexstokes/status/1382750001026146304

作者: Alex Stokes