一文了解柏林硬分叉對 Gas 影響幾何?

買賣虛擬貨幣

來源 |@fvictorio

作者 | Franco Victorio

柏林硬分叉已於 4 月 14 日在主網上線,引入了四份 EIP 。其中的兩份 (EIP-2929 和 EIP-2930)對交易的 gas 成本有影響。本文將解釋部分 gas 成本在柏林前是如何計算的,加入了 EIP-2929 後會如何變化,以及如何使用 EIP-2930 引入的訪問列表。

要點速覽

這篇文章很長,這是它的概要:

柏林硬分叉改變一些操作碼的 gas 成本。如果在一個 dapp 或一個智慧合約裡 gas 費的值是硬編碼的,它們可能會中止執行。如果這種情況發生了,且智慧合約是不可更新的,消費者將需要用 EIP-2930 的訪問列表才能使用那部分的操作碼。

訪問列表可以用作減少少量的 gas 成本,但實際上它們在一些情況下是會增加總 gas 消耗量的。

geth增加了一個叫eth_createAccessList的新 RPC方法,用以簡化訪問列表的建立。

柏林硬分叉前的 gas 成本

EVM 執行的每個操作碼都有一筆相關的 gas 成本。它們大多數的成本是固定的:PUSh2總是消耗 3 個單位的 gas,MUL消耗 5 個,等等。其他一些是會變化的:比如SHA3的操作碼成本依賴於它的輸入大小。

我們主要討論操作碼SLOAD和SSTORE,因為它們是最受柏林硬分叉影響的。我們以後會討論針對地址的操作碼,比如所有的EXT*和CALL* ,因為它們的 gas 成本也改變了。

柏林前SLOAD的 gas 成本

在沒有 EIP-2929 之前,SLOAD的 gas 消耗很簡單:它總是消耗 800 gas。所以(目前)沒有什麼可說的。

柏林前SSTORE的 gas 成本

在 gas 消耗方面,SSTORE可能是最複雜的操作碼了,因為它的成本取決於像儲存 slot 的當前值、新值、以及它是否之前被修改過。我們僅對一些情況進行分析以獲得一個基本理解;如果你想了解更多,請閱讀文末的 EIP 連結。

如果儲存 slot 的值從0 變成 1 (或任何非 0 的值),gas 消耗量是 20000。

如果儲存 slot 的值從1 變成2 (或任何其他非 0 的值),gas 消耗量是 5000。

如果儲存 slot 的值從 1 (或任何非 0 的值) 變成 0,gas 消耗量也是 5000,但在交易的最後你會獲得 1 筆 gas 費返還。本文不會討論 gas 費返還,因為它們在柏林硬分叉中不受影響。

如果儲存 slot 的值在之前相同的交易中被修改了,往後所有 SSTORE 的 gas 消耗量都是 800。

這部分的細節並不有趣,重要的是 SSTORE 很貴,而它的消耗取決於幾個因素。

EIP-2929 後的 gas 消耗

EIP-2929 對上述所有操作碼的 gas 消耗都有影響。但在深入這些變化前,我們需要先談談這份 EIP 引入的一個重要概念:訪問過的地址 (accessed addresses)與訪問過的儲存金鑰 (accessed storage keys)。

如果一個地址或一個儲存金鑰在之前的交易中被“使用”過,那麼它們就會被視為“訪問過的”。例如,當你CALL(呼叫)一個其他合約,該合約的地址就會被標為“ accessed (訪問過的)”。同樣地,當你SLOAD(載入)或SSTORE(儲存)一些 slot 的時候,交易的其他部分也會被視為訪問過的。哪個操作碼執行它並不重要:如果一個SLOAD讀取了一個 slot,接下來的SLOAD和 SSTORE都會被視為訪問過的。

這裡值得注意的是,儲存金鑰是“內建於“一些地址的。就如這份 EIP 所解釋:

“在執行交易時,維持一組accessed_addresses: Set[Address]和 accessed_storage_keys: Set[Tuple[Address, Bytes32]] ”

也就是說,當我們說一個儲存 slot 被訪問了,我們實際上說的一對 (address, storageKey) 被訪問了。

接下來談談新的 gas 消耗。

柏林後的SLOAD

在柏林硬分叉之前,SLOAD固定消耗 800 gas。現在,它取決於該儲存 slot 是否被訪問過。如果它沒有被訪問過,gas 消耗是 2100;如果被訪問過了,則是 100。因此,如果該 slot 是在訪問過的儲存金鑰列表裡的,SLOAD 的 gas 消耗會少於 2000。

柏林後的SSTORE

讓我們在 EIP-2929 語境下重溫前面的SSTORE例子:

➤ 如果儲存 slot 的值從0 變成 1 (或任何非 0 的值),gas 消耗量是:

如果儲存金鑰沒有被訪問過,22100

如果被訪問過了,20000

➤ 如果儲存 slot 的值從1 變成2 (或任何其他非 0 的值),gas 消耗量是:

如果儲存金鑰沒有被訪問過,5000

如果被訪問過了,2900

➤ 如果儲存 slot 的值從 1 (或任何非 0 的值) 變成 0,gas 消耗與上一種情況一樣,再加上返還。

➤ 如果儲存 slot 的值在之前相同的交易中被修改了,往後所有SSTORE的 gas 消耗量都是100。

如你所見,如果SSTORE正在修改的 slot 是之前被訪問過的,第一個 SSTORE消耗少於 2100 gas。

總結

下表對上述的值進行了比較:

請注意,在最後一行沒有必要談論 slot 是否已經被訪問過,因為如果它之前就被寫入,那它就被訪問過了。

EIP-2930: 可選訪問列表交易

我們一開始提及的其他 EIP 就是 EIP-2930。這份 EIP 增加了一種新的交易型別,它可以在交易里加入一個訪問列表。這意味著你可以在交易執行開始前,事先宣告哪些地址和 slot 應被視為訪問過的。例如,一個未被訪問過的 slot 的一個SLOAD需要消耗 2100 gas,但如果該 slot 被加入到交易訪問列表裡,同一個操作碼只需消耗 100 gas。

但如果已經被訪問過的地址或儲存金鑰會消耗更少 gas,這是否意味著我們可以把所有東西都新增到交易訪問列表來降低 gas 消耗了?棒!不用給 gas 費了!然而,不盡然是這樣,因為你每次新增地址和儲存金鑰的時候還是需要支付 gas 費的。

我們來看一個例子。假如我們正在向合約 A 傳送一筆交易,訪問列表可能如下:

accessList: [{  address: "<address of A>",  storageKeys: [    "0x0000000000000000000000000000000000000000000000000000000000000000"  ]}]

如果我們傳送一筆附有這個訪問列表的交易,使用 slot0x0的第一個操作碼是SLOAD,它消耗的是 100 而不是 2100 gas。這減少了 2000 gas。但每次把儲存金鑰新增到交易的訪問列表中都需要消耗 1900 gas。因此我們只省了100 gas。(如果訪問該 slot 的第一個操作碼是SSTORE而不是SLOAD,我們可以省 2100 gas,也就是說如果我們考慮的是儲存金鑰的消耗的話,我們總共節省 200 gas。)

這是否代表只要我們使用交易訪問列表就能節省 gas?不是的,因為我們還需要支付新增地址到訪問列表 (即我們的例子中的 "<address of A>" ) 的 gas。

訪問過的地址

到目前為止,我們只討論了操作碼SLOAD 和SSTORE,但柏林升級後不是隻有這些操作碼有變化。例如,操作碼CALL之前的固定消耗量是 700。但 EIP-2929 後,如果地址不在訪問列表裡,它的消耗量變成了 2600,如果在,則是 100。還有,像訪問過的儲存金鑰,無論之前訪問的是什麼操作碼 (例如,如果EXTCODESIZE是第一次被呼叫,那麼該操作碼將消耗 2600 gas,而往後任何使用同一個地址的EXTCODESIZE、 CALL還是STATICCALL都只消耗 100 gas)。

這是如何影響有訪問列表的交易的呢?例如,假如我們給合約 A 傳送一筆交易,而該合約呼叫另一個合約 B,那麼我們可以加入這樣一個列表:

accessList: [{ address: "<address of B>", storageKeys: [] }]

我們將需要支付 2400 gas 以把這個訪問列表加入到交易裡,但之後使用 B 地址的第一個操作碼只消耗 100 gas,而不是2600。因此,我們透過這樣做節省了 100 gas。如果B以某種方式使用它的儲存,且我們知道使用的是哪個金鑰,那麼我們也可以把它們加入到訪問列表裡,這樣可以為每個金鑰節省 100~200 gas (取決於你的第一個操作碼是SLOAD還是SSTORE )。

但是為什麼我們要談論另一個合約?我們正在呼叫的合約呢?為什麼不對這個合約進行這些操作?

accessList: [  {address: "<address of A>", storageKeys: []},  {address: "<address of B>", storageKeys: []},]

我們可以這樣做,但這樣不划算,因為 EIP-2929 明確規定正在被呼叫的合約 (即tx.to) 地址會預設加入到accessed_addresses列表裡。因此我們無須支付多餘的 2400 gas。

讓我們再對之前的例子進行分析:

accessList: [{  address: "<address of A>",  storageKeys: [    "0x0000000000000000000000000000000000000000000000000000000000000000"  ]}]

除非我們要加入多幾個儲存金鑰,否則這其實很浪費。如果我們預設 SLOAD 總是首先使用儲存金鑰,那麼我們起碼需要24 個儲存金鑰能保本。

你可以想象一下,做分析與手動建立一個訪問列表並不那麼有趣。幸運的是,其實有更好的方法。

eth_createAccessList RPC 方法

Geth (從 1.10.2 版本開始 ) 加入了一個新的eth_createAccessListRPC 方法,你可以用它來生成訪問列表。它的使用與 eth_estimateGas 相似,但它返回的不是 gas 估值,而是像下面這樣的結果:

{  "accessList": [    {      "address": "0xb0ee076d7779a6ce152283f009f4c32b5f88756c",      "storageKeys": [        "0x0000000000000000000000000000000000000000000000000000000000000000",        "0x0000000000000000000000000000000000000000000000000000000000000001"      ]    }  ],  "gasUsed": "0x8496"}

也就是它給你該交易會用到的地址與儲存金鑰的列表,加上訪問列表被加入情況下所消耗的 gas。(像 eth_estimateGas,這是一個估值,當交易實際上被挖的時候,這個列表可能會改變。)但,這並不代表 gas 消耗量會低於在沒有訪問列表情況下傳送同一筆交易所消耗的!

我想我們會隨著時間推移發現使用它的正確方法,但我猜的虛擬碼如下:

let gasEstimation = estimateGas(tx)let { accessList, gasUsed } = createAccessList(tx)if (gasUsed > gasEstimation) {  delete accessList[tx.to]}tx.accessList = accessList;sendTransaction(tx)

給合約鬆綁

值得一提的是,訪問列表的主要目的不在於使用 gas。如 EIP 所解釋:

“減輕由 EIP-2929 引入的合約斷裂風險,因為交易可以提前指定交易計劃訪問的賬戶和儲存slot並提前支付;最終在實際執行中,操作碼SLOAD和 EXT*只消耗 100 gas:這個低 gas 消耗不僅可以防止由該 EIP 引起的斷裂,還可以“鬆開”任何因 EIP-1884 而受限的合約。”

這意味著如果一個合約對執行某事務的成本做了假設,gas 成本的增加就可能使它停止運作。例如,一個合約呼叫另一個合約,像這樣 someOtherContract.someFunction{gas: 34500}(),因為它假設someFunction 會準確消耗 34500 gas,這樣它會出問題。但如果你新增了一個合理的訪問列表,那麼合約會再次運作。

自己做檢驗

如果你像自己去測試,複製這個程式碼庫,裡面由多個可以用 Hardhat 和 geth 執行的例項。在 README 檢視說明。

參考文獻

EIP-2929 和 EIP-2930 是與本文相關的兩個柏林硬分叉 EIP。

EIP-2930 依賴於柏林硬分叉的另一部分:EIP-2718,它又叫型別交易。

EIP-2929 參考了很多 EIP-2200,因此如果你想深入瞭解 gas 成本,你可以從那裡開始。

如果想看比較 gas 使用變更的更復雜例項,請看這裡。

點選“閱讀原文”獲取文章內部連結!

原文連結:https://hackmd.io/@fvictorio/gas-costs-after-berlin

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读