昨天发生了一件令人震惊的事情:以太坊二层扩容解决方案Linea主动停机了,这是为了减轻Velocore黑客攻击事件的影响。这件事让人们开始怀疑Web3倡导的去中心化价值观。事实上,这种事件发生的根本原因是基础设施的不完善,即缺乏足够的去中心化。由于大多数以太坊二层依赖于中心化的排序器,因此无法实现真正的去中心化。那么,面对这种情况,我们应该怎么办呢?
对于二层而言,排序器不去中心化带来的最直接危害是抗审查性和活性。如果排序器很少,它可以拒绝处理用户的交易,导致用户的资金无法离开二层。因此,解决二层的抗审查问题是非常重要的。
在过去的几年里,许多以太坊二层解决方案提出了各种解决方案来解决抗审查问题。这些方法可以在一定条件下对排序器产生制衡,防止其无端拒绝用户的交易请求。
今天,来自台北以太坊协会的NIC Lin亲自实验了Optimism、Arbitrum、StarkNet和zkSync等四个主流Rollup的抗审查交易功能,并深入分析了Force Inclusion的机制设计。这对于以太坊社区和大户来说是非常有参考价值的。
交易抗审查性对于区块链来说非常重要。以太坊目前的交易抗审查能力来自于众多的验证者。但是对于Rollup来说,只有一个中心化的排序器,它可以拒绝处理用户的交易,导致用户的资金被扣留无法离开。
为了解决这个问题,可以利用一种称为“Force Inclusion”的机制。该机制允许用户将交易直接插入到Rollup合约中,从而继承L1的抗审查能力。具体实现方法是在合约中加入一个设计,让用户可以将交易插入到Rollup合约,而排序器无法阻止用户在L1强制插入交易。
然而,为了避免影响到排序器收集的交易,Rollup一般不会让Force Inclusion交易立即生效,而是让用户将交易插入到L1上的等待队列中。在等待窗口期结束后,用户可以将这些交易强制插入到Rollup合约中。
Optimism的Force Inclusion机制的具体实现是通过OptimismPortal合约将交易发送到L1CrossDomainMessenger合约,然后转换为一笔L2交易执行。用户可以直接调用OptimismPortal合约的depositTransaction函数,将想要在L2执行的交易参数填入其中。
如果要调用Optimism的Force Inclusion功能,只需要用自己的L2地址调用OptimismPortal合约的depositTransaction函数,将想要在L2执行的交易参数填入其中。此时,交易格式和正常的L2交易一致。
总的来说,通过Force Inclusion机制,可以解决二层的抗审查问题,让用户能够自主插入交易,并继承L1的抗审查能力。回归简体中文:ism节点指的是Sequencer,毕竟这关系到交易排序,所以只有Sequencer可以决定何时要将前述事件转换成L2交易。
在监听到TransactionDeposited事件时,Sequencer并不一定会马上将event转换成L2交易,可以有一段延时,这段时间的最大值称为SequencerWindow。
目前Optimism主网上的Sequencer Window为24小时,也就是当用户从L1存入一笔钱或Force Inclusion一条交易,最糟情况是24小时后才被收入到L2交易历史中。
Arbitrum的Force Inclusion机制
在Optimism中L1的Deposit操作会抛出一个Transaction Deposited事件,剩下的就是等待Sequencer收录上述操作;但在Arbitrum中发生于L1的操作(存钱或传消息给L2等)会被存在L1上的一个队列里,而不是单纯抛出个事件。
Sequencer会被给予一段时间将上述队列里的交易纳入L2交易历史,如果时间到了Sequencer都没有作为,那任何人都可以去替Sequencer完成。
Arbitrum会在L1合约维护一个Queue,如果Sequencer没有主动处理Queue里的交易,时间到了任何人都可以把Queue里的交易强制收录到L2交易历史中。
Arbitrum的设计中,L1上发生的如存款等操作都要经由Delayed Inbox合约,顾名思义这里的操作都会延迟生效;另一个合约则是Sequencer Inbox,是Sequencer把L2交易上传到L1时的直接场所。每次Sequencer上传L2交易时,都可以顺便从Delayed Inbox取出一些待处理的交易一并写进交易历史中。
Sequencer写入新交易时可以顺便从DelayedInbox拿出交易一起写入
复杂的设计以及凡善可陈的参考资料
如果读者直接参考Arbitrum官方关于Sequencer及Force Inclusion的章节,会看到里面提到了Force Inclusion大致如何运作,以及一些参数名称和函数名称:
使用者先去DelayedInbox合约调用sendUnsignedTransaction函数,如果Sequencer没在约24小时内收录,那使用者可以调用SequencerInbox合约的forceInclusion函数。然后Arbitrum官方也没把函数的链接附加在官网文档里,只能自己去看合约代码里相对应的函数。
当找到sendUnsignedTransaction函数后,你发现竟然要自己填nonce值还有maxFeePerGas值。是哪个地址的nonce?是哪个网络上的maxFeePerGas?要怎么填比较好?没有文件参考,连Natpsec都没有。然后你还会在Arbitrum合约里发现一堆看着相似的函数:
sendL1FundedUnsignedTransaction、sendUnsignedTransactionToFork、sendContractTransaction、sendL1FundedContractTransaction,一样没有文件告诉你这些函数的区别、该怎么用、参数该怎么填,连Natpsec都没有。
你抱着姑且一试的心态来试填参数并送出交易,想用试错的方式看能不能找出正确的用法,但发现这些函数全都会把你的L1地址做AddressAliasing,导致最终在L2上发起交易时的Sender根本是不一样的地址,于是你的L2地址一动不动。
sendL2Message
后来偶然点开Google搜索,才发现原来Arbitrum自己有一个Tutorial程式库,里面有脚本示范怎么从L1发送L2交易(也就是Force Inclusion的意思),然后它列举的函数完全不是上面提到的任何一个,而是一个叫sendL2Message的函数,而且message参数要带入的竟然是用L2账户签完名的交易?
谁会知道要“通过Force Inclusion送给L2的消息”竟然会是一笔“签完名的L2交易”?而且没有任何文件及Natspec解释什么时候用及如何使用这个函数。
结论:要手动产生一个Arbitrum的强制交易比较麻烦,建议就照着官方Tutorial跑Arbitrum SDK呗。Arbitrum不像其他Rollup有清楚的开发者文件及程式码附注,许多函数的用途和参数缺乏说明,导致开发者得花费比预期多更多的时间来接入和使用。我也在Arbitrum Discord上询问Arbitrum的人,但并没有得到令人满意的答案。
在Discord上询问,对方也只会叫我去看sendL2Message,没有想要解释其他函数的功能(甚至是Force Inclusion文档里提到的sendUnsignedTransaction)是什么用途、怎么用、什么时候用。
StarkNet的ForceInclusion机制
很遗憾地,StarkNet目前还没有ForceInclusion机制。只有两篇在官方论坛上讨论到Censorship及ForceInclusion的文章。
无法证明失败的交易
上述原因其实是因为,StarkNet的零知识证明系统没办法证明一笔失败的交易,所以不能允许Force Inclusion。因为如果有人恶意(或无意)Force Include一笔失败的、无法被证明的交易,那StarkNet就会直接卡住:因为交易被强制收入后,Prover就必须证明该笔失败交易,但它却没办法证明。
而StarkNet预期在v0.15.0版引入证明失败交易的功能,之后应该就可以进一步实现Force Inclusion机制。
zkSync的ForceInclusion机制
zkSync的L1->L2讯息传送以及Force Inclusion机制,都是透过MailBox合约的requestL2Transaction函数进行,使用者指定L2地址、calldata、附加的ETH数量、L2GasLimit值等,requestL2Transaction会将这些参数组合成一个L2交易,然后放进优先队列(PriorityQueue)中,Sequencer会在交易打包上传到L1时(通过commitBatches函数),说明要顺便从优先队列中拿出多少笔交易一起收录进L2交易记录中。
zkSync在Force Inclusion形式上和Optimism很像,都是以发起者的L2地址(与L1地址一致)去调用相关函数,并填入资料(被呼叫者、calldata等等),而不是像Arbitrum一样是填一笔签完名的L2交易;但在设计上则是和Arbitrum一样,都是在L1维护一个队列Queue,并由Sequencer从Queue中拿出用户直接提交的待处理交易,并写入交易历史中。
如果你透过zkSync的官方桥去Deposit ETH,像是这笔交易,它便是去呼叫MailBox合约的requestL2Transaction函数,它会将这个Deposit ETH的L2交易放进优先队列中抛出一个NewPriorityRequest事件。因为合约把L2交易资料编码成一串bytes字串所以不易读,改成看这笔L1交易的参数的话,会看到参数中L2的接收方也是交易的发起人(因为是Deposit给自己),所以过一阵子这笔L2交易被Sequeuncer从优先队列拿出,并收录进交易历史时,它会在L2上被转换成一笔自己转给自己的交易,而转账的金额就是交易发起人在L1的Deposit ETH交易中带上的ETH金额。
L1Deposit交易中,交易发起者和接收者都是0xeDc1…6909,金额是0.03ETH,calldata为空
L2上会出现一笔0xeDc1…6909自己转账给自己的交易,交易类型(TxnType)是255,也就是系统交易。接着我直接像之前实验OP的强制交易功能一样,调用zkSync的requestL2Transaction函数,发了一笔自转账:没有带任何ETH,calldata带入“force inclusion”字串的HEX编码。接着它被转换成L2上一笔自己转自己的交易,calldata里是“force inclusion”的十六进制字串:0x666f72636520696e636c7573696f6e。
当Sequencer把交易从PriorityQueue拿出来并写进交易历史中,在L2上就会转换成相对应的L2交易。
透过requestL2Transaction函数,使用者可以用和L2地址一样的L1账户,在L1提交资料,指定L2接收方、附带的ETH金额以及calldata。如果使用者要call其他合约、带不同Data,那一样就是将参数一一填入requestL2Transaction函数。
还没有让使用者强制收录的功能
虽然L2交易放到优先队列中后,会顺便计算出这笔L2交易被Sequencer收录的等待期限,但目前zkSync设计中并没有让使用者能强制执行的Force Inclusion函数,等于是只做半套。也就是虽然有“收录等待期限”,但实际上还是“看Sequencer要不要收入”:Sequencer可以等到过期后才收入,也可以永远不再收入优先队列中任何交易。
未来zkSync应该要加入相关函数,让使用者可以在收入有效期过了但都还没被Sequeuncer收录时,能强制把交易包含进L2交易历史,如此才是真正有效的Force Inclusion机制。
总结
L1靠为数众多的验证者们来确保网络的“安全性”及“抗审查能力”,Rollup因为都是由少数甚至单一的Sequencer来写入交易,抗审查能力更弱。因此Rollup需要有Force Inclusion机制来让使用者可以绕过Sequencer,将交易写入历史中,避免被Sequencer审查导致无法使用也无法把资金撤离该Rollup。
Force Inclusion让使用者可以强制将交易写入历史中,但在设计上需在“交易是否能立即插入历史、立即生效”上做选择。如果允许交易立即生效,那就会对Sequencer产生负面影响,因为L2上等待被收入的交易都可能会被L1强制收入的交易所影响。
因此目前Rollup的Force Inclusion机制都会先让L1上插入的交易进入等待状态,并让Sequencer有一段时间窗口来反应、来选择要不要收入这些等待中的交易。
zkSync和Arbitrum都是在L1维护一个队列Queue,用来管理使用者从L1送出的L2交易或给L2的讯息。Arbitrum称为DelayedInbox;zkSync称为PriorityQueue。
但zkSync送出L2交易的方式和Optimism比较像,都是以L2地址去L1上发送消息,如此转换为L2交易后,其发起人才会是该L2地址。Optimism送L2交易的函数称为depositTransaction;zkSync称为requestL2Transaction。而Arbitrum则是生成一笔完整的L2交易并签名,然后透过sendL2Message函数送出,Arbitrum在L2上会透过签名还原签名者来作为L2交易的发起人。
StarkNet目前还没有Force Inclusion机制;zkSync则是像做了半套的Force Inclusion,—有PriorityQueue且每个Queue里的L2交易都有收录有效期限,但这个有效期限目前只是装饰用,实际上Sequencer可以选择完全不收入任何PriorityQueue里的L2交易。