以太坊中的 13 個 Gas 模型不一致問題
這篇文章總結了以太坊 gas 模型中的 13 個不一致之處。
所有符號均來自以太坊黃皮書(特別是附錄 G.費用表)。
1. 創建賬戶時,外部交易不會產生新賬戶費用( G_ { newaccount } G newaccount ) ,但內部交易會產生
例子
假設你想讓合約C
將ETH轉移到一個尚不存在的全新賬戶A
- 如果
C
直接轉賬,內部新建賬戶路徑收取25,000 gas ( G_ { newaccount } G newaccount ) 。 - 替代方案:首先從 EOA 向
A
發送一筆外部交易,費用為 1 wei。這筆交易的Gas 成本為 21,000(標準基數)。之後,A
就存在了。現在C
向A
轉賬不再產生新賬戶費用(節省約 4,000 Gas )。
結論
如果你依靠合約來創建新賬戶,則需要支付 25,000 gas。
但是如果你先發送一個外部交易(21,000 gas),然後讓合約發送資金,你實際上可以節省~4,000 gas 。
原因
外部和內部創建路徑使用不同的收費掛鉤;只有合約調用才會觸發明確的新賬戶費用。
2. 預編譯調用有時會跳過交易輸入字節費用
例子
調用預編譯合約(例如ECRECOVER
)可能會跳過交易輸入字節的計費。通常情況下,每個輸入字節的Gas 費用為 4(零)或16(非零) 。以下是兩個真實的交易示例:
* 1. 交易0x6b01使用 24,276 gas 調用ECRECOVER
,其中 276 gas 用於輸入字節。
* 2. 交易0x1fb0使用 24,000 gas 調用ECRECOVER
,其中 0 gas 用於輸入字節。
如果你閱讀執行客戶端的源代碼,你會發現第二個事務也可以執行ECRECOVER
,而無需對輸入字節進行計費。客戶端會用零數據填充輸入以匹配預期的大小。
補充說明
我想你足夠聰明,會注意到我提到的兩筆交易都是外部交易。
那麼他們為什麼要調用預編譯合約呢?
我猜部分原因是一些瀏覽器錯誤地將預編譯地址(0x01-0x0A)標記為“刻錄地址”,這進一步使用戶感到困惑(請參見下面的快照)。
此外,在這些特殊地址(0x01–0x0A)中部署預編譯地址是一個失敗的設計。
有時,人們只是想直接呼叫這些特殊地址。
原因
預編譯合約的地址設計不良以及區塊瀏覽器的誤導,導致混亂和錯誤標記。
3. 即使從未訪問過,訪問列表條目也會收費(即G_{accesslistaddress} G a c c e s s l i s t a d r e s s 、 G_{accessliststorage} G a c c e s s l i s t s t o r a g e )
例子
EIP-2930 引入了訪問列表,允許交易指定其想要訪問的地址和存儲槽。然而,交易可以將地址和存儲槽添加到其訪問列表中,但永遠不會觸及它們。
例如,事務0x0dd0c設置了訪問列表,但由於地址原因從未訪問指定的插槽。
原因
該協議收取包含費用以簡化執行,無論條目是否被使用。如果你相信你的用戶能夠提供正確的輸入,那你還不如相信泰勒·斯威夫特是你的妻子。
4. 自轉移仍需轉移氣體
例子
賬戶 A 將ETH發送給自己。
餘額沒有變化,但費用仍然包含價值轉移所需的9,000 gas ( G_{ callvalue } G callvalue ) 。根據@vbuterin的這篇帖子 。
兩次賬戶寫入(一次餘額編輯 CALL 通常需要花費 9000 gas)
為什麼一個賬戶寫入操作仍然需要花費9000 gas呢?其實,如果你看過執行客戶端的源碼,就會發現,當 from 地址和 to 地址相同時,客戶端不會做任何操作。
當交易為自我轉賬或使用CALLCODE轉移價值時,可能會發生上述情況。
原因
無論轉移是否為無操作,都會觸發執行費用。
5. 調用數據與合約字節碼磁盤定價不匹配
例子
- Tx calldata: 16 gas/字節(非零)或4 gas/字節(零) 。
- 合約字節碼: 200 gas/字節。
兩者都佔用磁盤空間,但定價卻不一致。這讓我很困惑,因為交易調用數據比合約字節碼更便宜,因為它應該考慮實際的磁盤使用量和網絡開銷。
原因
Gas 計劃將“調用數據”和“代碼存放”分開,而不將它們與實際磁盤使用情況對齊。
6. 撤銷的交易將按寫入磁盤的方式收費
例子
恢復的交易會修改內存中的狀態,但不會保留任何更改,但仍會收取寫入費用,以下 gas 費用會受到影響:
- 25,000 gas (新賬戶, G_ { newaccount } G newaccount )
- 9,000 gas (價值轉移, G_ { callvalue } G callvalue )
- 2,100 gas (冷槽, G_ { coldslot } G冷槽)
- 200 gas (代碼存款, G_ { codedeposit } G代碼存款)
實際內存成本約為100 gas 。
原因
執行期間會收取 Gas 費用;之後的回滾會取消狀態更改,但不會取消費用。具體實現會謹慎收費,以防止 DoS 攻擊。
7. 單筆交易中的多筆ETH轉賬被誤認為是冷轉賬
例子
假設合約在單筆交易中多次將ETH發送到不同的賬戶。
- 第一次轉賬正確產生了G_{callvalue} G call value ( 9,000 gas )寫入賬戶餘額。
- 同一交易中後續向其他賬戶的轉賬應收取熱訪問費( 100 gas + 4,500 gas ),但有時仍按冷訪問費計費( 9,000 gas )。
原因
對於單筆交易中的多次價值轉移,冷/暖訪問簿記並不會持續更新。
8. 礦工/驗證者獎勵或提現寫入不收取費用
例子
協議級餘額更新(例如獎勵、提款)會修改磁盤上的狀態,但消耗0 gas 。
原因
系統級簿記繞過了 gas 會計掛鉤。
9. SSTORE 首次磁盤讀取不收費(根據 EIP-2200)
例子
執行SSTORE
操作碼時,它首先從磁盤(合約存儲)讀取當前值,然後再決定是否寫入新值。根據EIP-2200規定,如果存儲的值與現有值匹配,則不會進行磁盤寫入,並且僅收取少量 Gas 費用。但是,初始磁盤讀取本身不收取任何 Gas 費用——協議僅在值發生變化時對後續寫入收取費用。
原因
EIP-2200 的邏輯側重於對狀態變化收費,但忽略了對總是首先發生的磁盤讀取的收費。這意味著對存儲槽的首次訪問是免費的,即使是冷讀。
10. 存儲讀取優化減少了 I/O,但 gas 保持不變
例子
以太坊客戶端已採用扁平存儲/快照優化(例如,Geth 的 快照加速結構),將狀態組織為扁平鍵值存儲,並允許直接磁盤讀取,從而繞過了傳統 Merkle-Patricia Trie (MPT) 所需的中間節點。此優化顯著減少了冷存儲讀取的磁盤 I/O。例如,Geth 和其他客戶端現在使用 SAS 或類似結構,但冷訪問的 Gas 費用(2,600 / 2,100 / 2,400 / 1,900)保持不變。
原因
冷訪問的 Gas 常數最初是針對 MPT 進行校準的,因為 MPT 需要遍歷多個 trie 節點,因此磁盤讀取成本更高。使用 SAS 後,實際磁盤資源消耗要低得多,但協議尚未更新相應的 Gas 費用。
減輕
當客戶端切換到 SAS 或類似的優化存儲後端時,重新校準氣體常數以反映減少的磁盤 I/O。
11. SLOAD 與 MLOAD 定價不匹配
例子
SLOAD
(溫暖)→ 100 氣體MLOAD
→ 3 氣體
兩者都是內存讀取,但是價格差別很大。
原因
狀態和內存操作之間的遺留區別;優化模糊了實際成本差距。
12. 內部交易有時無需 Gas 即可更新賬戶
例子
當磁盤中的賬戶更新不收取 Gas 費用時。具體來說,這個問題出現在用戶向合約 A 發送外部交易,合約 A 又對合約 B 進行內部調用的場景中。如果合約 B 修改了其存儲中的某個 slot,則必須在磁盤上更新合約 B 賬戶中對應的存儲根。然而,由於賬戶 B 的此次更新不收取 Gas 費用,導致不一致。
原因
出現此漏洞的原因是,合約 B 的存儲 trie 修改不會因更新其賬戶狀態而產生額外的 gas 費用。這是因為即使執行了磁盤寫入操作,協議也不會對由內部交易觸發的賬戶狀態更新收取費用。
13. EXT* 操作碼定價太粗略
例子EXTCODESIZE
可能讀取比BALANCE
更多數據,但兩者都收取相同的冷賬戶費用( 2,600 gas )。
原因
操作碼定價桶很粗略,並且忽略了可變的工作。
結束語
這個問題來自我的論文如下,我通過此鏈接分享它。
何哲、李哲、羅建、羅鋒、段建、李建…… & 張曉燕 (2025年2月)。Auspex:揭示區塊鏈交易費機制的不一致性漏洞。刊於第23屆USENIX文件與存儲技術會議論文集。
如果您能引用我的論文,我將非常高興。
所有這些都凸顯了全面審查和調整以太坊協議內 Gas 定價機制的必要性。通過解決這些不一致問題,我們可以確保一個更高效、更公平的 Gas 市場,準確反映各種操作的底層資源成本。