Gas 為何存在:一只防堵無窮迴圈的計量表
想像對以太坊最迷你的攻擊:一份合約的內容只有 `while (true) {}`。送出一筆交易去呼叫它。現在回想 EVM 究竟是什麼——它不是一台電腦,而是地球上每一個全節點都重跑同一段程式碼以達成對結果的共識。沒有煞車,這個迴圈就會在數千台機器上同時永遠空轉。鏈會直接停止出塊。一個垃圾訊息攻擊者,只用一筆交易,就能讓整個網路癱瘓。
要事先判斷任意程式是否會停機,正是著名的停機問題——已被證明在一般情況下不可能。以太坊的設計並不試圖解決它,而是繞過它:為每一步運算收費,給每筆交易一份有限的預算,預算用罄的那一刻就停下來。這份預算以 gas(燃料)計價,而為每一步開帳單的規則就是 gas 計量。無窮迴圈不再危險——它只會燒光自己的預算然後被切斷。
所以 gas 本質上是一種阻斷服務(DoS)防禦。它讓運算的花費正比於它所需的工作量,這正是為什麼一份惡意騷擾合約無法免費運行。其餘的一切——費用市場、最佳化、你兌換代幣時付的價格——全都建立在這一個概念之上:工作會被計量,而被計量的工作就必須付費。
每個操作碼都掛著一張價目牌
上一階你已認識 EVM 是一台一次執行一個操作碼的堆疊機。gas 系統替這些操作碼各自釘上一個固定價格,定義在以太坊的黃皮書裡。在堆疊上做廉價算術只要幾單位 gas;碰一下永久性的儲存卻要好幾千。以下是價目表的代表性片段(Berlin 升級後,含 EIP-2929 的 warm/cold 規則):
Opcode Gas Note ------------------------------------------------------- STOP 0 halts; costs nothing ADD / SUB 3 G_verylow (cheap math) MUL 5 G_low PUSH1 .. PUSH32 3 push a constant onto the stack POP 2 G_base MLOAD / MSTORE 3 + memory-expansion cost KECCAK256 30 + 6 per 32-byte word hashed SLOAD (cold) 2100 FIRST read of a storage slot SLOAD (warm) 100 slot already touched this tx SSTORE 0 -> X 22100 write a FRESH slot (20000 + 2100 cold) SSTORE X -> Y 5000 overwrite an existing non-zero slot LOG1 750+ 375 base + 375/topic + 8/byte of data CALL 100+ warm; +9000 to send ETH; +25000 new acct CREATE 32000 deploy a new contract
有兩個細節很重要。第一,warm 對 cold(EIP-2929,Berlin 分叉):交易在一個區塊裡第一次讀取某個儲存槽或某個帳戶時,要付昂貴的「cold(冷)」價;之後再碰同一個槽都很便宜、是「warm(熱)」的。協定向你收取的是「把新資料從磁碟撈出來」的代價,而不是「重讀節點快取中已有資料」的代價。第二,記憶體的成本是平方成長——見下方提醒。
實作範例:把帳單加總起來
我們來把一筆迷你交易從頭到尾算清楚。這份合約只計算 `3 + 5`,並把結果存進一個原本為零的狀態變數。有兩項成本落在操作碼之外:每筆交易都要付的固定起始成本 21,000 gas(涵蓋簽章還原、nonce 檢查與基礎開銷),再加上你送入資料的 calldata——每個零位元組 4 單位 gas、每個非零位元組 16 單位 gas。現在把操作碼加上去:
// Solidity: function set() external { stored = 3 + 5; } // stored was 0
Transaction intrinsic cost ........................ 21000
PUSH1 0x03 (push 3) ............................ 3
PUSH1 0x05 (push 5) ............................ 3
ADD (3 + 5 = 8 on the stack) ........... 3
PUSH1 0x00 (push storage slot 0) .............. 3
SSTORE (slot 0: 0 -> 8, a FRESH write) ..... 22100
--------------------------------------------------------
TOTAL ............................................. 43112 gas
// The single SSTORE is 22100 / 43112 = 51% of the entire bill.
// The actual arithmetic (ADD) is 3 gas — 0.007% of the cost.教訓直白得很:在 EVM 裡,運算很便宜,記住很昂貴。做這道算術花 3 單位 gas;把一個 32 位元組的字組永久寫進全世界共享的狀態裡,卻要 22,100。這個比例形塑了每一份正經合約的寫法,也是本篇接下來一再回到儲存這個主題的原因。
Gas 上限、用罄,以及無論如何都要付錢的人
你簽署的每一筆交易都帶著一個 gas 上限——你授權的最大工作量單位數。錢包會估算你的呼叫大約需要多少(上面的 43,112)並留些餘裕。如果執行過程都沒超過上限,你只會被收取實際消耗的 gas,未動用的額度永遠不會被花掉。到目前為止都很直覺。
現在來看違反直覺的部分。如果執行在完成前就撞上 gas 上限,EVM 會丟出 out-of-gas(gas 耗盡)例外,並回退這筆交易做過的每一項狀態變更——彷彿它從未執行。但你仍要為它一路燒掉的所有 gas 付費。狀態被回滾了,費用卻不會退還。在你想起 gas 為何存在之前,這感覺很不公平:數千個節點確實執行了那些工作,總得有人補償他們——否則「免費的 out-of-gas 回退」本身就會變成 DoS 的破口。
還有第二道、宏觀層級的上限:區塊 gas 上限。一整個區塊只能容納價值一定 gas 量的交易(目標為 1,500 萬 gas,可彈性拉到 3,000 萬——下一節詳述)。這是全網級的 DoS 界線:它保證任何單一區塊所要求的運算量,都不會超過誠實節點在兩塊之間的時間內能重跑完的份量,讓驗證始終落在一般硬體能負荷的範圍內。
EIP-1559:基礎費、小費與銷毀
我們已經為工作定了價;現在來談每單位的價格。在 2021 年 8 月之前,以太坊跑的是粗糙的「第一價格拍賣」:每個人猜一個 `gasPrice`,出價最高者得,出塊者全數收進口袋。使用者經常付太多、費用劇烈波動,錢包還得扮演預言機。EIP-1559(London 分叉)以一個透明、大致可預測的市場取代了它。
在 EIP-1559 之下,每個區塊都有一個由協定設定的、每單位 gas 的基礎費。你不對它出價——它是被算出來的。一個區塊之後,基礎費會依前一塊相對於 1,500 萬 gas 目標的滿載程度,最多調整 ±12.5%:滿載(3,000 萬)的區塊把它往上推 12.5%,空塊往下拉 12.5%,半滿的區塊則維持不變。關鍵在於,基礎費會被銷毀——直接消滅、付給任何人。在它之上,你再加一筆優先費(小費),這筆才會進到出塊提議者口袋,作為把你納入區塊的誘因。
// You set: maxPriorityFeePerGas = 2 gwei (tip to proposer)
// maxFeePerGas = 30 gwei (your hard ceiling)
// Network: base fee = 20 gwei (burned)
// Your tx uses 43112 gas (the example above).
effective price = min(maxFee, baseFee + priorityFee)
= min(30, 20 + 2) = 22 gwei
total fee = 43112 * 22 gwei = 948,464 gwei ~= 0.00095 ETH
burned = 43112 * 20 gwei = 862,240 gwei (removed from supply)
to proposer = 43112 * 2 gwei = 86,224 gwei (the tip)
refunded to you = (maxFee - effective) * gas
= (30 - 22) * 43112 gwei (you escrow maxFee, pay less)有兩個誠實的後果。基礎費讓費用變得可預測——你能看見下一塊的下限,並有信心地設定 `maxFeePerGas`,不必盲目出價。而銷毀基礎費會把以太幣移出供給;當網路使用量高時,被銷毀的以太幣多過發給驗證者的新幣,使以太幣淨通縮——但僅當使用量高時。在清淡時期,發行量超過銷毀量,供給仍會成長。這是一種取決於使用量的壓力,而非保證。
為什麼儲存主宰帳單——以及如何少付一點
回到那 700 倍的落差。為什麼 `ADD` 幾乎免費,`SSTORE` 卻貴得殘忍?因為儲存不是暫存空間——它是世界狀態裡的一筆永久條目,每一個節點都必須永遠保存、複製並證明它。一個過渡性的計算在呼叫結束的瞬間就蒸發了;一個被儲存的字組,卻是整個網路在鏈的餘生中都得扛起的負債。gas 誠實地為這份不對稱定價:你預先一次付清,買下你強加給所有人的、無上界的未來成本。
這有個反面:釋放儲存可以賺回退款。把一個槽清回零(`SSTORE X -> 0`)會退還 gas,因為你縮小了大家都要扛的狀態。但退款有上限——最多是該筆交易總 gas 的 20%(EIP-3529)。這個上限的存在有實在的理由:交易者曾在 gas 便宜時寫入垃圾儲存、又在 gas 昂貴時清掉以換退款,藉此鑄造「gas 代幣」(GST2、CHI),等於把便宜的 gas 存起來日後再花。EIP-3529 大砍退款,既為了終結這場遊戲,也為了避免大筆退款動搖剛上路的基礎費機制。
正因為 gas 帳單主要來自儲存,gas 最佳化也就主要是儲存的紀律。槓桿最大的幾個做法,大致依影響力排序:
- 盡可能少寫儲存。把一個槽讀進區域記憶體變數,所有運算都在那裡做,最後只寫回一次——別在迴圈裡寫。一次 SSTORE 勝過十次。
- 把變數打包進共用的槽。一個儲存槽是 256 位元;四個 `uint64`,或一個 `uint64`+`address`+`bool`,可以共用一個槽,讓 EVM 用單次 SSTORE 而非多次更新它們。
- 對不需要持久化的資料,優先用 calldata 與記憶體。從 calldata 讀取函式參數,遠比把它們複製進儲存便宜。
- 對只在鏈下需要的資料,改用事件(event)而非儲存。一個 LOG 遠比 SSTORE 便宜,而索引器能讀取事件——只在合約自己必須查詢的狀態上才動用儲存。
- 善用 warm/cold。如果你非得碰同一個槽或外部帳戶兩次,第二次存取約 100 gas 而非約 2100——所以把第一次讀到的快取起來重複用。