JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Gas:為運算計量,讓沒人能對鏈發動阻斷攻擊

每個全節點都得重跑每一筆交易——那麼,是什麼阻止了一行無窮迴圈凍結整個網路?答案是 gas:一只為每個操作碼定價、為每筆交易設限、把「停機問題」化成一張帳單的計量表。

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
EVM gas 價目表的一個片段。ADD(3)與全新 SSTORE(22,100)之間 700 倍的落差,就是整個 gas 最佳化的故事核心。

有兩個細節很重要。第一,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.
為「3 + 5 再存起來」定價。單單一次儲存寫入,就重過其他所有項目的總和。

教訓直白得很:在 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)
一筆 EIP-1559 費用,拆成被銷毀的基礎費與提議者的小費。1 gwei = 0.000000001 ETH。

有兩個誠實的後果。基礎費讓費用變得可預測——你能看見下一塊的下限,並有信心地設定 `maxFeePerGas`,不必盲目出價。而銷毀基礎費會把以太幣移出供給;當網路使用量高時,被銷毀的以太幣多過發給驗證者的新幣,使以太幣淨通縮——但僅當使用量高時。在清淡時期,發行量超過銷毀量,供給仍會成長。這是一種取決於使用量的壓力,而非保證。

為什麼儲存主宰帳單——以及如何少付一點

回到那 700 倍的落差。為什麼 `ADD` 幾乎免費,`SSTORE` 卻貴得殘忍?因為儲存不是暫存空間——它是世界狀態裡的一筆永久條目,每一個節點都必須永遠保存、複製並證明它。一個過渡性的計算在呼叫結束的瞬間就蒸發了;一個被儲存的字組,卻是整個網路在鏈的餘生中都得扛起的負債。gas 誠實地為這份不對稱定價:你預先一次付清,買下你強加給所有人的、無上界的未來成本。

這有個反面:釋放儲存可以賺回退款。把一個槽清回零(`SSTORE X -> 0`)會退還 gas,因為你縮小了大家都要扛的狀態。但退款有上限——最多是該筆交易總 gas 的 20%(EIP-3529)。這個上限的存在有實在的理由:交易者曾在 gas 便宜時寫入垃圾儲存、又在 gas 昂貴時清掉以換退款,藉此鑄造「gas 代幣」(GST2、CHI),等於把便宜的 gas 存起來日後再花。EIP-3529 大砍退款,既為了終結這場遊戲,也為了避免大筆退款動搖剛上路的基礎費機制。

正因為 gas 帳單主要來自儲存,gas 最佳化也就主要是儲存的紀律。槓桿最大的幾個做法,大致依影響力排序:

  1. 盡可能少寫儲存。把一個槽讀進區域記憶體變數,所有運算都在那裡做,最後只寫回一次——別在迴圈裡寫。一次 SSTORE 勝過十次。
  2. 把變數打包進共用的槽。一個儲存槽是 256 位元;四個 `uint64`,或一個 `uint64`+`address`+`bool`,可以共用一個槽,讓 EVM 用單次 SSTORE 而非多次更新它們。
  3. 對不需要持久化的資料,優先用 calldata 與記憶體。從 calldata 讀取函式參數,遠比把它們複製進儲存便宜。
  4. 對只在鏈下需要的資料,改用事件(event)而非儲存。一個 LOG 遠比 SSTORE 便宜,而索引器能讀取事件——只在合約自己必須查詢的狀態上才動用儲存。
  5. 善用 warm/cold。如果你非得碰同一個槽或外部帳戶兩次,第二次存取約 100 gas 而非約 2100——所以把第一次讀到的快取起來重複用。