Layer 2:深入理解Arbitrum

2021-06-23 15:06:36

Arbitrum是Layer2 Rollup的一種方案。和Optimism類似,狀態的終局性採用“挑战”(challenge)機制進行保證。Optimism的挑战方法是將某個交易完全在Layer1模擬執行,判斷交易執行後的狀態是否正確。這種方法需要在Layer1模擬EVM的執行環境,相對復雜。Arbitrum的挑战相對輕便一些,在Layer1執行某個操作(AVM),確定該操作執行是否正確。Arbitrum介紹文檔中提到,整個挑战需要大概500字節的數據和9w左右的gas。為了這種輕便的挑战機制,Arbitrum實現了AVM虛擬機,並在AVM虛擬機中實現了EVM的執行。AVM虛擬機的優勢在於底層結構方便狀態證明。

Arbitrum的开發者文檔詳細介紹了Arbitrum架構和設計。對AVM以及L1/L2交互細節感興趣的小夥伴可以耐心地查看"Inside Arbitrum"章節:

https://developer.offchainlabs.com/docs/developer_quickstart

整體框架

Arbitrum的开發者文檔給出了各個模塊關系:

Layer 2:深入理解Arbitrum

Arbitrum的系統主要由三部分組成(圖中的右部分,從下到上):EthBridge,AVM執行環境和ArbOS。EthBridge主要實現了inbox/outbox管理以及Rollup協議。EthBridge實現在Layer1。ArbOS在AVM虛擬機上執行EVM。簡單的說,Arbitrum在Layer2實現了AVM虛擬機,在虛擬機上再模擬EVM執行環境。用AVM再模擬EVM的原因是AVM的狀態更好表達,便於Layer1進行挑战。

EthBridge和AVM執行環境對應的源代碼:

https://github.com/OffchainLabs/arbitrum.git

ArbOS對應的源代碼:

https://github.com/OffchainLabs/arb-os.git

這個模塊關系圖太過籠統,再細分一下:

Layer 2:深入理解Arbitrum

EthBridge主要實現了三部分功能:inbox,outbox以及Rollup協議。inbox中“存放”交易信息,這些交易信息會“同步”到ArbOS並執行。outbox中“存放”從L2到L1的交易,主要是withdrawl交易。Rollup協議主要是L2的狀態保存以及挑战。特別注意的是,Arbitrum的所有的交易都是先提交到L1,再到ArbOS執行。ArbOS除了對外的一些接口外,主要實現了EVM模擬器。整個模擬器實現在AVM之上。整個EVM模擬器採用mini語言實現,Arbitrum實現了AVM上的mini語言編譯器。簡單的說,Arbitrum定義了新的硬件(machine)和指令集,並實現了一種上層語言mini。通過mini語言,Arbitrum實現了EVM模擬器,可以執行相應交易。

AVM State

因為所有的交易都是在AVM執行,交易的執行狀態可以用AVM狀態表示。AVM相關實現的代碼在arbitrum/packages/arb-avm-cpp中。

Layer 2:深入理解Arbitrum

AVM的狀態由PC,Stack,Register等狀態組成。AVM的狀態是這些狀態的hash值拼接後的hash結果。

AVM使用c++實現,AVM表示的邏輯實現在MachineStateKeys類的machineHash函數(machinestate.cpp)中。AVM的特別之處就是除了執行外,還能較方便的表達(證明)執行狀態。深入理解AVM的基本數據結構,AVM的基本的數據類型包括:

 using value =
    std::variant<Tuple, uint256_t, CodePointStub, HashPreImage, Buffer>;
enum ValueTypes { NUM, CODEPT, HASH_PRE_IMAGE, TUPLE, BUFFER = 12, CODE_POINT_STUB = 13 };    
  • uint256_t - 整數類型

  • CodePoint - 當前代碼指令表示

  • Tuple - 元組,由8個Value組成。元組中的某個元素依然可以是元組

  • Buffer - 數組,最長為2^64

  • HashPreImage - 固定的hash類型,hashValue = hash(value, prevHashValue)

每種數據類型除了數據表示外,還能非常方便地計算其hash值作為狀態。詳細看看CodePoint和Tuple基本數據類型。

CodePoint

CodePoint類型將多個操作“捆綁”在一起,每個CodePoint除了記錄當前的Operation外,還包括前一個CodePoint的hash信息。這樣所有的Operation可以串連起來,當前的CodePoint除了能表達當前的Operation外,還能明確Operation的依賴關系。CodePoint的類型定義在:packages/arb-avm-cpp/avm_values/include/avm_values/codepoint.hpp。

 struct CodePoint {
    Operation op;
    uint256_t nextHash;
    CodePoint(Operation op_, uint256_t nextHash_)
        : op(op_), nextHash(nextHash_) {}
    bool isError() const {
        return nextHash == 0 && op == Operation{static_cast<OpCode>(0)};
    }
};

Tuple

Tuple類型由RawTuple實現。RawTuple是由一組value組成。Tuple限制最多8個value。

 struct RawTuple {
    HashPreImage cachedPreImage;
    std::vector<value> data;
    bool deferredHashing = true;
    RawTuple() : cachedPreImage({}, 0), deferredHashing(true) {}
};

Tuple的類型定義在:packages/arb-avm-cpp/avm_values/include/avm_values/tuple.hpp。

在理解了基礎類型的基礎上,DataStack可以由一系列Tuple實現:

Layer 2:深入理解Arbitrum

總結一下,AVM中的PC,Stack,Register等等的狀態都能通過hash結果表示。AVM整個狀態由這些hash值的拼接數據的hash表示。

Rollup Challenge

在提交到L1的狀態有分歧時,挑战雙方(Asserter和Challenger)先將狀態分割,找出“分歧點”。明確分歧點後,挑战雙方都可提供執行環境,L1執行相關操作確定之前提交的狀態是否正確。L1的挑战處理邏輯實現在arb-bridge-eth/contracts/challenge/Challenge.sol。整個挑战機制有超時機制保證,為了突出核心流程,簡化流程如下圖所示:

Layer 2:深入理解Arbitrum

挑战者通過initializeChallenge函數發起挑战。接下來挑战者(Challenger)和應战者(Asserter)通過bisectExecution確定不可再分割的“分歧點”。在確定分歧點後,挑战者通過oneStepProveExecution函數確定Assert之前提交的狀態是否正確。

  • initializeChallenge

         function initializeChallenge(
            IOneStepProof[] calldata _executors,
            address _resultReceiver,
            bytes32 _executionHash,
            uint256 _maxMessageCount,
            address _asserter,
            address _challenger,
            uint256 _asserterTimeLeft,
            uint256 _challengerTimeLeft,
            IBridge _bridge
        ) external override {
            ...
           asserter = _asserter;
            challenger = _challenger;
            ...
            turn = Turn.Challenger;
            challengeState = _executionHash;
            ...
        }

    initializeChallenge確定挑战者和應战者,並確定需要挑战的狀態(存儲在challengeState)。challengeState是由一個和多個bisectionChunk狀態hash組成的merkle樹樹根:

    整個執行過程可以分割成多個小過程,每個小過程(bisection)由起始和結束的gas和狀態來表示。

    turn用來記錄交互順序。turn = Turn.Challenger表明在初始化挑战後,首先由Challenger發起分歧點分割。

  • Layer 2:深入理解Arbitrum

  • bisectExecution

    bisectExecution挑選之前分割片段,並如可能將片段進行再次分割:

    bisectExecution的函數定義如下:

         function bisectExecution(
            bytes32[] calldata _merkleNodes,                                                            
            uint256 _merkleRoute,                                                                      
            uint256 _challengedSegmentStart,                                                            
            uint256 _challengedSegmentLength,                                                          
            bytes32 _oldEndHash,
            uint256 _gasUsedBefore,
            bytes32 _assertionRest,                                                                    
            bytes32[] calldata _chainHashes                                                            
        ) external onlyOnTurn {

    _chainHashes是再次分割點的狀態。如果需要再次分割,需要滿足分割點的個數規定:

            uint256 private constant EXECUTION_BISECTION_DEGREE = 400;
           require(
                _chainHashes.length ==                                                                  
                    bisectionDegree(_challengedSegmentLength, EXECUTION_BISECTION_DEGREE) + 1,          
                "CUT_COUNT"
            );

    簡單的說,每次分割,必須分割成400份。

    _oldEndHash是用來驗證狀態這次分割的分割片段是上一次分割中的某個。需要檢查分割的有效性:

             require(_chainHashes[_chainHashes.length - 1] != _oldEndHash, "SAME_END");                  
            require(
                _chainHashes[0] == ChallengeLib.assertionHash(_gasUsedBefore, _assertionRest),          
                "segment pre-fields"                                                                    
            );  
            require(_chainHashes[0] != UNREACHABLE_ASSERTION, "UNREACHABLE_START");                    
            require(
                _gasUsedBefore < _challengedSegmentStart.add(_challengedSegmentLength),                
                "invalid segment length"                                                                
            );  

    起始狀態正確。這次分割不能超出上次分割範圍,並且最後一個狀態和上一個分割的結束狀態不一樣。

             bytes32 bisectionHash =                                                                     
                ChallengeLib.bisectionChunkHash(                                                        
                    _challengedSegmentStart,                                                            
                    _challengedSegmentLength,
                    _chainHashes[0],
                    _oldEndHash
                );
            verifySegmentProof(bisectionHash, _merkleNodes, _merkleRoute);

    通過merkle樹的路徑檢查確定起始狀態和結束狀態是上一次某個分割。

    updateBisectionRoot(_chainHashes, _challengedSegmentStart, _challengedSegmentLength);

    更新細分分割對應的challengeState。

  • Layer 2:深入理解Arbitrum

  • oneStepProveExecution

    當不能分割後,挑战者提供初始狀態(證明),並由L1進行相應的計算。計算的結果應該和提供的_oldEndHash不一致。不一致說明挑战者成功證明了之前的計算結果不對。

                 (uint64 gasUsed, uint256 totalMessagesRead, bytes32[4] memory proofFields) =
                    executors[prover].executeStep(
                        bridge,
                        _initialMessagesRead,
                        [_initialSendAcc, _initialLogAcc],
                        _executionProof,
                        _bufferProof
                    );

    通過executeStep計算出正確的結束狀態。executeStep實現在packages/arb-bridge-eth/contracts/arch/OneStepProofCommon.sol中。核心是executeOp函數,針對當前的context讀取op,執行並更新狀態。感興趣的小夥伴可以自行查看。

                 rootHash = ChallengeLib.bisectionChunkHash(
                    _challengedSegmentStart,
                    _challengedSegmentLength,
                    oneStepProofExecutionBefore(
                        _initialMessagesRead,
                        _initialSendAcc,
                        _initialLogAcc,
                        _initialState,
                        proofFields
                    ),
                    _oldEndHash
                );
            }
            verifySegmentProof(rootHash, _merkleNodes, _merkleRoute);

    確定初始狀態和結束狀態是上一次挑战狀態中的某個分割。初始狀態由提供的證明(proof)計算獲得。

                 require(
                    _oldEndHash !=
                        oneStepProofExecutionAfter(
                            _initialSendAcc,
                            _initialLogAcc,
                            _initialState,
                            gasUsed,
                            totalMessagesRead,
                            proofFields
                        ),
                    "WRONG_END"
                );

    確認_oldEndHash和計算獲得結束狀態不一樣。不一樣才說明之前提交的結束狀態是錯誤的。

    _currentWin();

    計算完成後,確定勝利方。

總結:

Arbitrum是Layer2 Rollup的一種方案。採用挑战機制確定Rollup狀態的終局性。為了引入輕便挑战機制,Arbitrum定義了AVM,一種可以方便證明執行狀態的虛擬機,並設計了mini語言和編譯器。在AVM上模擬了EVM的執行環境,兼容EVM。挑战時將執行過程進行400分分割,由L1執行少量指令確定狀態是否正確。

鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播信息之目的,不構成任何投資建議,如有侵權行為,請第一時間聯絡我們修改或刪除,多謝。

推薦文章

歐盟打響穩定幣战爭:21家發行商爭奪,Circle搶先登陸,Tether扶持“代理人”

作者:Weilin,PANews 歐盟《加密資產市場監管法案》(MiCA)對穩定幣發行方的監管規則...

PANews
6 10小時前

Gecky x WEEX聯名車隊馳騁歐洲頂級方程式賽道

當地時間 9 月 29 日,在巴塞羅那舉行的 Euroformula Open 第三場比賽中,Vl...

星球日報
7 10小時前

Fractal Bitcoin分形比特幣深度研究報告:原生擴展的比特幣高速公路,重新定義比特幣的可能性

比特幣網絡擴展問題一直是區塊鏈領域的核心話題。從最初的隔離見證(SegWit)到閃電網絡(Ligh...

星球日報
6 10小時前

薩爾瓦多的比特幣之城,建的怎么樣了?

在全球,薩爾瓦多或許只是一個名不見經傳的邊陲小國,給人留有的模糊印象是熱辣的火山地貌與多樣的生態系...

陀螺財經
6 10小時前

大餅新高、山寨暴跌,誰賺走了你的錢?

「如果不看大餅,只看山寨,我還以為 312 了。」這是一位社區成員發出的無奈感嘆。 這一感嘆並非空...

星球日報
7 10小時前

Stacks完成Nakamoto升級,BTC DeFi會是下一個關注點嗎?

當比特幣突破 9 萬美金,加密市場各個生態都开始了自己的狂歡。 AI 敘事持續火熱,Meme 持續...

星球日報
6 10小時前