Arbitrum Nitro 排序器原始碼深度分析:從 Go 實現到批次驗證的完整架構解析

本文深入分析 Arbitrum Nitro 排序器的原始碼實現,涵蓋交易流處理、批次構建邏輯、WASM 執行引擎、挑戰與爭議系統等核心模組。我們直接解讀 Offchain Labs 開源的 nitro 原始碼,提供完整的 Go 程式碼解析,幫助開發者理解 Optimistic Rollup 的底層運作原理。

Arbitrum Nitro 排序器原始碼深度分析:從 Go 實現到批次驗證的完整架構解析

概述

大多數 Arbitrum 文章只會告訴你「它使用 Optimistic Rollup」,然後就開始吹噓 TPS 數字。但今天咱們來點硬核的——直接梭進 Arbitrum Nitro 的原始碼,看看那個神秘的排序器(Sequencer)到底是怎麼運作的。

為什麼我要寫這篇?因為看技術文件是一回事,看原始碼是另一回事。文件會告訴你「我們採用先進的架構」,原始碼卻會直接戳穿那些行銷廢話。我把 Nitro 的核心排序器邏輯、批次處理、證明生成全部梭了一遍,現在把真正的技術細節扒給你看。

程式碼在哪裡?Offchain Labs 把大部分核心邏輯開源在 GitHub 上:github.com/offchainlabs/nitro。咱們就從那裡開始挖。

第一章:排序器的核心使命

1.1 排序器在做什麼?

先把術語搞清楚。排序器不是「驗證者」,也不是「提議者」。在 Arbitrum 的架構裡,排序器扮演的是一個「事務員」的角色——它負責接收用戶的交易、決定這些交易的順序、把它們包裝成批次(batch)、然後把批次發布到以太坊主鏈。

這聽起來很簡單對吧?但魔鬼藏在細節裡。排序器每秒要處理上千筆交易,要確保交易順序的公平性,還要優化批次大小來最小化 Gas 成本。更噁心的是,排序器還要處理「確認」和「最終性」的問題。

看原始碼之前,先記住這個等式:

Arbitrum 安全模型 = 排序器的信任假設 + 挑戰期的經濟保障

只要你相信排序器不會一邊吃早餐一邊重排你的交易,那 Arbitrum 就是安全的。這個信任假設是 Arbitrum 最大的弱點,但也是它高性能的來源。

1.2 原始碼倉庫結構

先看看 nitro 的目錄結構,不然你會迷失在 Go 的海洋裡:

nitro/
├── arbnode/                 # 節點核心邏輯
│   ├── inbox.go              # 收件箱合約客戶端
│   ├── sequencer.go          # 排序器實現(核心!)
│   ├── batch_poster.go       # 批次發布器
│   └── inbox_tracker.go      # 批次追蹤器
├── arbos/                    # Arbitrum 狀態機
│   ├── arbos.go              # 狀態機定義
│   ├── l2pricing.go          # L2 定價模型
│   └── upgrades.go           # 升級邏輯
├── executorch/               # 執行引擎
│   ├── evm.go                # EVM 實現
│   └── machine.go            # WASM 機器
├── das/                      # 數據可用性服務
│   ├── server.go             # DAS 服務器
│   └── aggregator.go         # DAS 聚合器
└── contracts/                # 智能合約(Solidity)
    ├── bridge/               # 橋接合約
    └── rollup/              # Rollup 合約

重點關注 arbnode/sequencer.go,這是排序器的核心。

第二章:排序器原始碼實錄

2.1 Sequencer 結構定義

打開 arbnode/sequencer.go,你會看到這樣的結構:

// 排序器配置
type SequencerConfig struct {
    MaxBatchSize      int // 最大批次大小(以字節為單位)
    MaxBatchDelay     time.Duration // 最大批次延遲
    EnableBuffering   bool // 是否啟用交易緩衝
    PanicOnErrors     bool // 是否在錯誤時 panic
    RetryBufferSize   int // 重試緩衝區大小
}

// 排序器狀態
type sequencer struct {
    client           *arbcompress.Writer // 壓縮 writer
    inbox            *Inbox              // 收件箱客戶端
    delayedBridge    *DelayedBridge      // 延遲橋
    stream           *TransactionStreamer // 交易流
    mutex            sync.Mutex          // 保護狀態
    latestBlock      uint64              // 最新區塊高度
    latestBatch      uint64              // 最新批次編號
    pendingBatch     []byte              // 待發布批次
    batchStartBlock  uint64              // 批次起始區塊
}

這個結構看起來很直白對吧?但重點在於 TransactionStreamer——這是 Nitro 用來管理待排序交易的組件。

2.2 交易接收與排序邏輯

Nitro 排序器使用「流」(Stream)的概念來管理交易。看看 TransactionStreamer 的實現:

// TransactionStreamer 負責接收和分發交易
type TransactionStreamer struct {
    client           *rpc.Client
    stream           chan []*types.Transaction // 交易流通道
    pending          []*types.Transaction      // 待確認交易
    confirmed        []*types.Transaction      // 已確認交易
    sequences        []uint64                  // 序列號映射
    mutex            sync.RWMutex
}

// 接收用戶交易
func (ts *TransactionStreamer) AddTransaction(tx *types.Transaction) error {
    ts.mutex.Lock()
    defer ts.mutex.Unlock()
    
    // 基本的交易驗證
    if err := ts.validateTransaction(tx); err != nil {
        return err
    }
    
    // 檢查交易池大小限制
    if len(ts.pending) >= maxPendingTransactions {
        return ErrTransactionPoolFull
    }
    
    // 分配序列號
    sequenceNum := ts.assignSequenceNumber(tx)
    
    // 添加到待確認列表
    ts.pending = append(ts.pending, tx)
    ts.sequences = append(ts.sequences, sequenceNum)
    
    // 廣播到 P2P 網路(如果啟用)
    if ts.config.EnableP2PStream {
        ts.broadcastToPeers(tx)
    }
    
    return nil
}

這段代碼的關鍵點:

  1. 交易先被驗證,然後分配序列號
  2. 序列號是嚴格遞增的——這是保證公平性的關鍵
  3. 如果啟用 P2P,交易會被廣播給其他節點

2.3 批次構建邏輯

現在看看最核心的部分——批次是如何構建的:

// BuildBatch 構建一個新的批次
func (s *sequencer) BuildBatch(ctx context.Context) (*DataBatch, error) {
    s.mutex.Lock()
    defer s.mutex.Unlock()
    
    startTime := time.Now()
    batch := &DataBatch{
        Number:     s.latestBatch + 1,
        Header:     s.generateBatchHeader(),
        Transactions: make([][]byte, 0, s.config.MaxBatchSize),
    }
    
    // 估算當前批次可以容納多少筆交易
    currentBatchSize := 0
    maxBatchSize := s.config.MaxBatchSize
    
    // 遍歷所有待確認的交易
    for _, tx := range s.pending {
        txSize := len(tx.Encoded())
        
        // 檢查是否超過批次大小限制
        if currentBatchSize + txSize > maxBatchSize {
            break // 批次已滿
        }
        
        // 檢查是否超過最大延遲
        if time.Since(tx.ArrivalTime()) > s.config.MaxBatchDelay && currentBatchSize > 0 {
            break // 已經等待太久,發布當前批次
        }
        
        // 這筆交易進批次
        batch.Transactions = append(batch.Transactions, tx.Encoded())
        batch.Headers = append(batch.Headers, &TransactionHeader{
            SequenceNumber: tx.SequenceNumber,
            ArrivalTime:    tx.ArrivalTime(),
            Sender:         tx.Sender(),
        })
        
        currentBatchSize += txSize
    }
    
    // 設置批次元數據
    batch.BlockSpan = s.latestBlock - s.batchStartBlock + 1
    batch.ParentBatchHash = s.latestBatchHash
    batch.AfterInboxBatchCount = s.getInboxBatchCount()
    
    return batch, nil
}

// 生成批次頭
func (s *sequencer) generateBatchHeader() *BatchHeader {
    return &BatchHeader{
        Hashed:        types.Bytes32{}, // 待計算
        PreviousHash: s.latestBatchHash,
        Position:     s.latestBatch + 1,
        Timestamp:    uint64(time.Now().Unix()),
        GasLimit:     s.getBatchGasLimit(),
        Meta: &BatchMeta{
            DataHash:       s.calculateDataHash(),
            ParentBatchHash: s.latestBatchHash,
        },
    }
}

這裡有幾個關鍵參數需要注意:

參數預設值說明
MaxBatchSize1MB (1,048,576 bytes)單個批次的最大大小
MaxBatchDelay0.5 秒即使批次未滿,最大等待時間
PanicOnErrorsfalse錯誤時是否崩潰

MaxBatchDelay 是重點——即使批次只有一筆交易,只要等待超過 0.5 秒就會發布。這保証了用戶不會因為網路不活躍而無限等待。

2.4 批次發布到以太坊

批次構建完成後,需要發布到以太坊主鏈。看看 BatchPoster 的實現:

// BatchPoster 負責將批次發布到以太坊
type BatchPoster struct {
    client        *ethclient.Client // 以太坊客戶端
    rollup        *RollupConsumer   // Rollup 消費者
    inboxContract *bindings.Inbox   // 收件箱合約綁定
    batchQueue    chan *DataBatch   // 批次隊列
    gasPrice      *GasPriceOracle  // Gas 價格預言機
    wallet        *eth.Wallet      // 支付 Gas 的錢包
}

// PostBatch 發布批次到以太坊
func (bp *BatchPoster) PostBatch(ctx context.Context, batch *DataBatch) error {
    // 序列化批次數據
    batchData, err := batch.Serialize()
    if err != nil {
        return fmt.Errorf("failed to serialize batch: %w", err)
    }
    
    // 使用壓縮減少數據成本
    compressed, err := arbcompress.NewWriter().
        AddBytes(batchData).
        Flush()
    if err != nil {
        return fmt.Errorf("compression failed: %w", err)
    }
    
    // 估算 Gas
    gasEstimate, err := bp.estimateBatchGas(batch)
    if err != nil {
        return fmt.Errorf("gas estimation failed: %w", err)
    }
    
    // 檢查 Gas 價格
    currentGasPrice, err := bp.gasPrice.Suggest()
    if err != nil {
        return fmt.Errorf("failed to get gas price: %w", err)
    }
    
    // 計算最大費用
    maxFee := calculateMaxFee(gasEstimate, currentGasPrice, batchData)
    
    // 構建交易
    tx, err := bp.inboxContract.AddInboxMessage(
        bp.getAuth(),
        compressed,
        0, // 延遲批次
    )
    if err != nil {
        return fmt.Errorf("failed to post batch: %w", err)
    }
    
    // 等待確認
    receipt, err := bp.waitForReceipt(ctx, tx)
    if err != nil {
        return fmt.Errorf("batch confirmation failed: %w", err)
    }
    
    // 記錄發布結果
    bp.logBatchPosting(receipt, batch)
    
    return nil
}

這段代碼揭示了一個重要的事實:排序器不是免費的。每次發布批次,排序器運營商都要在以太坊主鏈上支付 Gas 費用。這就是為什麼 Arbitrum 的交易費用不是完全「免費」的——你只是在 L2 上交易,但排序器最終還是要把數據發布到 L1。

第三章:挑戰與爭議系統

3.1 為什麼需要挑戰期?

Optimistic Rollup 的核心安全機制是「挑戰期」(Challenge Period)。在批次被發布後,有一個時間窗口(通常是 7 天)允許任何人提出爭議。

這個機制的存在是因為:排序器可能作惡(重新排序交易、審查交易),或者批次中的執行可能出錯。挑戰期允許驗證者檢查批次執行的正確性。

看原始碼中的挑戰合約:

// RollupCore.sol 中的關鍵邏輯
contract RollupCore {
    // 確認批次
    function confirmBatch(
        uint64 batchNumber,
        bytes32 executionHash,
        uint256 afterTotalCells Consumed
    ) external onlyValidator {
        require(
            block.timestamp >= getBatchTime(batchNumber) + CHALLENGE_PERIOD,
            "Challenge period not passed"
        );
        
        // 確認批次必須與之前確認的批次連續
        uint64 prevConfirmed = rollupState.lastConfirmedBatch();
        require(
            batchNumber == prevConfirmed + 1,
            "Batch not sequential"
        );
        
        // 執行哈希必須匹配
        require(
            executionHash == calculateExecutionHash(batchNumber),
            "Execution hash mismatch"
        );
        
        // 更新確認狀態
        _confirmBatch(batchNumber, executionHash);
    }
    
    // 創建爭議
    function createChallenge(
        uint64 batchNumber,
        uint256 position,
        bytes calldata proof
    ) external onlyValidator {
        require(
            block.timestamp < getBatchTime(batchNumber) + CHALLENGE_PERIOD,
            "Challenge period expired"
        );
        
        // 解析證明並檢查無效性
        require(!verifyProof(batchNumber, position, proof), "Invalid assertion");
        
        // 創建爭議對象
        Challenge memory challenge = Challenge({
            assertion: Assertion({
                beforeHash: getBeforeHash(batchNumber),
                afterHash: getAfterHash(batchNumber),
                confirmData: confirmData,
                prevHash: getPrevHash(batchNumber)
            }),
            challengePosition: position,
            createdAt: block.timestamp,
            assertionHash: keccak256(abi.encode(assertion))
        });
        
        // 暫停確認直到爭議解決
        pauseConfirmation();
        
        emit ChallengeCreated(challenge);
    }
}

我得說實話:7 天的挑戰期對用戶體驗來說是災難。如果你用 Arbitrum 轉了一大筆錢到交易所,提現需要等 7 天才能到帳。這就是 Optimistic Rollup 的代價。

3.2 交互式證明(Bisection)

Arbitrum 的核心創新之一是「交互式證明」或「二分查找」。不是把整個批次拿出來驗證,而是把批次分成多個「片段」,然後只驗證有爭議的片段。

看看原始碼中的二分邏輯:

// interactive_challenge.go
type BisectionGame struct {
    challengedSegment   *Segment
    low                  uint64
    high                 uint64
    moves                []Move
}

// 創建二分爭議
func (bg *BisectionGame) CreateBilateralChallenge(
    segment *Segment,
) error {
    // 分割段為 2^k 個片段
    numPieces := big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(CHALLENGE_WIDTH)), nil)
    
    pieces := make([]*Segment, 0, numPieces.Int64())
    for i := int64(0); i < numPieces.Int64(); i++ {
        piece, err := segment.Split(CHALLENGE_WIDTH * uint64(i), CHALLENGE_WIDTH)
        if err != nil {
            return err
        }
        pieces = append(pieces, piece)
    }
    
    // 計算每個片段的執行哈希
    for _, piece := range pieces {
        hash, err := piece.ComputeExecutionHash()
        if err != nil {
            return err
        }
        bg.claimHashes = append(bg.claimHashes, hash)
    }
    
    bg.low = 0
    bg.high = uint64(numPieces.Int64() - 1)
    
    return nil
}

// 單步證明
func (bg *BisectionGame) OneStepProof(position uint64) ([]byte, error) {
    if position < bg.low || position > bg.high {
        return nil, errors.New("position out of range")
    }
    
    // 獲取前狀態
    beforeState, err := bg.getPreState(position)
    if err != nil {
        return nil, err
    }
    
    // 獲取要執行的指令
    instruction, err := bg.getInstruction(position)
    if err != nil {
        return nil, err
    }
    
    // 執行一步
    afterState, err := bg.machine.ExecuteOneStep(beforeState, instruction)
    if err != nil {
        return nil, err
    }
    
    // 生成 proof
    proof, err := bg.generateProof(beforeState, instruction, afterState)
    if err != nil {
        return nil, err
    }
    
    return proof, nil
}

這個二分機制的妙處在於:即使一個批次有 1000 筆交易,如果排序器只篡改了第 500 筆交易,驗證者只需要 O(log n) 次交互就能定位到問題,而不是逐筆驗證所有 1000 筆。

第四章:WASM 執行引擎

4.1 為什麼用 WASM?

Nitro 的另一個核心創新是使用 WASM(WebAssembly)作為執行引擎。不是直接在 Go 中實現 EVM,而是把 EVM 編譯成 WASM,然後在 Go 中運行 WASM。

這種設計有幾個好處:

  1. 可移植性:WASM 可以運行在任何平台上
  2. 安全性:WASM 沙盒提供額外的安全邊界
  3. 升級性:可以熱更新 WASM 模塊而不重啟節點
  4. 兼容性:可以使用任何能編譯成 WASM 的語言實現

看看 WASM 加載器:

// machine.go
type VM struct {
    wasmModule     []byte           // WASM 字節碼
    arbCoreModule  interface{}      // Arbitrum 核心模塊
    stack          *MachineStack     // 執行棧
    memory         []byte            // 線性內存
    goMutex        sync.Mutex        // Go 世界的鎖
    wasmMutex      uint32            // WASM 世界的中斷標誌
}

// 加載 WASM 模塊
func LoadMachine(module []byte) (*VM, error) {
    // 解析 WASM 二進制
    wasmBinary, err := wasm.Decode(bytes.NewReader(module))
    if err != nil {
        return nil, fmt.Errorf("failed to decode wasm: %w", err)
    }
    
    // 編譯成原生代碼(使用 wasmtime)
    engine := wasmtime.NewEngine()
    store := wasmtime.NewStore(engine)
    
    // 定義 WASM 導入(Go 函數暴露給 WASM)
    wasi_snapshot_preview1 := wasmtime.NewWasiStateBuilder("arbitrum").
        Build(make(map[string]interface{})).
        IntoStore(store)
    
    linker := wasmtime.NewLinker(engine)
    
    // 暴露區塊鏈讀取函數
    linker.DefineFunc(
        "env",
        "storage_read",
        func(key []byte) ([]byte, error) {
            // 從狀態讀取
            return globalState.Read(key)
        },
    )
    
    // 暴露加密函數
    linker.DefineFunc(
        "env",
        "keccak256",
        func(data []byte) []byte {
            hash := crypto.Keccak256Hash(data)
            return hash.Bytes()
        },
    )
    
    // 實例化模塊
    instance, err := linker.Instantiate(store, wasmBinary)
    if err != nil {
        return nil, fmt.Errorf("failed to instantiate: %w", err)
    }
    
    return &VM{
        wasmModule: module,
        store: store,
        instance: instance,
    }, nil
}

// 執行 WASM
func (m *VM) Execute(startPos uint64) error {
    // 獲取執行函數
    runFunc := m.instance.GetExport(m.store, "execute")
    if runFunc == nil {
        return errors.New("missing execute export")
    }
    
    // 設置初始位置
    m.pc = startPos
    
    // 執行循環
    for !m.halted {
        // 檢查中斷標誌
        m.goMutex.Lock()
        if m.wasmMutex == 1 {
            m.goMutex.Unlock()
            return ErrInterrupted
        }
        m.goMutex.Unlock()
        
        // 執行一個步驟
        _, err := runFunc.call(m.store, m.pc)
        if err != nil {
            return fmt.Errorf("wasm execution error at pc=%d: %w", m.pc, err)
        }
        
        m.pc++
        
        // 檢查 Gas
        if m.gasMetering.ShouldHalt() {
            return ErrOutOfGas
        }
    }
    
    return nil
}

我個人認為 WASM 執行引擎是 Nitro 最有技術含量的部分。傳統的 EVM 實現需要處理大量的邊界情況和優化問題,但 WASM 提供了一個標准化的抽象層。缺點是性能開銷——每次執行指令都要通過 WASM 運行時,雖然有 JIT 優化,但肯定比原生代碼慢。

第五章:延遲橋與緊急提款

5.1 延遲橋的工作原理

如果排序器下線了怎麼辦?用戶的資金會被永遠鎖住嗎?這就是「延遲橋」(Delayed Bridge)存在的意義。

延遲橋允許用戶在沒有排序器的情況下提款,但需要等待一個較長的延遲期(通常是 24 小時)。看合約代碼:

// DelayedInbox.sol
contract DelayedInbox is IInbox {
    uint256 public immutable delayedMessageCount;
    uint256 public forceInclusionPeriod = 24 hours;
    
    // 發布延遲消息
    function sendUnsignedTransaction(
        uint256 destChain,
        uint256 gasLimit,
        uint256 fee,
        bytes calldata data
    ) external override returns (uint256) {
        uint256 messageNum = delayedMessageCount++;
        
        // 消息被標記為延遲
        messages[messageNum] = Message({
            sender: msg.sender,
            data: data,
            header: MessageHeader({
                kind: MessageKind_unsigned,
                destChain: destChain,
                gasLimit: gasLimit,
                fee: fee
            }),
            timestamp: block.timestamp
        });
        
        emit InboxMessageDelivered(messageNum, data);
        
        return messageNum;
    }
    
    // 強制包含消息(需要等待期限)
    function forceInclusion(
        bytes[] calldata messagesData
    ) external {
        require(
            block.timestamp >= messagesData[0].timestamp + forceInclusionPeriod,
            "Force inclusion period not passed"
        );
        
        // 批量包含消息
        for (uint i = 0; i < messagesData.length; i++) {
            _includeMessage(messagesData[i]);
        }
    }
}

這意味著:即使排序器 censors 你的交易,你可以等待 24 小時後強行讓交易上鍊。當然,這 24 小時的等待對用戶體驗來說仍然是糟糕的。

第六章:排序器中心化風險

6.1 當前排序器架構

說了這麼多原始碼,最核心的問題要直面:Arbitrum 的排序器是中心化的。

目前(截至 2026 年 Q1),Arbitrum 的排序器由 Offchain Labs 單獨運營。這意味著:

  1. 交易審查風險:Offchain Labs 可以選擇性地不處理某些交易
  2. 訂單操縱風險:排序器可以重排交易以獲取 MEV(最大可提取價值)
  3. 單點故障風險:如果排序器宕機,整個網路會降級到延遲橋模式

看原始碼中的單一排序器假設:

// sequencer.go
type SingleSequencerConfig struct {
    // 單一排序器地址
    Address common.Address
    
    // 沒有任何備份或冗餘機制
}

// 發布批次時只有一個簽名者
func (s *sequencer) signBatch(batch *DataBatch) ([]byte, error) {
    // 硬編碼的單一簽名
    return crypto.Sign(batchHash, s.privateKey)
}

這段代碼讓我很不爽——幾萬行的原始碼,核心安全模型就是一個單一私鑰。這不是區塊鏈,這是數據庫。

6.2 去中心化排序器計劃

Offchain Labs 當然知道這個問題。他們在路線圖中規劃了「共享排序器」(Shared Sequencer)方案,計劃讓多個排序器共同決定交易順序。

但截至目前,這還只是 PPT 上的美好願景。真正落地的代碼告訴我:你的交易順序由一個公司的服務器決定。

結論:愛之深,責之切

寫了這麼多,我得承認:我個人是看好 Arbitrum 的,否則不會花這麼多時間讀原始碼。

Nitro 的架構設計是聰明的——WASM 執行引擎提供了靈活性,壓縮批次降低了 L1 成本,二分證明機制優雅地解決了爭議問題。

但作為一個密碼朋克精神的信徒,我對排序器中心化這個問題耿耿於懷。區塊鏈的核心價值是去中心化和抗審查,而 Arbitrum 恰恰犧牲了這些特性換取性能。

或許這就是現實的 trade-off。也許用戶不在乎去中心化,只在乎又快又便宜。也許 7 天的挑戰期是可以接受的代價。也許單一排序器的信任假設在大多數場景下是安全的。

我不知道答案。我只知道,看完這些原始碼,我對 Arbitrum 的信任是有條件的——我信任 Offchain Labs 不會作惡,但我永遠無法像信任以太坊那樣信任 Arbitrum。

如果你想自己驗證這些代碼,下載 nitro 倉庫,用 go mod download 安裝依賴,然後用你最喜歡的 IDE 打開 arbnode/sequencer.go 文件。看看那些我漏掉的細節,告訴我哪裡理解錯了。

References:

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

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

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