原文作者:NICLin
愿原力包容与你同在。这篇文章将介绍Rollup的交易抗审查机制—ForceInclusion,并以几个著名Rollup的设计与实际作为示例。
先备知识:
知道Rollup的运作以及为什么Rollup需要把交易上传到L1上
交易的抗审查能力交易抗审查(审查抵抗)的能力对一条区块链来说非常重要,如果一条区块链能够为任何审查用户交易,那么这条区块链就和一个Web2服务器没有两样。以太坊目前的交易抗审查能力来自于为数众多的验证者们,如果Alice想要审查Bob的交易、不让他的交易上链,那Alice要不得尝试买通网路中的每一个验证者,要不得垃圾邮件整个网路、不断发送出比Bob交易高的垃圾交易来塞满区块。无论是哪一种方式,她的成本都会非常高。
注:在以太坊目前的PBS架构中,审查的成本确实降低明显,可以参考OFAC审查TornadoCash交易的区块比例。当前的抗审查能力仰赖于OFAC及政府管辖范围之外的独立验证者及中继。
但Rollup呢?Rollup不需要一大堆的验证者来保证它的安全性,补充Rollup只有一个中心化的角色(Sequencer,称为排序器或排序器)来批量区块,它也和L1一样安全。但安全和抗审查能力是两回事,构建了一个Rollup和以太坊一样安全,但却只有一个中心化的Sequencer,那该Sequencer想要审查任何用户的交易都行。
Sequencer可以拒绝收入用户交易,导致用户无法使用也无法离开Rollup
强制包容机制同时要求Rollup和L1一样多的验证者来确保抗审查能力,还不如直接利用L1的抗审查能力:测序仪也将交易数据压缩到L1的Rollup契约上,不如在Rollup契约里加入一个让监控用户也可以插入交易到排序,这样的办法就叫强制包含。只要Sequencer无法审查监控用户的「L1交易」,它并不能阻止用户通过L1强制插入Rollup交易,而Rollup的运行及安全性基础正是基于L1的抗审查能力。
排序器无法审查用户的L1交易,除非有很复杂的成本
注:强制包含是要该Rollup有设计才会有,用户一定可以突破L1强制插入Rollup交易。如果Rollup没有提供强制包含机制,那用户就只能祈祷Sequencer审查不会自己交易。
交易立即生效vs.延迟生效
如果我们允许透过ForceInclusion插入的交易可以直接写入到Rollup的交易历史中(突然立即生效),那Rollup的状态就会立刻被改变,例如Bob通过ForceInclusion机制插入笔「他转1000DAI给」Carol」的交易,如果此时交易立即生效,那最新的状态中Bob的余额就会少1000DAI而Carol会多1000DAI(当然前提是Bob余额超过1000DAI)。
如果Bob交易能直接被写进交易历史中,立即生效,那Rollup的状态就会立刻改变
如果此时Sequencer正在链下集合交易,等到把下一笔交易发送到Rollup合约上,那么就有可能被Bob强制插入并立即生效的交易给影响到。例如Bob只需事先发送了付款「他」转1000DAI给Alice」的交易到Sequencer那,Sequencer相当于验证没问题并承诺会收入,Alice和Bob都可以向Sequencer查询交易是否会被收入并计算出最新状态(Alice多1000DAI,Bob少1000DAI)。但等到Sequencer发现Bob抢先去强制插入交易后,它手上的交易的执行状态都改变了,知道那笔已「Bob转1000DAI给Alice」的交易已经变成会执行失败,Alice也拿不下来到1000DAI。
Sequencer收入Bob的交易,Alice因此相信自己会收到1000DAI
结果Bob直接在L1上强制收入另一笔冲突的交易,导致Sequencer手上的交易无法被收入,Alice也拿不到DAI
这可能不是一个好的用户体验,因此Rollup一般会使强制包含交易立即生效,而是让交易高级到一个「准备中」的状态。Sequencer可以在预算交易发送上来时选择是否要顺便塞进入这些「准备中」状态的交易到批次的最后面,如果Sequencer一直都不想处理这些「准备中」的状态,那么这些交易在过了一段时间后就可以被强制插入到交易历史中。
鲍勃想通过强制收入交易来骗爱丽丝,但实际上交易会先进到等待队列
Sequencer可以自己决定在什么时候「顺便收入」队列等待中交易,所以Bob给AliceDAI的交易会先执行
如果Sequencer一直没有等待队列中的交易,在一段时间内后用户(或者任何人)就可以自己去强制收入。
Sequencer拒绝收入Bob交易,所以Bob从L1将交易等待队列中
排序器仍然可以持续拒绝队列中的交易一段时间
但Sequencer无法永远拒绝等待队列中的交易,一段时间后任何人都可以触发强制收入接下来将依次介绍Optimism、Arbitrum、StarkNet及zkSync等较有名的Rollup的原力包容机制实作。
乐观的力量包容机制首先先介绍Optimism的Deposit流程,这个Deposit不单是指把存钱进Optimism的意思,而相当于更一般的「把给L2的消息」存入L2。L2节点在接收到新存入的消息后就传送消息转换成端口L2交易去执行,传送消息指定的接收方。
用户从L1存款给L2的消息
L1CrossDomainMessenger合约
当一个用户决定ETH或ERC-20代币存进乐观时,他会(交叉网页)与L1上的L1StandardBridge合约交互,指定要存多少数量以及由地址哪个接收等等。接着L1StandardBridge连接消息传递至下一层的L1CrossDomainMessenger合约,这个合约主要是作为一般通用的L1与L2之间相互通讯的合约,L1StandardBridge就是使用这个通用的通讯合约来和L2上的L2StandardBridge沟通,决定谁可以从L2铸造代币或可以从L1提领代币开始。如果开发者需要开发一个L1与L2之间互通、同步状态的合约,那他就可以搭建在L1CrossDomainMessenger之上。
用户的消息传递CrossChainMessenger合约从L1传递到L2
OptimismPortal合约
L1CrossDomainMessenger契约会再将消息发送至最底层的OptimismPortal契约,OptimismPortal契约处理完成后会发出一个TransactionDeposited事件,事件参数包含「发送消息的人」、「接收消息的人」,以及相关的执行参数。
然后L2的Optimism节点会监听OptimismPortal约定所发出的TransactionDeposited事件,并把事件里的参数转换为记下L2的交易,这个L2交易的发起者可能是TransactionDeposited事件里的「送消息的人」、交易的接收者就会是事件里的「接收消息的人」,其他执行参数也是由事件参数传来。
L2节点插入OptimismPortal发出的TransactionDeposited事件转换成注释L2交易
例如这一笔是某个用户透过L1StandardBridge契约存款0.01ETH的交易,这个消息及ETH一路传到OptimismPortal契约(地址是0xbEb5…06Ed),四十后转换成L2交易然后消息发起者是L1CrossDomainMessenger契约;接收者是L2上的L2CrossDomainMessenger合约;附带0.01ETH。
强制包容
当用户想要强制收入他的交易进乐观执行时,他不需要从L1发送消息到L2,所以他不会去使用L1CrossDomainMessenger合约。他要达到的效果是让账单具体「他从他的」L2地址在L2上送出并要执行的交易」能够顺利执行,所以他会直接去和OptimismPortal合约互动,并且让他L2地址去呼叫OptimismPortal合约,这样到时候TransactionDeposited事件转换成L2交易的「交易发起」者将会是他的L2地址”,才能重新看见那笔交易。
从TransactionDeposited事件转换而成的L2交易中,发起人会是Bob自己;接收人是Uniswap合约;而且会附带指定的ETH,就像Bob自己发起L2交易一样
用L2地址呼叫OptimismPortal签约的depositTransaction函式,记清楚L2交易的参数一一填入
我做了一个简单的强制包含交易:以我L2的地址(0xeDc1…6909)转钱给我自己,并附带一个“强制包含”的文字信息。这是我透过OptimismPortal契约执行depositTransaction函式的L1交易,可以看到在发射的TransactionDeposited事件中,from和to都是我自己,剩余的opaqueData栏位里的值(000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015BE000000000000C35000666F72636520696E636C7573696F6E)多少编码了「呼叫DepositTransaction的人附带了多少ETH」、「L2交易发起者要附带ETH给L2接收者」、「L2交易GasLimit」及「给L2接收者的数据」等等资讯。
L1交易发出的TransactionDeposited事件
将这opaqueData几个资讯的数值解读后分别会得到:
「呼叫DepositTransaction的人附带了多少ETH」:,0因为我没有要从L1充值ETH进到L2
「L2交易发起者要附带多少ETH给L2接收者」:5566(wei),因为我要转钱给我自己
「L2交易Gas限额」:50000
「给L2接收者的数据」:0x666f72636520696e636c7573696f6e,清晰“强制包含”这个字串的十六进制编码
接下来没多久就出现转换后的L2交易:记下我转钱给自己的L2交易,金额是5566,数据是“强制包含”字串。并且可以注意到在图中倒数第二行的OtherAttributes中TxnType(交易类型)是系统交易126(System),表示不是我自己在L2发起的交易,是由L1TransactionDeposited事件转换而来。
转换完成的L2交易
如果用户要召集其他合约、带不同的数据,那同样就是将参数一一填写入depositTransaction函,只要记住是用L2地址来去L1上执行,这样到时候L2交易发起者就会是该L2地址。
音序器窗口
前面提到的OptimismL2节点将TransactionDeposited事件转换成L2交易,其实这个Optimism节点指的是Sequencer节点,毕竟这关乎交易排序,所以只有Sequencer可以决定什么时候要转换成L2交易。监听到TransactionDeposited事件时,Sequencer不一定会立即将事件转换成L2交易,但是可以一段时间决定要进行转换,这段时间称为音序器窗口,目前Optimism主网上的音序器窗口为24小时,那么当用户从L1开始时存款补钱或者一个提示,或者强制包含补款交易时,最糟糕的情况会是24小时后才被正式收入进L2交易历史中,不过至少这胜过交易永远没办法被收入的结果。
Arbitrum的强制包容机制在乐观中L1的存款操作会发出一个TransactionDeposited事件,剩余的就是等待Sequencer收入这个存款操作;但在任意中L1的操作(存钱或传消息给L2等等)会被存在L1约定的一个队列里,而不是仅仅发出事件。而定序器会被给予一段时间来将这个队列里的L1操作交易历史中,如果时间到了定序器都作为,那任何人都没有去替换定序器就可以完成。
仲裁会在L1合约维护一个队列,如果Sequencer没有主动收入队列里的交易,时间到了任何人都可以强制收入队列里的交易进交易历史中
L1操作全部都是DelayedInbox的合约,顾名思义这里的操作会延迟失效;另一个合约是SequencerInbox,是给Sequencer上传L2交易的介面。Sequencer上传的交易会直接写进交易历史中,而另外Sequencer上传时都可以选择要顺便从DelayedInbox拿多少个L1操作一起写入交易历史中,让这些L1操作生效。
SequencerInbox里是交易历史,一般只有Sequencer可以直接写入新交易;DelayedInbox里等待被收入的交易
Sequencer写入新交易时可以顺便从DelayedInbox发起交易一起写入
复杂的设计以及凡善可陈的文件
如果读者直接参考Arbitrum官方关于Sequencer及ForceInclusion的章节,会看到里面提到的ForceInclusion如何兼容,以及一些参数名称和函式名称:用户先去Inbox约定呼叫sUnsignedTransaction函式,如果Sequencer未在约24小时内收入,那用户就可以去呼叫SequencerInbox签约的forceInclusion函式。就这样,连链接也没有附在里面,只能自己去约定方案码里相对应的函式。
当找到sUnsignedTransaction函式后,你发现其实要自己填nonce值还有maxFeePerGas值。是哪个地址的nonce?是哪个网路上的maxFeePerGas值?要怎么填比较好?没有文件记录,连Natpsec都没有。然后你还顺便发现一个堆积皮肤的函式:sL1FundedUnsignedTransaction、sUnsignedTransactionToFork、sContractTransaction、sL1FundedContractTransaction,一样没有文件告诉你这些函式的区别、如何使用、参数如何填写,连Natpsec都没有。
你综合考虑并试一试的心态来试填参数并送出交易,想用反复试验的方式看能不能找出正确的方法,但发现这些函式全都会把你的L1地址做地址别名,导致最终L2上的交易发起人根本上是不一样的地址,所以你的L2地址一动也不动。
发送L2消息后来偶然点开Google搜索页面中的某个链接才发现Arbitrum自己原来有一个教程程序库,里面有脚本示范怎么从L1送L2交易(或者ForceInclusion的意思),然后它的函式完全不是上面提到的任何一个,不过是一个叫的sL2Message函式,而且消息参数要带入的居然是签名的ArbitrumL2交易???谁会知道要「传送L2的消息」其实会是备注「签完名的L2交易”??而且再一次,没有任何文件及Natspec解释什么时候用及如何使用这个函式。
结论:要手动产生一个Arbitrum的强制收入交易比较麻烦,建议就照着官方教程跑ArbitrumSDK呗。Arbitrum不像其他Rollup清楚有的开发者文件及计划码附注,许多函式的用途和参数缺乏说明,导致开发者需要花费比预期更多的时间来接入和使用。我同时ArbitrumDiscord上询问Arbitrum的人,但并没有得到令人满意的答案。
注:在Discord上询问,对方也只是让我听sL2Message,不需要解释其他函式(甚至是强制包含文件里提到的sUnsignedTransaction)是什么用途、怎么用、什么时候用。
很遗憾,StarkNet目前还没有强制收录。只有两篇在官方论坛上讨论到审查及强制收录机制的文章。
无法证明交易失败
到底是因为StarkNet的零知识证明系统没办法证明记账失败的交易,所以不能允许强制记入。因为如果有人恶意(或无意)强制记入记失败、无法被证明的交易,那StarkNet就会直接卡住:因为交易被强制收入后,Prover就必须证明该笔失败交易,但它却没办法证明。
而StarkNet预期在版本引入证明失败交易的功能,之后应该就可以进一步实现强制包含机制。
zkSync的强制包含机制zkSync的L1-L2消息传递以及强制包含机制都穿透MailBox合约的requestL2Transaction函式进行,用户指定要呼叫的L2地址、通话数据、带上的ETH数量、L2GasLimit值等,分割requestL2Transaction这些参数组合成一个L2交易然后模拟优先伫列(PriorityQueue)中,Sequencer会在交易备份上传到L1时(commitBatches函式)指定要顺便从优先伫列中拿出多少笔交易一起收入。
zkSync在强制包含介面和乐观方面很像,都是以用户的L2地址去呼叫,并填入相关数据(被呼叫者、通话数据等),而不是像Arbitrum一样是填记签完名的L2交易;但在设计上交易和Arbitrum一样都在L1维护一个实体的队列,并由Sequencer从队列中取出交易写入交易历史中。
用户通过requestL2Transaction插入L2交易到优先伫列中,Sequencer在commitBatches时可以从优先伫列中顺便对话交易
如果你通过zkSync的官方桥去充值ETH,相当于此时交易,它就是去呼叫MailBox契约的requestL2Transaction函式,它会保留这个充值ETH的L2交易优先伫列中并发出一个NewPriorityRequest事件。因为契约把L2交易数据编码成一串字节字串所以不易读,改成看一下L1交易的参数的话,会看到参数中L2的接收方也是交易的发起人因为(是Deposit给自己),所以过一阵子兑换L2交易被序列器从优先伫列支出并收入进交易历史中时,它会在L2上被转换成记下自己转帐给自己的交易,而转帐的金额就是交易发起人在L1的存款ETH交易所带上的ETH金额。
L1充值交易中,交易发起者和接收者均为0xeDc1…6909,金额为0.03ETH,调用数据为空
L2上会出现记账0xeDc1…6909自己转帐给自己的交易,交易类型(TxnType)是255,玄系统交易
接下来我直接照本宣科呼叫requestL2Transaction函,发送了记下自己呼叫自己的交易:不带任何ETH,调用数据带入「强制包含」字串的十六进制编码。随后的式子被转换成L2上记下自己呼叫自己的交易,调用数据里是「强制包含」字串:0x666f72636520696e636c7573696f6e。
当排序器把交易从优先队列拿出来并写进交易历史中时,在L2上就可以转换成相对应的L2交易
通过requestL2Transaction函式,用户可以以L2地址在L1请求L2交易,指定L2接收方、带上的ETH金额以及通话数据。如果用户要调用其他合约、带不同数据,那同样就是将参数一一填入requestL2Transaction函式,只要记得是用L2地址来去L1上执行,这样到时候L2交易发起者就会是该L2地址。
还没有让用户强制收入的功能
虽然L2交易放在优先伫列中会顺便计算出L2交易要被Sequencer收入的期限,但目前zkSync设计中并没有让用户能够自己执行的强制包含相关函式,相当于做半套。那么虽然有「收入有效期限」,但实际上还是「看Sequencer要不要收入」:Sequencer可以等到过渡后才收入,也可以永远不再收入优先伫列中任何交易。未来zkSync应该要加入相关函式上,让用户可以在收入已经过了但还没有被Sequencer收入时,能够强制收入,这样才是真正有效的强制包容机制。
总结L1靠为数群的人来保证网路的「安全性」及「抗审查能力」,而Rollup靠「L1的抗审查能力」来获得「和L1同样的安全性」,但不包含Rollup的抗审查能力
相反地,Rollup因为都是由少数或什至单一的Sequencer来写入交易,抗审查能力反而更弱。因此Rollup需要有ForceInclusion机制来让用户绕过Sequencer,将交易写入历史中,避免被Sequencer审查而无法使用也无法离开Rollup
强制包含让用户可以强制将交易写入历史中,但在设计上需在「交易是否能立即插入历史、立即生效」上做选择。如果允许交易立即生效的话,会造成排序器的麻烦和使用体验上的困难,因为L2上等待被收入的交易都可能会被L1强制收入的交易所影响
因此目前Rollup的强制包含机制都会先让L1上插入的交易先进到一个等待状态中,并让Sequencer有一段时间窗口来反应、来选择要不要收入这些等待中的交易
zkSync和Arbitrum都是在L1维护一个实体的队列,用于管理用户从L1送出的L2交易或给L2的消息。Arbitrum称为延迟收件箱;zkSync称为优先队列
但zkSync发送L2交易的方式和Optimism比较像,都是以L2地址去L1上发送,所以L2交易的发起人会是该L2地址。Optimism发送L2交易的函式称为depositTransaction;zkSync称为requestL2Transaction。而Arbitrum制作填写完整的L2交易并签名,然后透sL2Message函的方式送出,Arbitrum在L2上会透签名还原签章者来作为L2交易的发起人
StarkNet目前还没有强制包含机制;zkSync撕像做了半套的强制包含—有优先队列且每个队列里的L2交易都有收入有效期限,但这个有效期限目前只是装饰用,实际上Sequencer可以选择完全不再收入任何优先队列里的L2交易