理论与实践:以太坊 Rollup 的抗审查交易如何触发?
极客Web3
2024-06-03 17:21
订阅此专栏
收藏此文章
Linea 主动停机一事敲响了警钟:人们必须重视 Layer2 的抗审查与可用性问题。


原文标题:《Rollup 的 Force Inclusion 機制介紹》

撰文:NIC Lin,Taipei Ethereum Meetup 负责人


就在昨天发生了一起震惊无数人的事情:由 Metamask 母公司 Consensys 推出的以太坊二层 Linea 主动停机了,官方称这么做的目的是为了降低 Velocore 黑客攻击事件的影响。而这不由得让人想起之前 BSC 链(BNB Chain)为了降低黑客攻击的损失,在官方主动协调下停机一事。每当人们谈论起这种事情,都会对 Web3 倡导的去中心化价值感到怀疑。



当然,上述事件发生的核心原因,更多在于基础设施本身的不完善,即不够去中心化:如果一条链足够去中心化,那么就不该说停就停。由于以太坊二层的独特构造,大多数 Layer2 都依赖于中心化的 Sequencer,虽然近些年去中心化排序器的论调越来越多,但考虑到二层的存在目的及其结构,我们大可以认为,Layer2 的排序器大概率不会有多去中心化,最后可能还比不上 BSC 链的去中心化程度。如果事实真的如此,那么我们该怎么办?


其实对于二层而言,排序器不去中心化带来的最直接危害,在于抗审查性和活性。如果处理交易的实体(Sequencer)很少,那么它在是否为你服务这件事上就掌握了绝对权力:想拒绝你就拒绝你,而你可能没有办法。如何解决 Layer2 的抗审查问题,显然是一个重要的话题。


在过去的数年中,各大以太坊二层针对抗审查问题提出了各种各样的解决方案,比如 Loopring 和 Degate 以及 StarkEx 的强制提款与逃生舱功能、Arbitrum 及其他 OP Rollup 的 Force Inclusion 功能,这些方法都可以在一定条件下对 Sequencer 产生制衡,以防止其无端拒绝任意用户的交易请求。


在今天的文章中,来自台北以太坊协会的 NIC Lin 现身说法,亲自实验了 4 个主流 Rollup 的抗审查交易功能,从工作流程和操作方法等方面深入的分析了 Force Inclusion 的机制设计,这对于以太坊社区和手握巨额资产的大户而言尤其具有参考价值。


交易审查与 Force Inclusion


交易抗审查性(Censorship Resistance)对一条区块链来说非常重要,如果区块链能够任意审查并拒绝用户发起的交易,那就和一个 Web2 服务器没有两样。以太坊目前的交易抗审查能力来自于它为数众多的 Validator,如果有人想审查 Bob 的交易、不让他的交易上链,要么就尝试买通网络中大部分 Validator,要不就 Spam 整个网路,不断送出手续费比 Bob 更高的垃圾交易来抢占区块空间。不管是哪种方式,成本都会非常高。


注:在 Ethereum 目前的 PBS 架构中,审查交易的成本会降低不少,可以参考配合 OFAC 审查 Tornado Cash 交易的区块比例。当前的抗审查能力仰赖在 OFAC 及政府管辖范围之外的独立验证者及 Relay。


但 Rollup 呢?Rollup 不需要一大堆的 Validator 来确保安全性,即便 Rollup 只有一个中心化的角色(Sequencer)来产出区块,它也和 L1 一样安全。但安全和抗审查能力是两回事,即便一个 Rollup 和以太坊一样安全,但在只有一个中心化 Sequencer 的情况下,想审查任何用户的交易都行。


Sequencer 可以拒绝处理用户的交易,导致用户资金被扣留无法离开该 Rollup


Force Inclusion 机制


与其要求 Rollup 有大量的去中心化的 Sequencer,还不如直接利用 L1 的抗审查能力:


本来 Sequencer 就是要将交易数据打包送到 L1 的 Rollup 合约中,不如在合约里加入一个设计,让用户可以自行把交易插入到 Rollup 合约,这个机制就称为「Force Inclusion」。只要 Sequencer 没办法在 L1 层面审查用户,它就没法阻止用户在 L1 强制插入交易。这样一来,Rollup 就可以继承 L1 的抗审查能力。


Sequencer 无法审查使用者的 L1 交易,除非付出很高的成本


强制交易应该怎么生效?


如果允许通过 Force Inclusion 把交易直接写入到 Rollup 合约中(也就是立即生效),那 Rollup 的状态就会马上改变,例如 Bob 透过 Force Inclusion 机制插入一笔「转 1000 DAI 给 Carol」的交易,如果交易立即生效,那最新的状态中 Bob 的余额会少 1000 DAI,Carol 会多 1000 DAI。


如果 Force Inclusion 能直接把交易写进 Rollup 合约中并马上生效,那状态就会马上改变


如果此时 Sequencer 也在链下收集交易,并把下一批交易送到 Rollup 合约上,就有可能被 Bob 强制插入并立即生效的交易给影响到。这种问题要极力避免,因此Rollup 一般不会让 Force Inclusion 交易立即生效,而是先让用户把交易插入到 L1 上的等待队列中,进入「准备中」状态。


Sequencer 在把链下交易打包送上 Rollup 合约时,选择是否在交易序列里塞入前述交易,如果 Sequencer 一直无视这些处于「准备中」状态的交易,等窗口期结束后,用户可以把这些交易强制插入到 Rollup 合约中。


Sequencer 可以决定在什么时候「顺便收入」等待队列中的交易


Sequencer 还是可以拒绝处理等待队列中的交易


如果 Sequencer 长期拒绝,一段时间后任何人都可以通过 Force Inclusion 功能把交易强行插入到 Rollup 合约中


接下来我们将依序介绍 Optimism、Arbitrum、StarkNet 及 zkSync 等四个较有名的 Rollup 的 Force Inclusion 机制实现。


Optimism 的 Force Inclusion 机制


首先介绍 Optimism 的 Deposit 流程,这个 Deposit 不单是指把钱存进 Optimism,还包括「把用户向 L2 发送的信息」送进 L2。L2 节点在收到新存入的消息后,会将消息转换成一笔 L2 交易去执行,送到消息指定的接收方。


使用者从 L1 Deposit 给 L2 的消息


L1CrossDomainMessenger 合约


当一个用户要把 ETH 或 ERC-20 代币存进 Optimism 时,他会通过前端网页和 L1 上的L1StandardBridge合约互动,指定要存多少金额以及由哪个 L2 地址接收这些资产。


L1StandardBridge 合约会将消息传递至下一层的L1CrossDomainMessenger合约,这个合约主要作为 L1 与 L2 之间互相通讯的组件,L1StandardBridge 便通过这个通用的通讯组件和 L2 上的 L2StandardBridge 交流,决定谁可以在 L2 铸造代币,或是谁可以从 L1 解锁代币。


如果开发者需要开发一个在 L1 与 L2 之间互通、同步状态的合约,那他就可以搭建在 L1CrossDomainMessenger 合约之上。


使用者的消息透过 CrossDomainMessenger 合约从 L1 传递到 L2


注:本文的部分图片中将 CrossDomainMessager 写成了 CrossChainMessager


OptimismPortal 合约


L1CrossDomainMessenger 合约会再将消息送至最底层的OptimismPortal合约,OptimismPortal 合约处理完后会抛出一个名为TransactionDeposited的事件,参数包含「发消息的人」、「收消息的人」,以及相关的执行参数。


接著 L2 的Optimism 节点会监听 OptimismPortal 合约抛出的 Transaction Deposited 事件,并把 event 里的参数转换为一笔 L2 交易,这个交易的发起者会是 Transaction Deposited 事件参数里指明的「发消息的人」,交易接收者就是事件参数里「收消息的人」,其他交易参数也是由上述事件中的参数而来。


L2 节点会将 OptimismPortalemit 的 Transaction Deposited 事件参数转换成一笔 L2 交易


例如,这是某个用户透过 L1StandardBridge 合约存款 0.01ETH 的交易,这个消息及 ETH 一路传到 OptimismPortal 合约(地址是 0xbEb5…06Ed),然后几分钟后被转换成 L2 交易:


消息发起者是 L1CrossDomainMessenger 合约;接收者是 L2 上的 L2CrossDomainMessenger 合约;消息内容是 L1StandardBridge 收到了 BoB 的 0.01ETH 存款。这之后还会触发一些流程,比如为 L2StandardBridge 增发 0.01 枚 ETH,再由后者转给 Bob。


具体怎么触发


当你想把交易强制收纳进 Optimism 的 Rollup 合约中时,你要达到的效果是让一笔「从你的 L2 地址在 L2 上发起并要执行的交易」能顺利执行,这时你应该用自己的 L2 地址把消息直接提交给 OptimismPortal 合约(注意 OptimismPortal 合约其实在 L1 上,但 OP 的地址格式和 L1 地址格式一致,你直接用和 L2 账户相同地址的 L1 账户调用上述合约即可)。


之后该合约抛出的 Transaction Deposited 事件转化的 L2 交易的「发起者」,才会是你的 L2 账户,此时交易格式和正常的 L2 交易一致。


从 Transaction Deposited 事件转换而成的 L2 交易中,发起人会是 Bob 自己;接收人是 Uniswap 合约;而且会附带指定的 ETH,就像 Bob 自己发起 L2 交易一样


如果要调用 Optimism 的 Force Inclusion 功能,你要直接调用 OptimismPortal 合约的 depositTransaction 函数,将你想在 L2 执行的交易的参数填入


我做了一个简单的 Force Inclusion 实验,这条交易想达成这样一件事:在 L2 上用我的地址自转账(0xeDc1…6909),并附带一个「force inclusion」的文字讯息。


这是我透过 OptimismPortal 合约执行 depositTransaction 函数的 L1 交易,可以看到在其抛出的 Transaction Deposited 事件中,from 和 to 都是我自己



剩下的 opaque Data 一栏里的值则编码了「调用 deposit Transaction 函数的人附带了多少 ETH」、「L2 交易发起者要把多少 ETH 发给接收者」、「L2 交易 GasLimit」及「给 L2 接收者的 Data」等等信息。


将上述信息解码后分别会得到:


  • 「调用 deposit Transaction 的人附加了多少 ETH」:0,因为我并不是从 L1 存 ETH 到 L2;
  • 「L2 交易发起者要把多少 ETH 发给接收者」:5566(wei)
  • 「L2 交易的 GasLimit」:50000
  • 「给 L2 接收者的 Data」:0x666f72636520696e636c7573696f6e,也就是「force inclusion」这个字串的 16 进制编码


接着没多久就出现转换后的 L2 交易:一笔我转钱给自己的 L2 交易,金额是 5566 wei,Data 是「force inclusion」字串。而且可以注意到,在图中倒数第二行的 Other Attributes 中的 TxnType(交易类型),显示是系统交易 126(System),表示这笔交易不是我自己在 L2 发起的,是由 L1 交易的 Deposited 事件转换而来。


转换而成的 L2 交易


如果你要通过 Force Inclusion 调用 L2 合约、发送不同的 Data,那无非就是将参数一一填入前面的 deposit Transaction 函数,只是要记得,要用和自己 L2 账户相同的 L1 地址去调用 deposit Transaction 函数,这样当 Deposited Event 转化为 L2 交易时,发起者就是你的 L2 账户。


SequencerWindow


前面提到的 Optimism L2 节点将 Transaction Deposited 事件转换成 L2 交易,其实这个 Optimism 节点指的是 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 交易

【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。

极客Web3
数据请求中
查看更多

推荐专栏

数据请求中
在 App 打开