前Arbitrum技术大使解读Arbitrum的组件结构(上)
作者:罗奔奔,前Arbitrum技术大使,极客web3贡献者
本文是Arbitrum前技术大使 及 智能合约自动化审计公司Goplus Security前联合创始人罗奔奔 对Arbitrum One的技术解读。
因为中文圈子里涉及Layer2的文章或资料,缺乏对Arbitrum乃至OP Rollup的专业解读,本文试图通过科普Arbitrum的运转机理,填补这一领域的空缺。由于Arbitrum本身的结构太复杂,全文在尽可能简化的基础上,还是超过了1万字篇幅,所以分成了上下两篇,建议作为参考资料收藏转发!
Rollup排序器简述
Rollup扩容的原理可以概括为两点:
成本优化:将⼤部分运算与存储任务移交至L1链下也即L2上。L2大多是运⾏在单台服务器也即排序器(Sequencer/Operator)上的⼀条链。
排序器在观感上接近于一台中心化服务器,在“区块链不可能三⻆”中舍弃“去中心化”来换取TPS与成本上的优势。 ⽤户可以让L2来代替以太坊处理交易指令,成本比在以太坊上交易要低得多。
(图源:BNB Chain)
安全保障:L2上的交易内容与交易后的状态,会同步⾄以太坊L1,通过合约来校验 状态转换的有效性。同时,以太坊上会保留L2的历史记录,排序器即便永久宕机,他⼈也可以通过以太坊上的记录,还原出整个L2的状态。
从根本上来说,Rollup的安全性是基于以太坊的。排序器如果不知道某个账户的私钥,就无法用该账户的名义发起交易,或者无法篡改该账户的资产余额(即便这么做了,也很快被识破)。
虽然排序器作为系统中枢带有中⼼化色彩,但在成熟度比较高的Rollup方案中,中心化排序器仅能实施交易审查等软性作恶⾏为,或者恶意宕机,但在理想状态的Rollup⽅案中,有相应的⼿段进⾏遏制(比如强制提款或排序证明等抗审查机制)。
(路印协议在L1上的合约源码中设置的,供用户调用的强制提款函数)
而防止Rollup排序器作恶的状态校验⽅式,分为欺诈证明(Fraud Proof)和有效性证明(Validity Proof)两类。使⽤欺诈证明的Rollup⽅案称为OP Rollup(Optimistic Rollup,OPR),⽽因为一些历史包袱,使⽤有效性证明的Rollup往往被称为ZK Rollup(Zero-knowledge Proof Rollup,ZKR),而不是Validity Rollup。
Arbitrum One是典型的OPR,它部署在L1上的合约,并不主动验证提交过来的数据,乐观地认为这些数据没有问题。如果提交的数据有错误,L2的验证者节点会主动发起挑战。
因此OPR也暗含一条信任假设:任意时刻⾄少有⼀个诚实的L2验证者节点。⽽ZKR的合约则通过密码学计算,主动但低成本地验证排序器提交的数据。
(乐观Rollup运转方式)
(ZK Rollup运转方式)
本文会深度介绍乐观式Rollup中的龙头项目——Arbitrum One,覆盖整个系统的方方面面,仔细阅读完后你将对Arbitrum和乐观式Rollup/OPR有深刻的理解。
Arbitrum的核心组件与工作流程
核心合约:
Arbitrum最重要的合约包括SequencerInbox, DelayedInbox, L1 Gateways, L2 Gateways, Outbox, RollupCore, Bridge等。后续将详细介绍。
排序器Sequencer:
接收用户交易并进行排序,计算交易结果,并迅速(通常<1s)返还给用户回执。用户往往在几秒内就能看到自己的交易在L2上链,体验就如同Web2平台。
同时,排序器还会在以太坊链下即时广播最新产生的L2 Block,任何一个Layer2节点都可以异步的接收。但此时,这些L2 Block不具备最终确定性,可以被排序器回滚掉。
每隔几分钟,排序器会将排序后的L2交易数据进行压缩,聚合成批次(Batch),提交至Layer1上的收件箱合约SequencerInbox,以保证数据可用性和Rollup协议的运转。一般而言,被提交至Layer1上的L2数据无法回滚,可以具备最终确定性。
从以上流程中我们可以概括:Layer2有自己的节点网络,但这些节点数量稀少,且一般没有公链惯用的共识协议,所以安全性是很差的,必须要依附于以太坊来保证,数据发布的可靠性与状态转换的有效性。
Arbitrum Rollup协议:
定义Rollup链的区块 RBlock 的结构,链的延续方式,RBlock的发布,以及挑战模式流程等⼀系列的合约。注意,这⾥说的Rollup链并不是大家理解的Layer2账本,而是Arbitrum One为了施展欺诈证明机制,而独立设置的一条抽象出来的“链状数据结构”。
⼀个RBlock可以包含多个L2区块的结果,⽽且数据也迥异,它的数据实体 RBlock 存储在RollupCore的⼀系列合约中。如果⼀个 RBlock 存在问题,Validator将⾯向该RBlock的提交者对其进⾏挑战。
验证者Validator:
Arbitrum的验证者节点其实是Layer2全节点的特殊子集,目前有白名单准入。
Validator根据排序器提交至SequencerInbox合约的交易批次batch,来创建新的RBlock(Rollup区块,也叫断⾔assertion),并监控当前Rollup链的状态,对排序器提交的错误数据进⾏挑战。
主动型的Validator需要事先在ETH链上质押资产,有时我们也称其为Staker。不进⾏质押的Layer2节点虽然也可以监控Rollup的运⾏动态,向⽤户发送异常报警等,但⽆法在ETH链上直接对排序器提交的错误数据进行⼲预。
挑战:
基础步骤可以概括为多轮互动式细分、单步证明。在细分环节,挑战双⽅先对有问题的交易数据进⾏多轮回合制细分,直⾄分解出有问题的那⼀步操作码指令,并进⾏验证。“多轮细分-单步证明” 这种范式,被Arbitrum开发者认为是欺诈证明中最节省gas的实现⽅式。所有环节都在合约控制之下,没有⼀⽅可以作弊。
挑战期:
由于OP Rollup的乐观optimistic本质,每个RBlock提交上链后,合约并不主动检查,预留给验证者一段时间窗⼝期去证伪。此时间窗⼝即为挑战期,在Arbitrum One主⽹上为1周。挑战期结束后,该RBlock才会被最终确认,块内对应的从L2传递到L1的消息(比如通过官方桥执行的提款操作)才能被放行。
ArbOS, Geth, WAVM:
Arbitrum采用的虚拟机名为AVM,包含Geth和ArbOS两部分。Geth是以太坊最常⽤的客户端软件,Arbitrum对其进⾏了轻量化的修改。ArbOS负责所有L2相关的特殊功能,如⽹络资源管理、⽣成L2区块、与EVM协同⼯作等。我们将两者的组合视为⼀个Native AVM,也就是Arbitrum采用的虚拟机。WAVM是把AVM的代码编译为Wasm后的结果。Arbitrum挑战流程中,最后的那个“单步证明”,验证的就是WAVM指令。
在此,我们可以将上述各个组件之间的关系和⼯作流⽤下图来表示:
L2交易生命周期
一笔L2交易的处理流程如下:
1.用户向排序器发送交易指令。
2.排序器先对待处理交易进数字签名等数据的验证,剔除无效交易,并进行排序和运算。
3.排序器将交易回执发送给⽤户(通常都⾮常快),但这只是排序器在ETH链下进行的“预处理”,处于Soft Finality的状态,并不可靠。但对于信任排序器的⽤户(⼤部分⽤户),可以乐观的认为交易已经完成,不会被回滚。
4.排序器将预处理后的交易原始数据,⾼度压缩后封装为⼀个Batch(批次)。
5.每隔⼀段时间(受到数据量、ETH拥堵程度等因素影响),排序器会向L1上的 Sequencer Inbox 合约发布交易Batch。此时可认为,交易已拥有最终性Hard Finality。
Sequencer Inbox合约
合约会接收排序器提交的交易batch,保证数据可⽤性。深⼊地看,SequencerInbox中的batch数据完整记录了Layer2的交易输入信息,即使排序器永久宕机,任何⼈都可以根据batch的记录还原Layer2的当前状态,接替故障/跑路的排序器。
⽤物理的⽅式理解,我们所看到的L2,只是 SequencerInbox 中batch的投影,光源则是STF。因为光源STF不会轻易变化,所以影⼦的形状只由充当物体的batch来决定。
Sequencer Inbox合约⼜称为快箱,排序器专门向其提交已经被预处理的交易,且只有排序器可向其提交数据。对应快箱的是慢箱Delayer Inbox,其功能在后续流程中会有描述。
Validator会一直监听SequencerInbox合约,每当排序器向该合约发布Batch后,就会抛出一个链上事件,Validator监听到这个事件发生后,就会去下载batch数据,在本地执⾏后,向ETH链上的Rollup协议合约发布RBlock 。
Arbitrum的bridge合约内有个叫累加器accumulator的参数,会针对新提交的L2 batch,以及慢Inbox上新接收的交易数和信息,进行记录。
(排序器向SequencerInbox不断提交batch)
(Batch的具体信息,data字段对应着Batch数据,这部分数据尺寸很大,截图没显示完)
SequencerInbox合约有两个主要函数:
add Sequencer L2Batch From Origin(),排序器每次都会调用该函数向Sequencer Inox合约提交Batch数据。
force Inclusion(),该函数任何人都可以调用,用于实现抗审查交易。这个函数的生效方式,会在后面谈到Delayed Inbox合约时详细解释。
上述两个函数都会调用 bridge.enqueueSequencerMessage(),来更新bridge合约内的累加器参数accumulator。
Gas定价
显然,L2的交易不可能免费,因为这样会引来DoS攻击,另外则是排序器L2本身的运⾏成本,以及在L1上提交数据都会有开销。⽤户在Layer2网络内发起交易时,gas费的结构如下:
占用Layer1资源产生的数据发布成本,主要来自于排序器提交的batch(每个batch有很多用户的交易),成本最终由交易发起者们均摊。数据发布产生的手续费定价算法是动态的,排序器会根据近期的盈亏状况、batch⼤⼩、当前以太坊gas价格进⾏定价。
用户因占用Layer2资源产生的成本,设定了⼀个可以保证系统稳定运⾏的,每秒处理的gas上限(⽬前Arbitrum One是700万)。L1和L2的gas指导价格均由ArbOS跟踪并调整,公式暂时不在此赘述。
虽然具体的gas价格计算过程⽐较复杂,但⽤户无需感知到这些细节,可以明显感到 Rollup交易费⽤比ETH主网便宜的多。
乐观式欺诈证明
回顾上文,L2实际上只是排序器在快箱中提交的交易输入batch的投影,也即:
Transaction Inputs -> STF -> State Outputs。输入已经确定,STF是不变的,则输出结果也是确定的,而欺诈证明和Arbitrum Rollup协议这套系统就是把输出的状态根,以RBlock (aka断言)的形式发布到L1上并对其进行乐观式证明的一套系统。
在L1上有排序器发布的输⼊数据,也有验证者发布的输出状态。我们再仔细考量⼀下,是否有必要向链上发布Layer2的状态呢?
因为输⼊已经完全决定了输出,而输入数据是公开可见的,再提交输出结果-状态似乎是多余的?但这种想法忽略了L1-L2两个系统之间实际上需要状态结算,也即L2向L1⽅向的提现⾏为,需要有对状态的证明。
在搭建Rollup的时候,⼀条最核⼼的思想就是把⼤部分运算和存储放到L2上来规避L1⾼昂的费⽤,这也就意味着,L1并不知道L2的状态,它仅仅帮助L2排序器发布全体交易的输入数据,但并不负责计算出L2的状态。
⽽提现⾏为,本质上是依照L2给出的跨链消息,从L1的合约⾥解锁相应资⾦,划转到⽤户的L1账户中或完成其他事情。
此时Layer1的合约就会问:你在Layer2上的状态是怎样的,怎么证明你真的拥有这些声明要跨走的资产。这个时候用户要给出对应该的Merkle Proof等。
所以,如果我们构建⼀条没有提现功能的Rollup,理论上不向L1进⾏状态同步是可以的,也不需要欺诈证明等状态证明系统(虽然可能带来其他问题)。但在现实应⽤中,这显然是不可⾏的。
所谓的乐观式证明中,合约不会去检查提交到L1的输出状态是否正确,乐观地认为一切都是准确无误的。乐观证明系统会假设,在任意时刻都有⾄少⼀名诚实的Validator,如果出现错误的状态,则通过欺诈证明进⾏挑战。
这么设计的好处是,不需要主动验证每⼀个发布到L1上的RBlock,避免浪费gas。实际上对于OPR⽽⾔,对每⼀个断⾔进⾏验证也是不现实的,因为每个 Rblock都包含着一或多个L2区块,要在L1上去对每笔交易重新执⾏⼀遍,与直接在L1上执行L2交易无异,这就失去了Layer2扩容的意义。
⽽ZKR不存在这个问题,因为ZK Proof有简洁性,只需要验证⼀个很⼩的Proof,不需要真地去执⾏该Proof背后所对应的许多条交易。所以ZKR并不是乐观式运⾏,每次发布状态都会有Verfier合约进⾏数学验证。
欺诈证明虽然不能像零知识证明那样具有⾼度的简洁性,但Arbitrum使⽤了⼀种“多轮分割-单步证明”的轮流式交互流程,最终需要证明的仅仅是单⼀的虚拟机操作码,成本相对较⼩。
Rollup协议
我们先来看一下,发起挑战和启动证明的入口,也即Rollup协议是如何工作的。
Rollup协议的核心合约是RollupProxy.sol,在保证数据结构一致的情况下,使用了一个罕见的双重代理结构,一个代理对应两个实现RollupUserLogic.sol和RollupAdminLogic.sol,在Scan等工具中目前还无法很好的解析。
另外还有ChallengeManager.sol合约负责管理挑战,OneStepProver系列合约来判定欺诈证明。
(图源:L2BEAT官网)
在RollupProxy中,记录由不同Validator提交的一系列RBlock(aka断言),也即下图中的方块:绿色-已确认,蓝色-未确认,黄色-已证伪。
RBlock中包含了自上一个RBlock以来,一个或多个L2区块执行后的最终状态。这些RBlock在形态上构成了一条形式上的Rollup Chain(注意L2账本本身相区别)。在乐观情况下,这条Rollup Chain应该是没有分叉的,因为有分叉意味着有Validator提交了彼此冲突的Rollup Block。
要提出或认同断言,需要验证者先为该断言质押一定数量的ETH,成为Staker。这样在发生挑战/欺诈证明时,输者的质押品将被罚没,这是保障验证者诚实行为的经济学基础。
图中右下角的111号蓝色块最终会被证伪,因为其父块104号区块是错误的(黄色)。
此外,验证者A提出了106号Rollup Block,而B不同意,对其进行挑战。
在B发起挑战后,ChallengeManager合约负责验证对挑战步骤的细分过程:
1.细分是一个双方轮流互动的过程,一方对某个Rollup Block中包含的历史数据进行分段,另一方指出是哪部分数据片段有问题。类似于二分法(实际是N/K)不断渐进缩小范围的一个过程。
2.之后,可以继续定位至哪条交易及结果有问题,再进一步细分至该交易中有争议的某条机器指令。
3.ChallengeManager合约只检查对原始数据进行细分后,产生的『数据片段』是否有效。
4.当挑战者和被挑战者定位到了将被挑战的那条机器指令后,挑战者调用oneStepProveExecution(),发送单步欺诈证明,证明这条机器指令的执行结果有问题。
单步证明
单步证明是整个Arbitrum的欺诈证明的核心。我们看一下单步证明具体证明的是什么内容。
这需要先理解WAVM,Wasm Arbitrum Virtual Machine,它是一个由ArbOS模块和Geth(以太坊客户端)核心模块共同编译成的虚拟机。由于L2与L1有许多截然不同的地方,原始的Geth核心必须经过轻量修改,并且配合ArbOS一起工作。
所以,L2上的状态转换其实是ArbOS+Geth Core的共同手笔。
Arbitrum的节点客户端(排序器、验证者、全节点等),是将上述ArbOS+Geth Core处理的程序,编译为节点主机能直接处理的原生机器代码(for x86/ARM/PC/Mac/etc.)。
如果把编译后得到的目标语言更改为Wasm,就得到了验证者生成欺诈证明时使用的WAVM,而验证单步证明的合约上,模拟的也是WAVM虚拟机的功能。
那为什么在生成欺诈证明时,要编译为Wasm字节码?主要还是因为,验证单步欺诈证明的合约,要用以太坊智能合约模拟出 能处理某套指令集的虚拟机VM,而WASM易于在合约上实现模拟。
但WASM相比于Native机器代码,运行速度略慢,所以只有在欺诈证明生成及验证的时候,Arbitrum的节点/合约才会用到WAVM。
在之前的多轮互动细分后,单步证明最终证明的是WAVM指令集中的单步指令。
下面的代码中可以看到,OneStepProofEntry首先要判定,待证明指令的操作码属于哪个类别,再调用相应的prover如Mem,Math等,将单步指令传入该prover合约。
最终结果afterHash会回到ChallengeManager,如果该哈希与Rollup Block上记录的,指令运算后的哈希不一致,则挑战成功。如果一致,则说明Rollup Block上记录的这个指令运行结果没问题,挑战失败。
在下一篇文章中,我们将解析Arbitrum乃至于Layer2与Layer1之间处理跨链消息/桥接功能 的合约模块,并进一步阐明,一个真正意义的Layer2应该怎么实现抗审查。