以太坊用戶期盼已久的「柏林」硬分叉終於上線,究竟柏林為現有的 GAS 消耗帶來了什麼改變,新引入的訪問清單(Access List)又該如何使用?本文源自於 Franco Victorio 文章《Understanding gas costs after Berlin》,由專欄作者 以太坊愛好者 編譯、撰稿及整理。
(前情提要:以太坊「柏林硬分叉」完成!卻遇客戶端共識錯誤;ETH續刷歷史新高達 2,548 美元)
(事件背景:1 年後終於到來!以太坊「柏林硬分叉」宣布 4月中上線,四個 EIP 提案將納入升級)
「柏林」硬分叉在 4 月 15 日啟動,該硬分叉所包含的兩個 EIP (EIP-2929 和 EIP-2930)都會影響事務的 Gas 開銷。
本文會解釋 「柏林」啟動之前,一些指令碼的 Gas 消耗量是如何計算的,而 EIP-2929 對此有何影響,以及 2930 引入的訪問清單(Access List)功能應如何使用。
摘要
這篇文章很長,你如果只想知道結論,看完這部分就可以把網頁關掉了:
- 柏林硬分叉改變了某些指令碼的 Gas 開銷。如果你在自己的應用中硬編寫了一些指令可使用的 Gas 數量,這些指令可能會卡死。如果真的出現了這種情況,而你的智能合約又是沒法升級的,用戶就需要使用「訪問清單」功能來使用你的應用。
- 訪問清單功能可略微減少 Gas 開銷,但有些時候也可能會提高 Gas 的總消耗量。
- geth 客戶端引入了一種新的 RPC 方法,叫做
eth_createAccessList
來簡化訪問清單的生成。
「柏林」升級以前的 Gas 開銷
EVM 所執行的每一個指令碼都有一個對應的 Gas 消耗量。大部分指令碼的消耗量都是固定的:PUSH1
總是消耗 3 gas,而 MUL
消耗 5 gas,等等。有一些指令碼的消耗量是可變的:舉個例子,SHA3
指令碼的開銷由輸入值的長度決定。
我們先了解 SLOAD
和 SSTORE
指令碼,因為這兩個指令碼受 「柏林」 影響最大。後面我們會再談談那些以地址為目標的指令,比如所有的 EXT*
類指令碼和 CALL*
類指令碼,因為它們的 Gas 開銷也被改變了。
「柏林」以前的 SLOAD
在 EIP-2929 實施前,SLOAD
開銷的計算方式很簡單:總是消耗 800 gas。
「柏林」以前的 SSTORE
要講到 Gas 消耗量的計算,SSTORE
指令碼可能是最複雜的了。因為消耗多少取決於該儲存槽當下的值、要寫入的新值、該儲存項是否已經修改過。我們只會分析少數幾種場景,了解個大概。如果你想了解更多,請閱讀本文末尾所附的 EIP 連結。
- 如果儲存項的值從 0 改為 1(或者任意非零的值),Gas 消耗量是 20000
- 如果儲存項的值從 1 改為 2(或者任意非零的值),Gas 消耗量是 5000
- 如果儲存項的值從 1(或任意非零的值) 改為 0,消耗量也是 5000,但你會在事務執行結束後獲得 gas 補貼。我們這裡也不討論 gas 返還機制,因為它不會受到柏林的影響
- 在一筆事務中,如果儲存項已不是第一次修改,則後續每一次 SSTORE 都消耗 800 gas
細節在這裡並不重要,重要的是,SSTORE 是昂貴的,具體消耗多少 gas 則依賴於多個因素。
延伸閱讀:以太坊有救?改革「ETH手續費市場機制」的 EIP 1559,今年 7月「倫敦硬分叉」將推出
EIP-2929 之後的 Gas 消耗量
EIP-2929 改變了所有這些數值。但在開始之前,我們要先談談該 EIP 引入的一個重要概念:被訪問過的地址和被訪問過的儲存項的鍵(storage key)。
當一個地址或者一個儲存項的鍵,在一筆事務中被 「使用過」 之後,在該筆交易剩下的執行過程中,這個地址(或者這個鍵)都會被當成「已被訪問過的」處理。
舉個例子,如果你在一筆事務中 CALL
(調用)另一個合約,那麼該合約的地址就會被標記為 「訪問過的」。類似地,如果你 SLOAD
或者 SSTORE
過一些儲存項槽 ,在該筆事務剩下的執行過程裡,這些槽也會被當成已經訪問過的。
和使用的指令碼無關,即使你只 SLOAD
過某個槽,接下來使用 SSTORE
時,該槽也會被當成已訪問過的。
要注意的是:儲存項的鍵是「內在於」某些地址中的,正如該 EIP 所解釋:
執行事務時,維持單一集合:
accessed_addresses: Set[Address]
以及accessed_storage_keys: Set[Tuple[Address, Bytes32]]
也就是說,當我們說某個儲存槽已被訪問過了,我們的實際意思是:(address, storageKey)
已被訪問過了。
搞清楚了這個概念,我們來談談新的 Gas 消耗量計算模式。
「柏林」以後的 SLOAD
升級前,SLOAD
的 Gas 消耗量是固定的 800。但升級後,Gas 消耗量要看這個儲存槽是否已經被訪問過。
還沒訪問過的,消耗量就是 2100 gas;訪問過的,就是 100 gas。所以,如果某個儲存項槽已經在「已訪問過的儲存項鍵」的集合裡了,就可以省掉 2000 gas。
「柏林」以後的 SSTORE
我們一個一個對比一下,在 EIP-2929 實施後,上面的幾個例子會發生什麼樣的變化:
- 如果儲存項的值從 0 改為 1(或者任意非零的值),Gas 消耗量是 20000
- 如果該儲存項鍵還未訪問過,消耗 22100 gas
- 若已訪問過,消耗 20000 gas
- 如果儲存項的值從 1 改為 2(或者任意非零的值),Gas 消耗量是 5000
- 如果該儲存項鍵還未訪問過,消耗 5000 gas
- 若已訪問過,消耗 2900 gas
- 如果儲存項的值從 1(或任意非零的值) 改為 0,消耗量保持不變,gas 返還機制也不變
- 在一筆事務中,如果儲存項已不是第一次修改,則後續每一次
SSTORE
都消耗 100 gas
由此可見,如果某個槽此前已訪問過,則對它的第一次 SSTORE
指令會節省 2100 gas(相比於從未訪問過)。
延伸閱讀:鏈上數據|一文看懂「以太坊世界的現況、各個趨勢、未來」,Defi Eth2 NFT 正加速擴張
總結一下
上面的文字實在囉嗦,我們就直接做一張表,把上面提到的值都匯總一下:
指令碼 | 「柏林」前 | 「柏林」後 | |
未訪問過 | 訪問過 | ||
SLOAD | 800 | 2100 | 100 |
SSTORE from 0 to 1 | 20000 | 22100 | 20000 |
SSTORE from 1 to 2 | 5000 | 5000 | 2900 |
SLOAD+SSTORE* | 5800 | 5000 | 3000 |
SSTORE*+SLOAD | 5800 | 5100 | 3000 |
SSORE一個已經被寫過的槽 | 800 | 100 | |
*從一個非 0 值改為另一個非 0 值,如第三行 |
注意看最後一行:此時已不再需要區分它到底有沒有被訪問過,因為如果在此之前已被寫入,則必定已被訪問過。
EIP-2930:可選「訪問清單」的事務類型
另一個「柏林」升級包含的 EIP 是 2930。
該 EIP 加入了一種新類型的事務,可以在事務的負載中包含一個「訪問清單」,意思是,你可以在事務執行前就聲明哪些地址和儲存槽應被認為是「訪問過的」 。
舉個例子,對一個未訪問過的槽執行 SLOAD
需要耗費 2100 gas,但如果該儲存槽被包含在了事務的「訪問清單」中,則指令的消耗量機會降為 100 gas。
但如果只要地址和槽被當成「已訪問過的」就可以降低指令的 Gas 消耗量;而訪問清單可以把地址和槽標記為「已訪問過的」;那豈不是說我們可以把這些東西都放在訪問清單中,來獲得 Gas 消耗量的減免?真棒,天賜 Gas!
並不完全如此,因為你每添加一個地址或儲存項鍵,都要支付額外的 Gas。舉個例子。假如我們要向合約 A 發送了一條事務。我們編寫了一條這樣的訪問清單:
accessList: [{
address: "<address of A>",
storageKeys: [
"0x0000000000000000000000000000000000000000000000000000000000000000"
]
}]
如果我們發送了一條帶有這條訪問清單的事務,而使用 0x0
儲存槽的第一個指令碼就是 SLOAD
,則 Gas 消耗量會是 100 而非 2100,也就是減免了 2000 gas。
但是,在訪問列表中聲明一個儲存項鍵需要額外支付 1900 gas,所以我們只節約了 100 gas。 (如果對該儲存槽的第一個指令是 SSTROE,我們在一個指令中就省下了 2100 gas,也就是總共省下了 200 gas,因為訪問清單本身需要消耗 gas)。
這是不是說,每次使用訪問清單我們都能節省 gas 呢?很遺憾,也不是,因為在訪問清單中填入地址也需要支付 gas。 (也就是我們示例中的 "<address of A>"
)
訪問過的地址
迄今為止,我們只討論了 SLOAD
和 SSTORE
指令碼,但「柏林」升級還改變了別的指令碼。舉個例子,CALL 指令碼原來的 Gas 消耗量為固定的 700,但 2929 實施後,如果所調用的地址不在訪問清單中,消耗量將提高到 2600;而如果在,則降低為 100。
而且,就像訪問過的儲存鍵一樣,到底哪個指令碼訪問過那個地址並不重要(比如,如果用戶最先調用的是EXTCODESIZE
,這一個指令的消耗量是2600,但後續的調用,只要是對同一個地址的,無論是EXTCODESIZE
、CALL
還是STATICCALL
,都只消耗100 gas。
那個這個設計對帶有訪問清單的事務有何影響?假設我們向合約 A 發送一條交易,而合約 A 調用了合約 B,而我們在訪問清單中寫入這樣的內容:
accessList: [{ address: "<address of B>", storageKeys: [] }]
我們首先需要為在這條事務的訪問清單中加入這個地址支付 2400 gas,但對 B 使用的第一個指令碼就只需要消耗 100 gas 而不是 2600 gas,這就剩下了 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 個鍵,才能賺回來。
而且,如你所見,自己一五一十地分析這些因素、手動生成訪問清單,顯然是極其繁瑣而令人崩潰的事。好在,還有更好的辦法。
延伸閱讀:獨立觀點|以太坊世界停擺 1 天!如何看待「Infura 節點崩潰」及其造成的影響?
eth_createAccessList RPC 方法
Geth 客戶端(從 1.10.2 開始)將包含一個新的 eth_createAccessList
RPC 方法,你可以用它來生成訪問清單,就像使用 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]a
}
tx.accessList = accessList;
sendTransaction(tx)
防止合約變磚
值得注意的是,訪問清單功能的主要目的不是節省 Gas。如該 EIP 所述:
緩解由EIP-2929 帶來的合約變磚風險,因為事務可以預先指定、預先支付自身嘗試範文的賬戶和儲存槽。
因此,在實際的執行中,SLOAD 和 EXT* 指令碼都只會消耗 100 gas :這個值低到既足以防止2929 打破某些合約,也可以「解封」被 EIP-1884 封印的合約。
原本,只要一個合約預設了執行的 Gas 開銷,指令碼的 Gas 消耗量變動就有可能導致它變磚。
比如,如果一個合約預設另一個合約的 someFunction 只會用到 34500 gas,因此總是用 someOtherContract.someFunction{gas: 34500}() 調用那個合約,這個合約就有可能變磚。但只要你在事務中添加合適的訪問清單,這個合約就還能運作。
自己驗證
如果你想自己測試一下,複製這個資料庫,這裡面有很多例子,可以使用 Hardhat 和 Geth 客戶端來運行。請仔細閱讀 README 裡的說明。
參考文獻
- EIP-2929 和 EIP-2930 是兩個跟本文有關的 「柏林」 EIP。
- EIP-2930 依賴於 「柏林」 升級納入的另一個 EIP:EIP-2718,也叫標準化的事務信封。
- EIP-2929 大量參考了 EIP-2200,如果你想更深入地理解 Gas 消耗量,你應該從那裡開始。
- 想了解更複雜的情形中 Gas 消耗量會如何變化,請看這裡。
📍相關報導📍
礦工號召大反攻!「愚人節」遷移算力「51%持續51小時」,武力逼宮以太坊 EIP-1559
V神:以太坊 Rollups「快來了」!無須分片就可擴容百倍,最高達4,000TPS
解讀|除了美圖秀秀,為何上市公司狂買BTC不愛以太坊?機構會是ETH新支柱嗎?
讓動區 Telegram 新聞頻道再次強大!!立即加入獲得第一手區塊鏈、加密貨幣新聞報導。
LINE 與 Messenger 不定期為大家服務