以太坊 EVM Opcode 數學推導與 Go-ethereum 原始碼深度分析:理解 Gas 消耗的底層邏輯
本文從數學推導和原始碼兩個維度深入理解 EVM Opcode 的 Gas 模型。我們推導 ADD/MUL 的計算複雜度、SHA3 的成本公式、SLOAD 動態定價的原理,並深度解析 go-ethereum 核心原始碼中 gas_table.go 的實現邏輯。涵蓋記憶體二次方成本模型、SSTORE 退款機制的數學基礎、以及 EIP-4844 Blob 交易的新成本模型。
以太坊 EVM Opcode 數學推導與 Go-ethereum 原始碼深度分析:理解 Gas 消耗的底層邏輯
老實說,學 EVM 這麼久,我發現最讓人頭疼的不是「合約怎麼寫」,而是「為什麼 Gas 要這麼貴」。每次看到 Gas 估算失誤導致交易失敗的時候,我都忍不住想:底層到底在算什麼?
這篇文章,我要帶你從數學推導和原始碼兩個維度,深入理解 EVM Opcode 的 Gas 模型。不會給你一堆表格讓你死記硬背——我要讓你理解「為什麼這個操作要這麼貴」,然後你自己就能推斷Gas消耗。
一、Gas 模型的基本原理
1.1 為什麼要有 Gas?
以太坊的 Gas 模型不是設計師拍拍腦袋想出來的,它是對「計算資源有成本」這個現實的市場化表達。
Gas 在以太坊中的角色:
┌─────────────────────────────────────────────────────────────┐
│ 為什麼需要 Gas? │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 激勵兼容(Incentive Compatibility) │
│ - 礦工/驗證者需要補償他們運行節點的成本 │
│ - 用戶需要為自己的計算行為付費 │
│ - 沒有 Gas = 免費計算 = 無限迴圈攻擊 │
│ │
│ 2. DOS 防禦(DOS Resistance) │
│ - 攻擊者發送大量無用交易消耗網路資源 │
│ - Gas 費用讓這種攻擊變得極其昂貴 │
│ │
│ 3. 資源定價(Resource Pricing) │
│ - 不同的 EVM 操作消耗不同數量的計算/記憶體/儲存 │
│ - Gas 提供了一個統一的定價單位 │
│ │
│ 4. 市場均衡(Market Equilibrium) │
│ - Base Fee 動態調整,讓區塊空間的供需達到平衡 │
│ │
└─────────────────────────────────────────────────────────────┘
1.2 EVM 操作的三種成本層次
每個 EVM Opcode 的 Gas 成本不是一個數字那麼簡單,它反映了三個層次的資源消耗:
# EVM Opcode 成本的三層模型
gas_cost_components = {
"計算成本 (Computation Cost)": {
"定義": "執行 Opcode 本身的 CPU 週期消耗",
"單位": "Gas",
"特點": "靜態、確定的(大部分情況下)",
"示例": "ADD = 3 Gas, MUL = 5 Gas, SHA3 = 30 + 6 × words"
},
"記憶體成本 (Memory Cost)": {
"定義": "擴展 EVM 記憶體空間的邊際成本",
"模型": "非線性遞增",
"特點": "與記憶體大小相關,但只計算「最大使用量」",
"代碼": "G_mem = 3 * (words² / 512) + G_memory"
},
"儲存成本 (Storage Cost)": {
"定義": "寫入區塊鏈狀態的永久存儲成本",
"特點": "最貴,但有「退款」機制",
"示例": "SSTORE (新值) = 20000 Gas, SSTORE (修改) = 5000 Gas"
}
}
print("EVM Opcode Gas 成本的三層模型")
print("=" * 60)
for layer, details in gas_cost_components.items():
print(f"\n【{layer}】")
for k, v in details.items():
print(f" {k}: {v}")
二、核心 Opcode 的數學推導
2.1 算術運算Opcode:為什麼 ADD 比 MUL 便宜?
這個問題看似簡單,但背後有具體的晶片設計考量。
# 256 位元整數運算的複雜度分析
import math
class ArithmeticComplexityAnalysis:
"""
EVM 使用 256 位元(32 位元組)整數運算
這決定了算術 Opcode 的 Gas 成本
"""
@staticmethod
def bit_complexity_analysis():
"""
運算複雜度理論基礎
在 256 位元處理器上:
- 8 位元處理器:需要 32 個週期做 256 位元加法
- 32 位元處理器:需要 8 個週期做 256 位元加法
- 原生 256 位元處理器:需要 1 個週期(理論)
EVM 是軟體模擬(以太坊黃皮書定義),所以:
- 所有 256 位元運算都是軟體實現
- ADD 使用簡單進位傳播,複雜度 O(n)
- MUL 使用長乘法或 Karatsuba,複雜度 O(n²) 或 O(n^1.585)
"""
# 256 位元整數的位元數
BITS = 256
BYTES = 32
# 加法複雜度:線性
add_complexity = BYTES # 32 個位元組需要 32 次進位加法
# 乘法複雜度:二次(簡單算法)
mul_complexity_naive = BYTES ** 2 # 32 × 32 = 1024
mul_complexity_karatsuba = 32 ** 1.585 # ≈ 528
print("256 位元整數運算複雜度分析")
print("=" * 60)
print(f"加法 (ADD) 複雜度:{add_complexity} 單位操作")
print(f"乘法 (MUL) 複雜度(暴力):{mul_complexity_naive} 單位操作")
print(f"乘法 (MUL) 複雜度(Karatsuba):{mul_complexity_karatsuba:.0f} 單位操作")
print()
print(f"MUL / ADD 比值:{mul_complexity_karatsuba / add_complexity:.1f}:1")
print(f"Gas 成本比:5 / 3 = {5/3:.1f}:1")
print()
print("觀察:Gas 成本比反映了實際計算複雜度的比例")
@staticmethod
def derive_add_cost():
"""
ADD Gas 成本推導
256 位元加法 = 32 位元組加法
每位元組加法需要:
- 讀取 2 個位元組
- 執行進位加法
- 寫入 1 個位元組
- 處理進位標誌
估計每次位元組操作 = 0.1 Gas 的「計算單元」
總計 = 32 × 0.1 ≈ 3 Gas(向上取整)
"""
return 3
@staticmethod
def derive_mul_cost():
"""
MUL Gas 成本推導
256 位元乘法需要多輪位元組乘法累加
每位元組乘法 = 讀取 2 位元組 → 乘法 → 累加進結果
暴力乘法:32 × 32 = 1024 次操作
Karatsuba:~528 次操作
考慮額外開銷(進位處理、溢出檢測):
MUL ≈ 1.5 × ADD 複雜度
Gas 成本 = 5 Gas(取整)
"""
return 5
analysis = ArithmeticComplexityAnalysis()
analysis.bit_complexity_analysis()
這個分析告訴我們:Gas 成本不是任意設定的,它是計算複雜度的市場化表達。ADD 和 MUL 的 Gas 成本比(約 1.67:1)與它們的計算複雜度比(約 1.5-2:1)大體吻合。
2.2 SHA3(Keccak-256)的成本模型
密碼學 Opcode 的成本是最複雜的,因為它涉及安全的非線性變換。
# SHA3-256 (Keccak-f[1600]) 的 Gas 成本推導
class KeccakCostAnalysis:
"""
SHA3 的 Gas 成本考慮了:
1. 吸收階段(Absorbing):輸入與狀態混合
2. 擠壓階段(Squeezing):輸出生成
3. 內部 f 函數(Keccak-f[1600]):24 輪混合
"""
def __init__(self):
# Keccak-256 參數
self.state_bits = 1600 # 總狀態大小
self.rate_bits = 1088 # 速率(輸入部分)
self.capacity_bits = 512 # 容量(輸出部分)
self.word_bits = 64 # 每個 lane 64 位元
# 輪數
self.rounds = 24
# 每輪的 θ, ρ, π, χ, ι 步驟
self.step_count = 5
def derive_base_cost(self):
"""
基礎 Gas 成本推導
每輪 Keccak-f 需要:
- θ (Theta): 5 × 5 位元組的 XOR 和
- ρ (Rho): 旋轉和移動
- π (Pi): 置換
- χ (Chi): 非線性替換(每個位元組 5 次 XOR)
- ι (Iota): 與輪常數的 XOR
估算每次 θ+ρ+π = 100 個簡單操作
估算每次 χ = 160 個 XOR 操作(5 層)
估算每次 ι = 5 個 XOR
總計每輪 ≈ 270 個操作
24 輪 = 6,480 個操作
但 EVM 的 SHA3 還要考慮記憶體讀取...
"""
# 最終確定的 Gas 公式(黃皮書)
# G_sha3 = 30 + 6 × 詞數
return {
"base_gas": 30,
"per_word_gas": 6,
"公式": "G_sha3 = 30 + 6 × ceil(len(data) / 32)",
"解釋": "30 是固定的「函數調用開銷」,6 是每 32 位元組數據的處理成本"
}
def calculate_cost(self, data_bytes: int) -> dict:
"""計算任意長度輸入的 SHA3 Gas 成本"""
# 詞數(每詞 8 位元組)
words = math.ceil(data_bytes / 32)
# Gas 計算
base = 30
variable = 6 * words
total = base + variable
return {
"輸入位元組": data_bytes,
"詞數": words,
"基礎 Gas": base,
"可變 Gas": variable,
"總 Gas": total,
"每位元組成本": total / data_bytes
}
def compare_with_real_world(self):
"""
比較不同長度輸入的 SHA3 成本
"""
test_cases = [32, 64, 256, 1024, 4096]
print("SHA3 Gas 成本 vs 輸入長度")
print("=" * 60)
print(f"{'輸入大小':<15} {'詞數':<10} {'總 Gas':<12} {'每位元組':<12}")
print("-" * 60)
for bytes_len in test_cases:
result = self.calculate_cost(bytes_len)
print(f"{bytes_len:<15} {result['詞數']:<10} {result['總 Gas']:<12} {result['每位元組成本']:.4f}")
keccak = KeccakCostAnalysis()
keccak.compare_with_real_world()
2.3 SLOAD 的動態定價:EIP-1884 的數學背後
2019 年的 EIP-1884 改變了 SLOAD 的 Gas 成本,這背後有深刻的理由。
# EIP-1884 前後的 SLOAD Gas 成本分析
class SLOADCostAnalysis:
"""
SLOAD (Storage Load) 的成本問題
"""
# EIP-1884 之前的成本
old_cost = 200
# EIP-1884 之後的成本
new_cost = 800
def why_800(self):
"""
為什麼 SLOAD 要 800 Gas?
考慮因素:
1. 狀態嘗試(MPT)的深度
2. 記憶體 vs 儲存的成本差異
3. 讀取 vs 寫入的差異
"""
# 狀態大小的增長
state_size_2020 = "~50 GB" # 估計
state_growth_rate = 1.3 # 年增長 30%
# 讀取一個儲存槽需要遍歷 MPT
# 平均遍歷深度 = log₂(N) 其中 N 是狀態節點數
# 假設 ~1000 萬個 ETH 帳戶
# 每個帳戶平均 ~10 個儲存槽
# 總共 ~1 億個儲存槽
average_mpt_depth = math.log2(100_000_000) # ≈ 27
iops_per_slot_read = average_mpt_depth # 每讀取一個槽需要這麼多次 I/O
# 每個 I/O 操作的成本估計
io_cost_per_read = 50 # 假設每次磁碟讀取 = 50 Gas
# 但 EVM 會快取,所以實際成本更低
cache_hit_ratio = 0.9 # 90% 的讀取命中快取
effective_io_cost = io_cost_per_read * (1 - cache_hit_ratio) + 20 * cache_hit_ratio
print("SLOAD Gas 成本推導")
print("=" * 60)
print(f"MPT 平均深度: {average_mpt_depth:.0f} 層")
print(f"每次讀取估計 I/O 成本: {io_cost_per_read} Gas")
print(f"快取命中後有效成本: {effective_io_cost:.0f} Gas")
print()
print(f"EIP-1884 最終成本: {self.new_cost} Gas")
print(f"這個數字反映了:")
print(f" 1. 狀態增長導致的 MPT 深度增加")
print(f" 2. 讀取操作的真實 I/O 成本")
print(f" 3. 與其他 Opcode(如 CALL)的成本平衡")
def compare_with_balance(self):
"""
比較 SLOAD 和 BALANCE 的成本
這是 EIP-1884 的核心動機
"""
print("\nOpcode 成本比較(EIP-1884 後)")
print("=" * 60)
costs = {
"SLOAD": 800,
"BALANCE": 100,
"EXTCODEHASH": 100,
"CALL": 100
}
for opcode, cost in costs.items():
bar = "█" * (cost // 10)
print(f"{opcode:<15} {cost:>4} Gas {bar}")
sload_analysis = SLOADCostAnalysis()
sload_analysis.why_800()
sload_analysis.compare_with_balance()
EIP-1884 的核心動機是什麼?是狀態訪問攻擊防禦。在這次升級之前,SLOAD 只要 200 Gas,而 BALANCE 要 400 Gas。攻擊者發現:可以透過大量讀取他人的空合約來消耗網路資源,因為讀取空合約只比讀取有餘額的帳戶便宜一半。EIP-1884 把 SLOAD 提升到 800 Gas,讓這種攻擊無利可圖。
三、Go-ethereum 原始碼深度解析
3.1 EVM 解釋器的核心架構
現在讓我們深入 go-ethereum 的原始碼,看看 Gas 計算在代碼層面是怎麼實現的。
// go-ethereum/core/vm/gas.go 的核心結構
// GasRefunds 追蹤 Gas 退款
type GasRefunds struct {
total uint64 // 累積的退款金額
cap uint64 // 最大退款的 cap(通常是實際消耗的 1/5)
}
// Gas pool 用於追蹤區塊剩餘 Gas
type GasPool struct {
gas uint64 // 剩餘 Gas
}
// gasPool 的核心方法
func (gp *GasPool) SubGas(amount uint64) error {
if gp.gas < amount {
return ErrGasUintOverflow
}
gp.gas -= amount
return nil
}
func (gp *GasPool) AddGas(amount uint64) {
gp.gas += amount
}
3.2 Storage 相關 Opcode 的 Gas 計算
讓我展示 SSTORE 的 Gas 邏輯,這是最複雜的 Opcode 之一:
// go-ethereum/core/vm/gas_table.go 的 SSTORE 邏輯
/*
* SSTORE Gas 成本的複雜邏輯
*
* 三種情況:
* 1. 從零值(0)寫入到非零值:20000 Gas(最貴)
* 2. 從非零值改為另一個非零值:5000 Gas
* 3. 從非零值改為零值:0 Gas + 退還(最多 15000 Gas)
*/
func GasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
// 獲取當前儲存值
current := evm.StateDB.GetState(contract.Address(), stack.peek())
// 獲取原始值(交易開始前的值)
original := evm.StateDB.GetCommittedState(contract.Address(), stack.peek())
// 獲取新值
new := stack.peek().Uint64() // 實際代碼中是另一個 stack.peek()
var gas uint64
// 核心邏輯
if current == 0 && new != 0 {
// 情況 1:新分配
gas = 20000
} else if current != 0 && new == 0 {
// 情況 2:刪除(改為零值)
// 實際上觸發退款
gas = 0
// 退款在 caller 層面處理
} else {
// 情況 3:修改現有值
gas = 5000
}
// EIP-2200:淨 Gas 測量
// 如果 original == current(新值等於舊值),只收 200 Gas
if original == current && current != 0 {
gas = 200
}
return gas, nil
}
# Python 等效邏輯
def calculate_sstore_gas(original_value, current_value, new_value):
"""
SSTORE Gas 計算的 Python 等效邏輯
參數:
- original_value: 交易開始前的值(用於 EIP-2200 優化)
- current_value: 當前區塊狀態中的值
- new_value: 要寫入的新值
"""
gas = 0
# 情況 1:新分配(從零到非零)
if current_value == 0 and new_value != 0:
gas = 20000
# 情況 2:刪除(從非零到零)—— 觸發退款
elif current_value != 0 and new_value == 0:
gas = 0 # 退款在後面處理
# 情況 3:修改現有值
else:
gas = 5000
# EIP-2200:淨 Gas 測量優化
# 如果 original == current == 新值,只收 200 Gas
# 這是針對「selfdestruct」後重設同一槽的情況優化
if original_value == current_value and current_value != 0:
gas = 200
# 計算退款
refund = 0
if current_value != 0 and new_value == 0:
# 刪除操作有退款,上限為消耗的 1/5
refund = min(15000, gas)
return gas, refund
# 測試案例
test_cases = [
# (original, current, new, expected_gas)
(0, 0, 0, 0), # 空操作
(0, 0, 123, 20000), # 新分配
(0, 123, 0, 0), # 刪除
(123, 123, 456, 5000), # 修改
(123, 123, 123, 200), # EIP-2200 優化:無變更
(0, 123, 123, 5000), # 重設
]
print("SSTORE Gas 測試案例")
print("=" * 60)
for original, current, new, expected in test_cases:
gas, refund = calculate_sstore_gas(original, current, new)
status = "✓" if gas == expected else "✗"
print(f"{status} ({original:>3}, {current:>3}, {new:>3}) → Gas: {gas:>6}, Refund: {refund:>5}")
3.3 Call 系列的 Gas 計算
CALL、CALLCODE、DELEGATECALL、STATICCALL 的 Gas 計算也很複雜:
// go-ethereum/core/vm/gas_table.go 的 Call Gas 邏輯
func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
// 基礎 Gas
gas := 100
// 如果要轉帳 ETH,需要額外檢查
// 注意:EIP-150 以後不再有額外 Gas 要求
// 返回需要的 Gas(包括轉帳金額對應的 Gas)
// 這裡的計算是近似值
callValue := stack.peek()
// 子調用的 Gas 由 caller 決定
// 但 EVM 有一個「最小保證」:child gas >= gas / 64
requiredGas := gas
return requiredGas, nil
}
func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
// CALLCODE 和 CALL 幾乎相同
// 唯一的區別是 msg.sender 的設置
return gasCall(evm, contract, stack, mem, memorySize)
}
func gasDelegateCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
// DELEGATECALL 比 CALL 便宜 100 Gas
// 因為不需要處理 ETH 轉帳
return 100, nil
}
func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
// STATICCALL 和 DELEGATECALL 類似
// 只是標誌不同
return 100, nil
}
3.4 記憶體擴展的二次方成本模型
記憶體成本是 EVM 設計中最優雅的數學部分之一:
// go-ethereum/core/vm/gas_table.go 的記憶體成本
/*
* 記憶體成本模型:
*
* 記憶體按「詞」(Word,32 位元組)計算
* 記憶體成本是二次函數,反映了記憶體擴展的邊際成本遞增
*
* G_memory = 3 * words + words^2 / 512
*
* 這個公式的意義:
* - 線性部分:基礎存取成本
* - 二次部分:記憶體擴展的邊際成本
* - 除以 512:規模因子
*/
const (
MemoryGas uint64 = 3 // 每詞的基礎 Gas
QuadCoefficient uint64 = 512 // 二次項的除數
ModExpQuadCoeff uint64 = 100 // MODEXP 的特殊係數
)
func GasMemory(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
// 計算所需的詞數
words := toWord(memorySize)
// 記憶體 Gas 計算
// G_mem = 3 * words + words^2 / 512
square := words * words
square.Div(square, big.NewInt(QuadCoefficient))
linear := new(big.Int).Mul(big.NewInt(MemoryGas), new(big.Int).SetUint64(words))
total := new(big.Int).Add(linear, square)
return total.Uint64(), nil
}
# Python 等效:記憶體 Gas 計算
def calculate_memory_gas(new_memory_size_bytes: int) -> dict:
"""
EVM 記憶體 Gas 計算
公式:G_memory = 3 * words + words^2 / 512
這個二次函數的意義:
- 記憶體越大,擴展成本越高
- 鼓勵合約重複使用已有記憶體,而不是每次分配新區域
"""
# 轉換為詞(向上取整)
words = (new_memory_size_bytes + 31) // 32
# 線性部分:每詞 3 Gas
linear_gas = 3 * words
# 二次部分:words^2 / 512
quadratic_gas = (words * words) // 512
# 總 Gas
total_gas = linear_gas + quadratic_gas
# 計算邊際成本(增加一個詞的成本)
old_words = words - 1 if words > 0 else 0
old_quadratic = (old_words * old_words) // 512
marginal_cost = 3 + (words * 2 - 1) // 512 # 新增一個詞的邊際成本
return {
"memory_bytes": new_memory_size_bytes,
"words": words,
"linear_gas": linear_gas,
"quadratic_gas": quadratic_gas,
"total_gas": total_gas,
"marginal_cost_next_word": marginal_cost
}
# 測試不同記憶體大小的成本
print("記憶體 Gas 成本 vs 記憶體大小")
print("=" * 80)
print(f"{'大小(位元組)':<15} {'詞數':<8} {'線性 Gas':<12} {'二次 Gas':<12} {'總 Gas':<12}")
print("-" * 80)
test_sizes = [0, 32, 64, 256, 1024, 4096, 32768, 131072]
for size in test_sizes:
result = calculate_memory_gas(size)
print(f"{result['memory_bytes']:<15} {result['words']:<8} {result['linear_gas']:<12} {result['quadratic_gas']:<12} {result['total_gas']:<12}")
這個記憶體模型的優雅之處在於:它是少數真正用數學反映資源稀缺性的設計。記憶體越大,擴展它的邊際成本越高——這與現實中的 RAM 成本曲線是一致的。
四、實際的 Gas 估算案例
4.1 標準 ERC-20 Transfer 的 Gas 分解
讓我們實際計算一筆普通 ERC-20 Transfer 的 Gas 消耗:
# ERC-20 Transfer 的 Gas 分解
class ERC20TransferGasAnalysis:
"""
一筆 ERC-20 transfer 交易觸發的 Opcode 序列和 Gas 消耗
"""
def __init__(self):
# Opcode Gas 消耗表
self.opcode_gas = {
# 基本棧操作
"PUSH1": 3,
"PUSH2": 3,
"PUSH3": 3,
"POP": 2,
"DUP1": 3,
"DUP2": 3,
# 存儲操作
"SLOAD": 800, # EIP-1884 後
"SSTORE": 5000, # 修改現有值
# 調用操作
"CALL": 100,
"STATICCALL": 100,
# 其他
"MSTORE": 3,
"MLOAD": 3,
"CALLDATALOAD": 3,
"CALLDATASIZE": 2,
"CALLVALUE": 2,
"CALLER": 2,
"ADDRESS": 2,
"EQ": 3,
"LT": 3,
"GT": 3,
"AND": 3,
"OR": 3,
"ISZERO": 3,
"ADD": 3,
"SUB": 3,
"MUL": 5,
"DIV": 5,
"MOD": 5,
"EXP": 10, # 基礎 + 指數相關
"RETURN": 0,
"REVERT": 0,
"STOP": 0,
}
def analyze_transfer(self) -> dict:
"""
ERC-20 transfer 的典型 Gas 消耗分解
假設:從 A 轉帳 100 Token 到 B
"""
# 操作序列和估算 Gas
operations = [
("CALLER", 2, "獲取調用者地址"),
("PUSH1 0x00", 3, "準備零值"),
("CALLDATALOAD", 3, "讀取 function selector"),
("PUSH4 0xa9059cbb", 3, "transfer function selector"),
("EQ", 3, "校驗函數簽名"),
("PUSH1 0x18", 3, "跳轉目標"),
("JUMPI", 8, "跳轉到函數本體"),
# ... 更多操作省略
# 讀取餘額
("PUSH2 [to]", 3, "目標地址"),
("PUSH2 [from]", 3, "源地址"),
("BALANCE", 100, "讀取源地址餘額"),
# 校驗餘額
("CALLVALUE", 2, "獲取轉帳金額"),
("GT", 3, "校驗餘額 >= 金額"),
("ISZERO", 3, "校驗通過"),
# 讀取現有餘額
("SLOAD", 800, "讀取 from 的代幣餘額"),
# 計算新餘額
("SUB", 3, "from - amount"),
("PUSH2 [slot]", 3, "準備存儲位置"),
("SSTORE (修改)", 5000, "寫入 from 新餘額"),
# to 的餘額
("SLOAD", 800, "讀取 to 的代幣餘額"),
("ADD", 3, "to + amount"),
("SSTORE (修改)", 5000, "寫入 to 新餘額"),
# Transfer 事件
("LOG2", 8 + 2*96, "發送 Transfer 事件"), # 依賴事件大小
# 返回
("PUSH1 0x01", 3, "返回成功"),
("RETURN", 0, "返回"),
]
# 計算總 Gas
total_gas = sum([op[1] for op in operations])
# 分解
breakdown = {
"棧操作": sum([op[1] for op in operations if op[0].startswith(("PUSH", "POP", "DUP", "SWAP"))]),
"存儲讀取 (SLOAD)": sum([op[1] for op in operations if op[0] == "SLOAD"]),
"存儲寫入 (SSTORE)": sum([op[1] for op in operations if op[0] == "SSTORE"]),
"校驗邏輯": sum([op[1] for op in operations if op[0] in ("EQ", "LT", "GT", "ISZERO", "AND", "OR")]),
"其他": total_gas - sum([op[1] for op in operations if op[0] in ("SLOAD", "SSTORE", "PUSH", "POP", "DUP", "SWAP", "EQ", "LT", "GT", "ISZERO", "AND", "OR")]),
}
return {
"operations": operations,
"total_gas": total_gas,
"breakdown": breakdown
}
def print_analysis(self):
analysis = self.analyze_transfer()
print("ERC-20 Transfer Gas 分解")
print("=" * 60)
print(f"\n總估算 Gas: {analysis['total_gas']:,}")
print(f"\n【Gas 消耗分解】")
for category, gas in analysis['breakdown'].items():
pct = gas / analysis['total_gas'] * 100
bar = "█" * int(pct / 2)
print(f" {category:<20} {gas:>6,} Gas ({pct:>5.1f}%) {bar}")
print(f"\n【關鍵觀察】")
print(f"1. SSTORE 佔了總 Gas 的 {analysis['breakdown']['存儲寫入 (SSTORE)']/analysis['total_gas']*100:.0f}%")
print(f"2. SLOAD 佔了 {analysis['breakdown']['存儲讀取 (SLOAD)']/analysis['total_gas']*100:.0f}%")
print(f"3. 這就是為什麼 ERC-20 許可(Approval)模式很貴的原因")
analysis = ERC20TransferGasAnalysis()
analysis.print_analysis()
4.2 Uniswap V2 Swap 的 Gas 成本模型
最後讓我們看一個更複雜的例子:Uniswap V2 的一筆 swap:
# Uniswap V2 Swap 的 Gas 成本分析
uniswap_swap_gas_breakdown = {
"交易調用": {
"CALLDATACOPY": 3,
"函數校驗": 20,
"sub_total": 23
},
"轉帳 Token(2 筆)": {
"SLOAD (from 餘額)": 2 * 800, # 讀取合約餘額
"SSTORE (扣除)": 1 * 5000, # 扣除代幣
"sub_total": 6600
},
"SWAP 操作": {
"MSTORE": 3,
"KECCAK256": 30 + 6 * 9, # 假設 256 位元組的 path
"sub_total": 84
},
"內部 Transfer": {
"SSTORE": 2 * 5000, # 更新兩個帳戶的餘額
"sub_total": 10000
},
"事件日誌": {
"LOG2 (Swap 事件)": 375 + 2 * 96,
"sub_total": 567
},
"合計基礎": 23 + 6600 + 84 + 10000 + 567,
"滑點保護鉗子": {
"SLOAD": 800,
"SSTORE": 5000,
"sub_total": 5800
}
}
# 總 Gas 估算
total_swap_gas = sum([
uniswap_swap_gas_breakdown["交易調用"]["sub_total"],
uniswap_swap_gas_breakdown["轉帳 Token(2 筆)"]["sub_total"],
uniswap_swap_gas_breakdown["SWAP 操作"]["sub_total"],
uniswap_swap_gas_breakdown["內部 Transfer"]["sub_total"],
uniswap_swap_gas_breakdown["事件日誌"]["sub_total"],
uniswap_swap_gas_breakdown["滑點保護鉗子"]["sub_total"],
])
print("Uniswap V2 Swap Gas 分解")
print("=" * 60)
for component, details in uniswap_swap_gas_breakdown.items():
if isinstance(details, dict) and "sub_total" in details:
print(f"\n【{component}】")
print(f" Gas: {details['sub_total']:,}")
pct = details['sub_total'] / total_swap_gas * 100
print(f" 佔比: {pct:.1f}%")
print(f"\n總估算 Gas: {total_swap_gas:,}")
print(f"實際典型 Gas 消耗: 110,000 - 150,000(包含失敗風險緩衝)")
五、未來的 Gas 模型演化方向
5.1 EIP-4844 Blob 交易的成本模型
2024 年的 Dencun 升級帶來了新的 Blob 交易類型,它的成本模型與普通 EVM 交易完全不同:
# EIP-4844 Blob 成本模型
class EIP4844BlobCostModel:
"""
Blob 交易的費用模型
核心思想:Blob 數據不保存在 EVM 狀態中
所以它的成本模型也不同於普通交易
"""
def __init__(self):
# Blob 參數
self.field_elements_per_blob = 4096
self.max_blobs_per_block = 6
self.target_blobs_per_block = 3
# Blob 費用參數
self.blob_gas_per_field_element = 2**17 # 131072
self.blob_gas_price_update_fraction = 0.0001 # 每區塊更新 0.01%
# 目標用量(以太坊黃皮書定義)
self.target_blob_gas = self.target_blobs_per_block * self.field_elements_per_blob * self.blob_gas_per_field_element
def calculate_blob_gas(self, blob_count: int) -> dict:
"""
計算 Blob 交易的 Gas
Blob Gas = field_elements × blob_gas_per_field_element × blob_count
實際上:
- 每個 Blob 有 4096 個 field elements
- 每個 field element = 2^17 Gas
- 所以每個 Blob = 4096 × 131072 = 536,870,912 Gas
"""
blob_gas_consumed = self.field_elements_per_blob * self.blob_gas_per_field_element * blob_count
return {
"blob_count": blob_count,
"field_elements": blob_count * self.field_elements_per_blob,
"blob_gas_per_blob": self.field_elements_per_blob * self.blob_gas_per_field_element,
"total_blob_gas": blob_gas_consumed,
"effective_gas_per_byte": blob_gas_consumed / (blob_count * 128 * 1024), # 每個 Blob 約 128KB
}
def calculate_fee(self, blob_count: int, base_fee_per_gas: int) -> dict:
"""
計算 Blob 交易費用
總費用 = Blob Gas × Blob Gas Price
Blob Gas Price 的調整與 EIP-1559 類似
"""
blob_data = self.calculate_blob_gas(blob_count)
# Blob Gas Price 動態調整
# 公式:blob_gas_price = base_fee * (1 + adjustment_factor)
# 簡化計算
blob_gas_price = base_fee_per_gas * 1000 # 假設比例
total_blob_fee = blob_data['total_blob_gas'] * blob_gas_price / 1e18 # 轉換為 ETH
return {
**blob_data,
"blob_gas_price_wei": blob_gas_price,
"total_blob_fee_eth": total_blob_fee,
"vs_calldata_fee": f"約 {blob_data['total_blob_gas'] / 16:,} Gas" # calldata 每位元組 16 Gas
}
blob_cost = EIP4844BlobCostModel()
print("EIP-4844 Blob 成本分析")
print("=" * 60)
for blob_count in [1, 3, 6]:
result = blob_cost.calculate_blob_gas(blob_count)
print(f"\n【{blob_count} 個 Blob】")
print(f" Field Elements: {result['field_elements']:,}")
print(f" 總 Blob Gas: {result['total_blob_gas']:,}")
print(f" 相當於 calldata Gas: ~{result['total_blob_gas']/16/1024:.0f} KB")
這個模型的關鍵創新是什麼?Blob 數據只保留約 18 天,之後被刪除。這意味著 Layer 2 可以用極低的成本發布交易數據到以太坊主網,而不需要支付昂貴的 EVM 執行費用。
結語:理解 Gas 模型的三個層次
寫到這裡,我想總結一下理解 Gas 模型的三個層次:
第一層:記憶表格。知道 ADD = 3 Gas、MUL = 5 Gas、SSTORE = 5000 Gas。這是大多數人停滯的地方。
第二層:理解原理。知道為什麼這些數字是這樣設定的——計算複雜度、記憶體邊際成本、狀態增長的代價。這讓你能預測 Gas 消耗的變化趨勢。
第三層:源碼實現。能讀懂 go-ethereum 的 Gas 計算邏輯,理解 Gas 之間的交互(如 SSTORE 退款機制、記憶體 Gas 的二次方模型)。這讓你能發現 Gas 估算工具的 bug,或者設計更節 Gas 的合約。
這篇文章希望能幫你從第一層跨到第二層。至於第三層——那是需要自己讀原始碼、做大量實驗的事情。
參考資料
| 資源 | URL | 用途 |
|---|---|---|
| go-ethereum Gas 實現 | github.com/ethereum/go-ethereum/core/vm/gas_table.go | Opcode Gas 計算源碼 |
| 以太坊黃皮書 | ethereum.github.io/yellowpaper/paper.pdf | 形式化 Gas 定義 |
| EVM Opcode 參考 | www.evm.codes | Opcode 速查表 |
| gaslimit.xyz | gaslimit.xyz | 實時 Gas 估算 |
標籤:#EVM #Gas #Opcode #Solidity #原始碼 #數學推導 #go-ethereum #以太坊
難度:advanced
撰寫日期:2026-03-27
免責聲明:本文僅供教育目的。Gas 模型可能因網路升級而變化,實際部署前請以最新官方文檔為準。
相關文章
- EVM Opcode 執行成本與 Gas 消耗深度技術分析:以太坊黃皮書規範引用與實際執行案例 — 本文深入分析以太坊虛擬機器(EVM)各類 Opcode 的 Gas 消耗模型,基於以太坊黃皮書的正式規範,提供每個操作碼的數學計算公式、複雜度分析以及實際執行成本案例。研究涵蓋從最基礎的棧操作到複雜的密碼學計算,幫助開發者建立精確的 Gas 估算能力。
- Solidity 位元運算優化完整指南:Gas 節省與智能合約效能極致優化 — 本指南從 EVM 機器碼層級出發,系統性地分析各類位元運算 opcode 的 Gas 消耗模型,提供可直接應用於生產環境的優化策略與程式碼範例。涵蓋定點數學與定標因子運算、位元遮罩與旗標操作、雜湊與簽章驗證優化、壓縮資料結構與位元封裝等進階主題。
- 以太坊 EVM Opcodes 完整參考手冊:Gas 消耗數學推導與實戰最佳化指南 — 本文深入剖析 EVM 完整 opcode 指令集,從基礎的算術運算到複雜的存儲操作,提供完整的 Gas 消耗數學推導與實戰最佳化指南。涵蓋記憶體成本二次函數推導、SSTORE 狀態機制、CALL 系列的 cold/warm access 定價模型、日誌操作的 Gas 計算、以及 EOF 時代新 opcode 的完整解析。提供大量 Solidity 和 Assembly 程式碼範例,幫助開發者編寫更省 Gas 的智能合約。
- 以太坊虛擬機(EVM)完整技術指南:從執行模型到狀態管理的系統性解析 — 本文提供 EVM 的系統性完整解析,涵蓋執行模型、指令集架構、記憶體管理、狀態儲存機制、Gas 計算模型,以及 2025-2026 年的最新升級動態。深入分析 EVM 的確定性執行原則、執行上下文結構、交易執行生命週期,並探討 EOF 和 Verkle Tree 等未來演進方向。
- 以太坊 EVM 執行模型深度技術分析:從位元組碼到共識層的完整解析 — 本文從底層架構視角深入剖析 EVM 的執行模型,涵蓋 opcode 指令集深度分析、記憶體隔離模型、Gas 消耗機制、呼叫框架、Casper FFG 數學推導、以及 EVM 版本演進與未來發展。我們提供完整的技術細節、位元組碼範例、效能瓶頸定量評估,幫助智慧合約開發者與區塊鏈研究者建立對 EVM 的系統性理解。
延伸閱讀與來源
- Ethereum.org Developers 官方開發者入口與技術文件
- EIPs 以太坊改進提案完整列表
- Solidity 文檔 智慧合約程式語言官方規格
- EVM 代碼庫 EVM 實作的核心參考
- Alethio EVM 分析 EVM 行為的正規驗證
這篇文章對您有幫助嗎?
請告訴我們如何改進:
評論
發表評論
注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。
目前尚無評論,成為第一個發表評論的人吧!