以太坊 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.goOpcode Gas 計算源碼
以太坊黃皮書ethereum.github.io/yellowpaper/paper.pdf形式化 Gas 定義
EVM Opcode 參考www.evm.codesOpcode 速查表
gaslimit.xyzgaslimit.xyz實時 Gas 估算

標籤:#EVM #Gas #Opcode #Solidity #原始碼 #數學推導 #go-ethereum #以太坊

難度:advanced

撰寫日期:2026-03-27

免責聲明:本文僅供教育目的。Gas 模型可能因網路升級而變化,實際部署前請以最新官方文檔為準。

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。

目前尚無評論,成為第一個發表評論的人吧!