以太坊黃皮書完整深度解析系列(二):Gas計算公式推導、EVM執行模型與智能合約部署

本文是黃皮書深度解析系列的第二篇,聚焦於 Gas 計算公式的完整推導、EVM 執行模型的形式化描述、以及智能合約部署的狀態轉換過程。我們涵蓋操作碼 Gas 成本的完整推導(棧操作、記憶體操作、存儲操作、調用操作)、EVM 機器狀態的形式化定義、日誌與事件的 Gas 計算、區塊後處理函數、以及 EIP-1559 費用機制的黃皮書更新。這是智慧合約開發者、協議設計者和安全審計師必讀的參考資料。

以太坊黃皮書完整深度解析系列(二):Gas計算公式推導、EVM執行模型與智能合約部署

概述

本文是黃皮書深度解析系列的第二篇,聚焦於三個核心主題:Gas計算公式的完整推導、EVM執行模型的形式化描述、以及智能合約部署的狀態轉換過程。Gas是以太坊經濟模型的基石,理解其計算邏輯對於智慧合約開發者、協議設計者和安全審計師而言至關重要。我們將從黃皮書的數學定義出發,逐步推導出每個操作碼的Gas成本計算邏輯,並提供實務應用場景的說明。

第一部分:Gas機制的經濟學基礎

1.1 Gas作為資源定價機制

Gas是以太坊區塊鏈的「計算燃料」,用於衡量執行操作所需的計算資源和存儲資源。黃皮書將Gas定義為一種防止服務濫用的機制,其經濟學原理如下:

Gas定價機制原理:

    成本函數 C(o) = base_cost(o) + memory_cost(o) + storage_cost(o)
    
    其中:
    - base_cost: 操作的基礎計算成本
    - memory_cost: 記憶體使用成本
    - storage_cost: 持久化存儲成本
    
    區塊容量約束:
    Σ g_i ∀ i ∈ block ≤ BLOCK_GAS_LIMIT
    
    交易費用:
    fee = Gas_used × Gas_price

黃皮書定義了三種Gas成本計算模型:

  1. 靜態成本模型:操作成本固定,如STOP、RETURN、REVERT為0 Gas
  2. 線性成本模型:成本與輸入大小成正比,如SHA3、KECCAK256
  3. 階梯成本模型:成本隨使用量非線性增長,如記憶體擴展

1.2 區塊Gas限制的動態調整

黃皮書定義了區塊Gas限制的調整機制(此機制在The Merge後由質押者共識決定):

區塊Gas限制調整公式(PoW時代):

    BLOCK_GAS_LIMIT(n) = 
        BLOCK_GAS_LIMIT(n-1) × 
        (1 + δ × (parent_gas_used - parent_gas_limit) / parent_gas_limit)
    
    其中:
    - δ = 0.5 (最大調整係數)
    - 調整範圍:±0.5 × parent_gas_limit
    
    約束條件:
    12,000,000 ≤ BLOCK_GAS_LIMIT ≤ 30,000,000
    
    現代以太坊:
    - 目標值:15,000,000 Gas(基礎目標)
    - 最大值:30,000,000 Gas

第二部分:操作碼Gas成本完整推導

2.1 棧操作成本分析

棧操作是EVM中最基礎也最便宜的運算。黃皮書定義如下:

棧操作Gas成本(靜態):

    成本表:
    ┌──────────────────────────────────────────────────────────────┐
    │  操作碼        │  助記符      │  Gas成本  │  說明              │
    ├──────────────────────────────────────────────────────────────┤
    │  0x00         │  STOP        │     0     │  終止執行          │
    │  0x01         │  ADD         │     3     │  棧頂相加          │
    │  0x02         │  MUL         │     5     │  棧頂相乘          │
    │  0x03         │  SUB         │     3     │  棧頂相減          │
    │  0x04         │  DIV         │     5     │  整數除法          │
    │  0x06         │  MOD         │     5     │  取模運算          │
    │  0x08         │  ADDMOD      │     8     │  加後取模          │
    │  0x09         │  MULMOD      │     11    │  乘後取模          │
    │  0x0a         │  EXP         │     10+    │  指數運算(動態)    │
    └──────────────────────────────────────────────────────────────┘
    
    指數運算動態成本:
    G_exp = 10 + 50 × bytes(exponent)
    
    推導:
    - 基礎成本:10 Gas
    - 每個指數位元組額外成本:50 Gas
    - 原因:指數運算時間隨指數大小線性增長

2.2 記憶體操作成本推導

記憶體成本是EVM中最複雜的定價模型之一。黃皮書定義:

記憶體成本函數:

    記憶體成本公式:
    
    C_mem(words) = G_memory × words + floor(words² / 512)
    
    其中:
    - words = ceil(byte_size / 32)  (記憶體使用字數)
    - G_memory = 3 (每個記憶體字的線性成本)
    - 平方項:記憶體擴展的邊際成本遞增
    
    記憶體成本推導:
    
    1. 初始化新記憶體字
       C_init(word) = 3 × word + floor(word² / 512)
    
    2. 記憶體維持成本
       C_maintain = 0.032 × max_words  (每個操作執行後)
    
    3. 實際記憶體成本(淨成本)
       C_actual = C_mem(words_new) - C_mem(words_old)

記憶體成本階梯效應解析

記憶體成本階梯表:

    使用字數    線性成本    平方項        總成本    增量成本/字
    ─────────────────────────────────────────────────────────
    0           0           0           0          -
    1           3           0           3          3.000
    64          192         8           200        0.016
    1024        3072        2048        5120       0.004
    4096        12288      32768       45056      0.013
    16384       49152      524288      573440     0.036
    65536       196608     8388608     8585216    0.128
    ─────────────────────────────────────────────────────────

    觀察:每1024字的增量成本從~0.004增加到~0.128
    結論:記憶體使用量越大,邊際成本越高

2.3 存儲操作成本深度分析

存儲(Storage)是EVM中最昂貴的記憶體區域。黃皮書的定義經歷了多次調整(EIP-1283、EIP-2200等):

存儲操作Gas成本(最新版):

    存儲讀取 (SLOAD):
    ┌──────────────────────────────────────────────────────────────┐
    │  狀態              │  冷存儲成本  │  熱存儲成本              │
    ├──────────────────────────────────────────────────────────────┤
    │  首次讀取(冷)      │  2100 Gas   │  -                       │
    │  重複讀取(熱)      │  100 Gas    │  100 Gas                 │
    └──────────────────────────────────────────────────────────────┘
    
    存儲寫入 (SSTORE):
    ┌──────────────────────────────────────────────────────────────┐
    │  操作類型                        │  Gas成本                   │
    ├──────────────────────────────────────────────────────────────┤
    │  從零值寫入非零值 (新槽)        │  20000 Gas                │
    │  從非零值改為非零值 (修改)       │  5000 Gas                 │
    │  從非零值改為零值 (刪除)         │  2900 Gas (含退還)        │
    │  將零值寫為零值 (無操作)         │  0 Gas                    │
    └──────────────────────────────────────────────────────────────┘

存儲Gas成本推導

存儲Gas成本計算邏輯:

    G_sstore(current_value, new_value, original_value) =
        if original_value == new_value:
            return 0  # 無變更
        
        if current_value == new_value:
            # 值未變(可能被重置)
            if original_value == 0:
                return 20000  # 恢復到原狀態(零值)
            else:
                return 2900  # 恢復到原狀態(非零值,觸發退還)
        
        if current_value == 0:
            # 從零值開始
            if new_value != 0:
                return 20000  # 新增非零值
            else:
                return 0  # 仍然是零
        
        if new_value == 0:
            # 刪除操作
            if original_value != 0:
                return 2900  # 觸發退還(延遲到交易結束)
            else:
                return 0
        
        # 一般修改
        return 5000

2.4 調用操作成本分析

跨合約調用(Call)是EVM中最昂貴的操作之一:

調用操作Gas成本:

    CALL, DELEGATECALL, CALLCODE, STATICCALL:
    
    G_call = G_call_new + G_call_code + [G_call_value × is_value_transfer]
    
    其中:
    G_call_new = 2600 (冷存儲訪問)
    G_call_code = 2600 (合約代碼執行)
    G_call_value = 9000 (ETH轉帳,2022年EIP-1884後)
    
    EIP-2929 後的熱存儲優化:
    ┌──────────────────────────────────────────────────────────────┐
    │  目標地址狀態        │  基礎成本增加                        │
    ├──────────────────────────────────────────────────────────────┤
    │  冷存儲(首次訪問)   │  +2600 Gas                          │
    │  熱存儲(重複訪問)   │  +100 Gas                           │
    └──────────────────────────────────────────────────────────────┘

調用Gas傳遞模型

調用Gas傳遞公式:

    可用Gas = min(gas_provided, gas - gas / 64)  (63/64規則)
    
    其中:
    - gas_provided: 調用者指定的Gas量
    - gas: 當前剩餘Gas
    - 63/64: 防止Gas量過小導致執行失敗
    
    嵌套調用層次損耗:
    depth 1: 可用 ≈ 63/64 × gas
    depth 2: 可用 ≈ (63/64)² × gas
    depth N: 可用 ≈ (63/64)^N × gas
    
    限制:當可用Gas < 100時,強制停止嵌套調用

2.5 合約創建與銷毀成本

CREATE / CREATE2 操作:

    G_create = 32000 Gas
    G_create2 = 32000 Gas + G_txdata × bytecode_length
    
    CREATE2 額外成本說明:
    - CREATE2 需要計算鹽值哈希
    - 代碼部署成本與代碼長度成正比
    
    SELFDESTRUCT 操作:

    G_selfdestruct = 0 Gas (執行成本)
    
    註意:SELFDESTRUCT 免費,但:
    1. 仍消耗調用的基礎Gas (如 CALL 的 2600 Gas)
    2. 觸發存儲退還機制
    3. EIP-4758/6206 將限制其使用場景

第三部分:EVM執行模型的完整形式化

3.1 機器狀態的形式化定義

黃皮書將EVM定義為一個七元組的狀態機:

EVM機器狀態:

    μ = (g, pc, m, i, s, o, gas_refund)
    
    其中:
    - g ∈ ℕ₂₅₆       : 可用Gas
    - pc ∈ ℕ₂₅₆      : 程序計數器 (Program Counter)
    - m ∈ B^n         : 記憶體內容 (Memory)
    - i ∈ ℕ₂₅₆       : 記憶體使用的高水位字數
    - s ∈ (ℕ₂₅₆)^1024: 棧 (Stack),最多1024個元素
    - o ∈ B^*         : 返回資料 (Output)
    - gas_refund ∈ ℕ₂₅₆: 累積退還Gas

3.2 指令執行週期的數學描述

EVM執行週期:

    (g', pc', m', i', s', o', a', μ'_refund) = 
        EXECUTE(μ, σ, B, I, o, refund)
    
    其中:
    - σ: 世界狀態
    - B: 區塊信息
    - I: 執行環境信息
    - o: 當前操作碼
    - refund: 退還Gas
    
    執行步驟:
    
    Step 1: 取指 (Fetch)
    ----------
    i = m[pc]
    
    Step 2: 解碼 (Decode)
    ----------
    if i ∈ [PUSH1, PUSH32]:
        (operands, pc') = decode_push(i, m, pc)
    else:
        (operands, pc') = decode_standard(i, m, pc)
    
    Step 3: 執行 (Execute)
    ----------
    (g', m', s', o', refund') = APPLY(op, g, m, s, refund, I)
    
    Step 4: 異常檢測 (Halt Conditions)
    ----------
    if OUT_OF_GAS(g', op_cost):
        return REVERT
    if STACK_OVERFLOW(len(s'), 1024):
        return REVERT
    if INVALID_OPCODE(i):
        return REVERT

3.3 操作碼分類與語義

黃色皮書將操作碼分為以下類別:

操作碼分類表(完整):

┌────────────────────────────────────────────────────────────────────┐
│  類別          │  操作碼示例                        │  數量      │
├────────────────────────────────────────────────────────────────────┤
│  停止與返回     │  STOP, RETURN, REVERT, RETURNDATASIZE │  4        │
│  算術運算       │  ADD, MUL, SUB, DIV, MOD, EXP     │  12       │
│  比較與邏輯    │  LT, GT, EQ, AND, OR, XOR          │  10       │
│  密碼學        │  SHA3, KECCAK256                    │  2        │
│  環境信息       │  CALLER, CALLVALUE, GAS, TIMESTAMP  │  10       │
│  區塊信息       │  BLOCKHASH, COINBASE, DIFFICULTY  │  6        │
│  棧操作        │  PUSH*, DUP*, SWAP*, POP           │  24       │
│  記憶體操作     │  MLOAD, MSTORE, MSIZE, MCOPY       │  4        │
│  存儲操作       │  SLOAD, SSTORE                     │  2        │
│  控制流        │  JUMP, JUMPI, JUMPDEST, RJUMP      │  8        │
│  調用操作       │  CALL, DELEGATECALL, CREATE,       │  10       │
│                 │  CALLCODE, STATICCALL, CREATE2      │           │
│  系統操作       │  LOG0-4, CREATE, SELFDESTRUCT     │  7        │
│  日誌操作       │  LOG0, LOG1, LOG2, LOG3, LOG4     │  5        │
└────────────────────────────────────────────────────────────────────┘

3.4 JUMPDEST分析的必要性

JUMPDEST 有效性檢查:

    valid_jump(pc) ⟺
        1. m[pc] == JUMPDEST (0x5b)
        2. pc < len(code)
        3. code[0:pc] 中不存在 PUSH* 指令
    
    JUMPDEST掃描算法:

    def scan_jumpdests(code):
        jumpdests = []
        i = 0
        while i < len(code):
            if code[i] in [PUSH1..PUSH32]:
                i += code[i] - PUSH1 + 2  # 跳過操作數
            elif code[i] == JUMPDEST:
                jumpdests.append(i)
                i += 1
            else:
                i += 1
        return jumpdests
    
    為什麼需要JUMPDEST?
    - 防止執行跳轉到代碼中間
    - 阻斷JUMPDEST攻擊(動態跳轉)
    - 為JIT編譯器提供有效目標列表

第四部分:智能合約部署的狀態轉換

4.1 合約創建的完整流程

黃皮書定義的合約創建狀態轉換:

合約創建狀態轉換:

    Υ_CREATE(σ, sender, endowment, init_code, gas_limit) =
    
        # Step 1: 創建新地址
        address = keccak256(RLP(sender, nonce))[12:]
        
        # Step 2: 檢查地址未佔用
        assert σ[address] == ∅  OR σ[address].code == empty
        
        # Step 3: 扣減餘額
        σ' = σ with σ'[sender].balance -= endowment
        
        # Step 4: 創建帳戶
        σ'' = σ' with σ''[address] = (
            nonce = 0,
            balance = endowment,
            storage = empty_trie,
            code = empty
        )
        
        # Step 5: 執行初始化代碼
        μ = (gas=gas_limit, pc=0, memory=[], stack=[])
        (output, μ') = execute(init_code, μ, σ'')
        
        # Step 6: 計算部署代碼
        code = output
        assert len(code) <= 24576  # 最大合約大小限制
        
        # Step 7: 驗收代碼
        if execution_failed or code_deployment_out_of_gas:
            σ''' = σ with σ'''[sender].balance += endowment
            return (σ, 0, [])
        
        # Step 8: 部署代碼
        σ''' = σ'' with σ'''[address].code = code
        
        # Step 9: 計算新nonce
        σ'''' = σ''' with σ''''[sender].nonce += 1
        
        return (σ'''', gas_remaining, [address])

4.2 合約地址的確定性

黃皮書定義了兩種合約地址計算方式:

合約地址計算公式:

    # 方式一:使用 sender 和 nonce
    address_1 = last_20_bytes(keccak256(RLP(sender, nonce)))
    
    RLP編碼:
    RLP(sender, nonce) = 
        if sender < 128 and nonce < 128:
            [0x80 + len(sender)] || sender || [nonce]
        else:
            0xf7 || len(sender) || sender || len(nonce) || nonce
    
    # 方式二:使用 CREATE2 (EIP-1014)
    address_2 = last_20_bytes(
        keccak256(0xff || sender || salt || keccak256(init_code))
    )
    
    其中:
    - 0xff: 固定字節(前綴,避免衝突)
    - salt: 32字節鹽值
    - keccak256(init_code): 初始化代碼的哈希

4.3 初始化代碼與構造函數

初始化代碼執行模型:

    初始化代碼的執行過程:
    
    ┌─────────────────────────────────────────────────────────────┐
    │  初始化代碼執行                                              │
    │                                                              │
    │  ┌─────────────────────────────────────────────────────┐    │
    │  │  構造函數 (Constructor)                              │    │
    │  │  - 初始化合約狀態                                    │    │
    │  │  - 設置管理員地址                                    │    │
    │  │  - 配置初始參數                                      │    │
    │  │  - RETURN 部署代碼 + 構造函數參數                      │    │
    │  └─────────────────────────────────────────────────────┘    │
    │                          │                                  │
    │                          │ deploy()                        │
    │                          ▼                                  │
    │  ┌─────────────────────────────────────────────────────┐    │
    │  │  Runtime 代碼                                        │    │
    │  │  - 用戶交互時執行的代碼                              │    │
    │  │  - 存儲在合約的 code 欄位                            │    │
    │  └─────────────────────────────────────────────────────┘    │
    │                                                              │
    └─────────────────────────────────────────────────────────────┘
    
    Solidity 編譯流程:
    
    1. 構造函數源代碼
    2. 構造函數 + 部署代碼 (Runtime Bytecode)
    3. 部署代碼 = CODECOPY(runtime) + RETURN(len(runtime))

第五部分:日誌與事件的形式化

5.1 日誌操作的結構

黃皮書將日誌定義為:

日誌條目 (Log Entry):

    L = (address, topics[], data)
    
    其中:
    - address: 發出日誌的合約地址
    - topics: 最多4個32字節的「主題」
    - data: 可變長度的任意數據
    
    結構示意圖:
    
    ┌─────────────────────────────────────────────────────────────┐
    │  Log Entry                                                   │
    │  ┌─────────────────────────────────────────────────────────┐│
    │  │  address: 0x1234... (20 bytes)                        ││
    │  ├─────────────────────────────────────────────────────────┤│
    │  │  topics[0]: keccak256("Transfer(address,address,uint)") ││
    │  │  topics[1]: from_address (32 bytes)                    ││
    │  │  topics[2]: to_address (32 bytes)                     ││
    │  │  topics[3]: amount (32 bytes)                          ││
    │  ├─────────────────────────────────────────────────────────┤│
    │  │  data: 額外的非索引數據 (可選)                          ││
    │  └─────────────────────────────────────────────────────────┘│
    └─────────────────────────────────────────────────────────────┘

5.2 日誌的Gas成本計算

日誌Gas成本公式:

    G_log = G_base + G_log_topic × num_topics + G_log_data × data_size
    
    其中:
    - G_base = 375 Gas (LOG0基礎成本)
    - G_log_topic = 375 Gas (每個topic)
    - G_log_data = 8 Gas (每個數據字節)
    
    各類型日誌成本:
    
    LOG0: 375 + 0 + G_log_data × len(data)
    LOG1: 375 + 375 + G_log_data × len(data) = 750 + ...
    LOG2: 375 + 750 + G_log_data × len(data) = 1125 + ...
    LOG3: 375 + 1125 + G_log_data × len(data) = 1500 + ...
    LOG4: 375 + 1500 + G_log_data × len(data) = 1875 + ...
    
    Solidity event 轉換:
    event Transfer(address indexed from, address indexed to, uint256 value)
    
    編譯為:LOG3(address, [topic0, from, to], [value])
    Gas = 1125 + 8 × len(value)

第六部分:區塊後處理函數

6.1 區塊獎勵計算

區塊獎勵函數 (PoW時代):

    block_reward = R_block + Σ R_uncle × n_uncles
    
    其中:
    R_block = 2 × 10^18 Wei (基礎獎勵,後因EIP-3607調整)
    R_uncle = R_block × (N_R - N_U + 1) / 8 (Uncle獎勵)
    N_R = 主區塊編號
    N_U = Uncle區塊編號
    
    Uncle獎勵示例:
    主區塊高度 100 包含高度 95 的 Uncle:
    uncle_reward = 2 ETH × (100-95+1)/8 = 2 × 6/8 = 1.5 ETH
    
    主區塊的額外獎勵:
    nephew_reward = R_block / 32 = 2/32 = 0.0625 ETH

6.2 區塊後處理步驟

區塊後處理函數 Ω:

    Ω(σ, B) = σ''
    
    其中後處理步驟包括:
    
    1. Uncle獎勵
    for each uncle in B.ommers:
        σ'[uncle.coinbase] += uncle_reward(uncle, B)
        σ'[B.coinbase] += R_block / 32
    
    2. 區塊獎勵
    σ''[B.coinbase] += R_block
    
    3. 驗收叔塊列表
    verify_uncles(B)
    
    4. 更新狀態根
    assert B.stateRoot == state_root(σ'')

第七部分:EIP-1559費用機制的黃皮書更新

7.1 費用市場的形式化改革

EIP-1559 後的交易格式:

    t = (
        chain_id,              # 新增:網路標識符
        nonce,
        max_priority_fee,      # 原 gasPrice 改為優先費用
        max_fee,              # 新增:最大費用
        gas_limit,
        to,
        value,
        data,
        access_list,           # EIP-2930 新增
        y_parity,              # 原 v (0/1 替代 27/28)
        r, s
    )

7.2 費用計算的新公式

費用計算(EIP-1559):

    # 優先費用
    priority_fee = min(max_priority_fee, max_fee - block.base_fee)
    
    # 礦工/驗證者收入
    miner_fee = priority_fee × gas_used
    
    # 燃燒金額
    burned = base_fee × gas_used
    
    # 用戶總費用
    total_cost = (base_fee + priority_fee) × gas_used

7.3 Base Fee 調整公式

Base Fee 動態調整公式:

    base_fee_new = base_fee_old × (1 + Δ)
    
    其中:
    Δ = (parent_gas_used - parent_gas_target) / parent_gas_target × 1/8
    
    約束條件:
    - base_fee ≥ 1 Wei (最小值)
    - |Δ| ≤ 1/8 (最大調整幅度)
    
    調整方向:
    parent_gas_used > parent_gas_target → base_fee 上升
    parent_gas_used < parent_gas_target → base_fee 下降
    
    數值示例:
    parent_gas_target = 15,000,000
    parent_gas_limit = 30,000,000
    
    情境1: parent_gas_used = 20,000,000 (充滿133%)
    Δ = (20M - 15M) / 15M × 0.125 = 0.0417
    base_fee_new = base_fee_old × 1.0417
    
    情境2: parent_gas_used = 10,000,000 (充滿67%)
    Δ = (10M - 15M) / 15M × 0.125 = -0.0417
    base_fee_new = base_fee_old × 0.9583

結論:Gas計算的實務應用

理解黃皮書中的Gas計算公式對以下場景至關重要:

智慧合約開發者

安全審計師

協議設計者

參考資源

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

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

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