简单来说,Arbitrum在Layer2实现了AVM虚拟机,在虚拟机上再模拟EVM执行环境。推荐阅读:《》Arbitrum是Layer2Rollup的一种方案。和Optimism类似,状态的终局性采用「挑战」(challenge)机制进行保证。Optimism的挑战方法是将某个交易完全在Layer1模拟执行,判断交易执行后的状态是否正确。这种方法需要在Layer1模拟EVM的执行环境,相对复杂。Arbitrum的挑战相对轻便一些,在Layer1执行某个操作(AVM),确定该操作执行是否正确。Arbitrum介绍文档中提到,整个挑战需要大概500字节的数据和9w左右的gas。为了这种轻便的挑战机制,Arbitrum实现了AVM虚拟机,并在AVM虚拟机中实现了EVM的执行。AVM虚拟机的优势在于底层结构方便状态证明。Arbitrum的开发者详细介绍了Arbitrum架构和设计。对AVM以及L1/L2交互细节感兴趣的小伙伴可以耐心地查看「InsideArbitrum」章节。整体框架Arbitrum的开发者文档给出了各个模块关系:Arbitrum的系统主要由三部分组成(图中的右部分,从下到上):EthBridge,AVM执行环境和ArbOS。EthBridge主要实现了inbox/outbox管理以及Rollup协议。EthBridge实现在Layer1。ArbOS在AVM虚拟机上执行EVM。
简单的说,Arbitrum在Layer2实现了AVM虚拟机,在虚拟机上再模拟EVM执行环境。用AVM再模拟EVM的原因是AVM的状态更好表达,便于Layer1进行挑战。EthBridge和AVM执行环境对应的ArbOS对应的这个模块关系图太过笼统,再细分一下: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模拟器,可以执行相应交易。AVMState因为所有的交易都是在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的基本的数据类型包括:usingvalue=std::variant
RollupChallenge在提交到L1的状态有分歧时,挑战双方(Asserter和Challenger)先将状态分割,找出「分歧点」。明确分歧点后,挑战双方都可提供执行环境,L1执行相关操作确定之前提交的状态是否正确。L1的挑战处理逻辑实现在arb-bridge-eth/contracts/challenge/Challenge.sol。整个挑战机制有超时机制保证,为了突出核心流程,简化流程如下图所示:挑战者通过initializeChallenge函数发起挑战。接下来挑战者(Challenger)和应战者(Asserter)通过bisectExecution确定不可再分割的「分歧点」。在确定分歧点后,挑战者通过oneStepProveExecution函数确定Assert之前提交的状态是否正确。initializeChallengefunctioninitializeChallenge(IOneStepProof[]calldata_executors,address_resultReceiver,bytes32_executionHash,uint256_maxMessageCount,address_asserter,address_challenger,uint256_asserterTimeLeft,uint256_challengerTimeLeft,IBridge_bridge)externaloverride{...asserter=_asserter;challenger=_challenger;...turn=Turn.Challenger;challengeState=_executionHash;...}initializeChallenge确定挑战者和应战者,并确定需要挑战的状态(存储在challengeState)。challengeState是由一个和多个bisectionChunk状态hash组成的merkle树树根:整个执行过程可以分割成多个小过程,每个小过程(bisection)由起始和结束的gas和状态来表示。turn用来记录交互顺序。turn=Turn.Challenger表明在初始化挑战后,首先由Challenger发起分歧点分割。bisectExecutionbisectExecution挑选之前分割片段,并如可能将片段进行再次分割:bisectExecution的函数定义如下:functionbisectExecution(bytes32[]calldata_merkleNodes,uint256_merkleRoute,uint256_challengedSegmentStart,uint256_challengedSegmentLength,bytes32_oldEndHash,uint256_gasUsedBefore,bytes32_assertionRest,bytes32[]calldata_chainHashes)externalonlyOnTurn{_chainHashes是再次分割点的状态。如果需要再次分割,需要满足分割点的个数规定:uint256privateconstantEXECUTION_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),"segmentpre-fields");require(_chainHashes[0]!=UNREACHABLE_ASSERTION,"UNREACHABLE_START");require(_gasUsedBefore<_challengedSegmentStart.add(_challengedSegmentLength),"invalidsegmentlength");起始状态正确。这次分割不能超出上次分割范围,并且最后一个状态和上一个分割的结束状态不一样。
bytes32bisectionHash=ChallengeLib.bisectionChunkHash(_challengedSegmentStart,_challengedSegmentLength,_chainHashes[0],_oldEndHash);verifySegmentProof(bisectionHash,_merkleNodes,_merkleRoute);通过merkle树的路径检查确定起始状态和结束状态是上一次某个分割。updateBisectionRoot(_chainHashes,_challengedSegmentStart,_challengedSegmentLength);更新细分分割对应的challengeState。oneStepProveExecution当不能分割后,挑战者提供初始状态(证明),并由L1进行相应的计算。计算的结果应该和提供的_oldEndHash不一致。不一致说明挑战者成功证明了之前的计算结果不对。(uint64gasUsed,uint256totalMessagesRead,bytes32[4]memoryproofFields)=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是Layer2Rollup的一种方案。采用挑战机制确定Rollup状态的终局性。为了引入轻便挑战机制,Arbitrum定义了AVM,一种可以方便证明执行状态的虚拟机,并设计了mini语言和编译器。在AVM上模拟了EVM的执行环境,兼容EVM。挑战时将执行过程进行400分分割,由L1执行少量指令确定状态是否正确。