1

L2 - 深入理解Arbitrum

 2 years ago
source link: https://learnblockchain.cn/article/2640
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
L2 - 深入理解Arbitrum | 登链社区 | 深入浅出区块链技术

L2 - 深入理解Arbitrum

Arbitrum是Layer2 Rollup的一种方案。采用挑战机制确定Rollup状态的终局性。为了引入轻便挑战机制,Arbitrum定义了AVM,一种可以方便证明执行状态的虚拟机,并设计了mini语言和编译器。在AVM上模拟了EVM的执行环境,兼容EVM。挑战时将执行过程进行400分分割,由L1执行少量指令确定状态是否正确。

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的开发者文档给出了各个模块关系:

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

这个模块关系图太过笼统,再细分一下:

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中。

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实现:

总结一下,AVM中的PC,Stack,Register等等的状态都能通过hash结果表示。AVM整个状态由这些hash值的拼接数据的hash表示。

Rollup Challenge

在提交到L1的状态有分歧时,挑战双方(Asserter和Challenger)先将状态分割,找出“分歧点”。明确分歧点后,挑战双方都可提供执行环境,L1执行相关操作确定之前提交的状态是否正确。L1的挑战处理逻辑实现在arb-bridge-eth/contracts/challenge/Challenge.sol。整个挑战机制有超时机制保证,为了突出核心流程,简化流程如下图所示:

挑战者通过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发起分歧点分割。

  • 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。

  • 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执行少量指令确定状态是否正确。

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 17小时前
  • 阅读 ( 49 )
  • 学分 ( 2 )
  • 分类:扩容

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK