篇文章将会介绍Rollup的交易抗审查机制—ForceInclusion,并以几个著名Rollup的设计与实现为例
May the Force Inclusion be with you. 这篇文章将会介绍 Rollup 的交易抗审查机制 — Force Inclusion,并以几个著名 Rollup 的设计与实现为例。
Photo by Artur Tumasjan on Unsplash
预备知识:
交易抗审查(Censorship Resistance)的能力对一条区块链来说非常重要,如果一条区块链能够任意审查使用者交易,那这条区块链就和一个 Web2 服务器没有两样。 以太坊目前的交易抗审查能力来自于它为数众多的验证者们,如果Alice 想要审查Bob 的交易、不让他的交易上链,那Alice 要不得尝试买通网络中每一个验证者,要不得Spam整个网络、不断送出手续费比Bob 交易高的垃圾交易来塞满区块。不管是哪一个方式,她的成本都会非常高。
注:在以太坊目前的 PBS 架构中,审查的成本其实降低不少,可以参考配合 OFAC 审查 Tornado Cash 交易的区块比例。当前的抗审查能力仰赖在 OFAC 及政府管辖范围之外的独立验证者及 Relay。
但 Rollup 呢? Rollup 不需要一大堆的验证者来确保它的安全性,即便 Rollup 只有一个中心化的角色(Sequencer,称为排序者或排序器)来产出区块,它也和 L1 一样安全。但安全和抗审查能力是两回事,即便一个 Rollup 和以太坊一样安全,却只有一个中心化 Sequencer,那该 Sequencer 想要审查任何使用者的交易都行。
Sequencer 可以拒绝打包使用者交易,导致使用者无法使用也无法离开该 Rollup
与其要求Rollup 要有和L1 一样多的验证者来确保抗审查能力,还不如直接利用L1 的抗审查能力:Sequencer 也是要将交易资料打包送到L1 的Rollup 合约上,不如在Rollup 合约里加入一个机制让使用者也可以插入交易到排序之中,这样的机制就称为Force Inclusion。只要 Sequencer 没办法审查使用者的「L1 交易」,它就没办法阻止使用者通过 L1 强制插入 Rollup 交易,而 Rollup 的运作及安全性正是奠基于 L1 的抗审查能力。
Sequencer 无法审查使用者的 L1 交易,除非付出很高的成本
注:Force Inclusion 机制是要该 Rollup 有设计才会有,使用者并不是一定可以通过 L1 强制插入 Rollup 交易。如果 Rollup 没有提供 Force Inclusion 机制,那使用者就只能祈祷 Sequencer 不会审查自己交易。
如果我们允许通过Force Inclusion 插入的交易可以直接写入到Rollup 的交易历史中(也就是立即生效),那 Rollup 的状态就会马上被改变,例如 Bob 通过Force Inclusion 机制插入一笔「他转1000 DAI 给Carol」的交易,如果这笔交易立即生效,那最新的状态中Bob 的余额就会少1000 DAI 而Carol 会多1000 DAI(当然前提是Bob 余额超过1000 DAI)。
如果 Bob 交易能直接被写进交易历史中,马上生效,那 Rollup 的状态就会马上改变
如果此时 Sequencer 也在链下搜集交易,等着把下一批交易送到 Rollup 合约上,那就有可能被 Bob 强制插入并立即生效的交易给影响到。例如Bob 其实事先送了一笔「他转1000 DAI 给Alice」的交易到Sequencer 那,Sequencer 当下验证没问题并承诺会收入,Alice 和Bob 都可以向Sequencer 查询交易是否会被收入并且计算出最新状态( Alice 多1000 DAI,Bob 少1000 DAI)。但等到Sequencer 发现Bob 抢先一步去强制插入交易后,它手上的交易的执行状态都已改变,原本那笔「Bob 转1000 DAI 给Alice」的交易会变成执行失败,Alice 也拿不到1000 DAI。
Sequencer 收入 Bob 的交易,Alice 因此相信自己会收到 1000 DAI
结果 Bob 直接在 L1 上强制收入另一笔冲突的交易,造成 Sequencer 手上的交易不能被收入,Alice 也拿不到 DAI
这可不是一个好的使用者体验,因此 Rollup 一般都不会让 Force Inclusion 交易立即生效,而是让交易先进到一个「准备中」的状态。 Sequencer 可以在打包交易送上来时选择是否要顺便塞入这些「准备中」状态的交易到批次的最后面,如果 Sequencer 一直都没有想要处理这些「准备中」的状态,那这些交易在过了一段时间后就可以被强制插入到交易历史中。
Bob 想通过强制收入交易来骗 Alice,但实际上交易会先进到等待队列
Sequencer 可以自己决定在什么时候「顺便收入」等待队列中交易,所以 Bob 给 Alice DAI 的交易会先执行
如果 Sequencer 一直没有收入等待队列中的交易,在一段时间后使用者(或是任何人)就可以自己去强制收入。
Sequencer 拒绝收入 Bob 交易,所以 Bob 从 L1 将交易放进等待队列中
Sequencer 还是可以持续拒绝收入等待队列中的交易一段时间
但 Sequencer 无法永远拒绝收入等待队列中的交易,一段时间后任何人都可以触发强制收入
接下来将依序介绍 Optimism、Arbitrum、StarkNet 及 zkSync 等四个较有名的 Rollup 的 Force Inclusion 机制实现。
首先先介绍 Optimism 的 Deposit 流程,这个 Deposit 不单是指把存钱进 Optimism 的意思,而像是更一般的「把给 L2 的信息」存进去 L2。 L2 节点在接收到新存入的信息后就会将信息转换成一笔 L2 交易去执行,送信息到指定的接收方。
使用者从 L1 Deposit 给 L2 的信息
当一个使用者要把ETH 或ERC-20 代币存进Optimism 时,他会(通过前端网页)和L1 上的L1StandardBridge 合约互动,指定[要存多少数量以及由哪个地址接收](https:// github.com/ethereum-optimism/optimism/blob/a3cc8f275d1d4a20bcdddfe7b421b3b34bbf0785/packages/contracts-bedrock/src/L1/L1StandardBridge.sol#L105-L175)等等。接着L1StandardBridge 会将信息传递至下一层的L1CrossDomainMessenger 合约,这个合约主要是作为一般通用的L1 与L2 之间互相通信的合约,L1StandardBridge 便是使用这个通用的通信合约来和L2 上的L2StandardBridge 沟通,决定谁可以从L2 铸造代币或是谁可以从L1 提领代币。如果开发者需要开发一个 L1 与 L2 之间互通、同步状态的合约,那他就可以搭建在 L1CrossDomainMessenger 合约之上。
使用者的信息通过 CrossChainMessenger 合约从 L1 传递到 L2
L1CrossDomainMessenger 合约会再将信息送至最底层的OptimismPortal 合约 ,OptimismPortal 合约处理完后会[触发 一个TransactionDeposited
事件](https://github.com/ethereum-optimism/optimism/blob/a3cc8f275d1d4a20bcdddfe7b421b3b34bbf0785/packages/contracts-bedrock/src/L1/OptimismPortal.sol#L413C14- L413C34),事件 参数包含「发送信息的人」、「接收信息的人」,以及相关的执行参数。
接着L2 的Optimism 节点会监听OptimismPortal 合约所触发 的TransactionDeposited
事件,并把事件里的参数转换为一笔L2 的交易,这个L2 交易的发起者就会是TransactionDeposited
事件 里的「发送信息的人」、交易的接收者就会是事件 里的「接收信息的人」,其他执行参数也是由事件 参数而来。
L2 节点会将 OptimismPortal 触发 的 TransactionDeposited 事件 转换成一笔 L2 交易
例如这一笔是某个使用者通过L1StandardBridge 合约Deposit 0.01 ETH 的交易,这个信息及ETH 一路传到OptimismPortal 合约(地址是0xbEb5…06Ed
) ,然后几分钟后转换成L2 交易:信息发起者是L1CrossDomainMessenger 合约;接收者是L2 上的L2CrossDomainMessenger 合约;附带0.01 ETH。
当使用者想要强制收入他的交易进 Optimism 执行时,他并不是想要从 L1 送信息至 L2,所以他不会去使用 L1CrossDomainMessenger 合约。他要达到的效果是让一笔原本「他从他的L2 地址在L2 上送出并要执行的交易」能顺利执行,所以他会直接去和OptimismPortal 合约互动,而且是以他L2 地址去调用 OptimismPortal 合约,如此到时候TransactionDeposited
事件 转换成的L2 交易的「交易发起者才会是他的L2 地址」,才能重现原本那笔交易。
从 TransactionDeposited 事件 转换而成的 L2 交易中,发起人会是 Bob 自己;接收人是 Uniswap 合约;而且会附带指定的 ETH,就像 Bob 自己发起 L2 交易一样
用L2 地址调用OptimismPortal 合约的depositTransaction 函数 ,并将原本L2 交易的参数一一填入
我做了一个简单的 Force Inclusion 交易:以我 L2 的地址(0xeDc1…6909
)转钱给我自己,并附带一个 “force inclusion” 的文字信息。这是我通过OptimismPortal 合约执行depositTransaction
函数的L1 交易,可以看到在触发 的TransactionDeposited
事件 中,from
和to
都是我自己,剩下的opaqueData
栏位里的值(000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015BE000000000000C35000666F72636520696E636C7573696F6E
)则是[编码了「调用depositTransaction 的人附带了多少ETH」、「L2 交易发起者要附带多少ETH 给L2接收者」、「L2 交易Gas Limit」及「给L2 接收者的Data」等等数据](https://github.com/ethereum-optimism/optimism/blob/111f3f3a3a2881899662e53e0f1b2f845b188a38/packages/contracts-bedrock/src/ L1/OptimismPortal.sol#L414)。
这笔 L1 交易 触发 的
TransactionDeposited
事件
将 opaqueData
值的这几个数据解码后分别会得到:
「调用 depositTransaction 的人附带了多少 ETH」:0
,因为我没有要从 L1 Deposit ETH 进到 L2
「L2 交易发起者要附带多少 ETH 给 L2 接收者」:5566
(wei),因为我要转钱给我自己
「L2 交易 Gas Limit」:50000
「给 L2 接收者的 Data」:0x666f72636520696e636c7573696f6e
,也就是 “force inclusion” 这个字串的 hex 编码
接着没多久就出现转换后的L2 交易:一笔我转钱给自己的L2 交易,金额是5566
,Data 是“force inclusion”字串。而且可以注意到在图中倒数第二行的Other Attributes
中Txn Type
(交易类型)是系统交易126(System)
,表示不是我自己在L2 发起的交易,是由L1 TransactionDeposited
事件转换而来。
转换而成的 L2 交易
如果使用者要调用其他合约、带不同Data,那一样就是将参数一一填入depositTransaction
函数,只要记得是要用L2 地址来去L1 上执行,如此到时候 L2 交易发起者才会是该L2 地址。
前面提到的 Optimism L2 节点将 TransactionDeposited
事件 转换成 L2 交易,其实这个 Optimism 节点指的是 Sequencer 节点,毕竟这攸关交易排序,所以只有 Sequencer 可以决定何时要转换成 L2 交易。在监听到TransactionDeposited
事件 时,Sequencer 并不一定会马上将事件 转换成L2 交易,而是可以有一段时间决定要何时转换,这段时间称为Sequencer Window,目前Optimism 主网上的Sequencer Window 为24 小时,也就是当使用者从L1 Deposit 一笔钱或一个信息,或是Force Include 一笔交易时,最糟情况会是24 小时后才被正式收入进L2 交易历史中,不过至少这胜过交易永远没办法被收入的结果。
在Optimism 中L1 的Deposit 操作会触发 一个TransactionDeposited
事件,剩下的就是等待Sequencer 收入这个Deposit 操作;但在Arbitrum 中L1 的操作(存钱或传信息给L2 等等)会被存在L1 合约的一个Queue 里,而不是单纯只触发事件。而 Sequencer 会被给予一段时间来将这个 Queue 里的 L1 操作放进交易历史中,如果时间到了 Sequencer 都没有作为,那任何人都可以去替 Sequencer 完成。
Arbitrum 会在 L1 合约维护一个 Queue,如果 Sequencer 没有主动收入 Queue 里的交易,时间到了任何人都可以强制收入 Queue 里的交易进交易历史中
L1 操作都要经由称为 Delayed Inbox 的合约,顾名思义这里的操作都会延迟生效;另一个合约则是 Sequencer Inbox,是给 Sequencer 上传 L2 交易的接口。 Sequencer 上传的交易会直接写进交易历史中,而每次 Sequencer 上传时都可以选择要顺便从 Delayed Inbox 拿多少个 L1 操作一起写进交易历史中,让这些 L1 操作生效。
SequencerInbox 里是交易历史,一般只有 Sequencer 可以直接写入新交易;DelayedInbox 里则是等待被收入的交易
Sequencer 写入新交易时可以顺便从 DelayedInbox 拿出交易一起写入
如果读者直接参考Arbitrum 官方关于Sequencer 及Force Inclusion 的章节,会看到里面提到了Force Inclusion 大致如何运作,以及一些参数名称和函数名称:使用者先去Inbox 合约调用sendUnsignedTransaction
函数,如果Sequencer 没在约24 小时内收入,那使用者就可以去调用Sequencer Inbox 合约的forceInclusion
函数。就这样,连链接也没有附在里面,只能自己去看合约源代码里相对应的函数。
当找到sendUnsignedTransaction
函数后,你发现竟然要自己填nonce
值还有maxFeePerGas
值。是哪个地址的 nonce
?是哪个网络上的 maxFeePerGas
值?要怎么填比较好?没有文件纪录,连 Natpsec 都没有。然后你还顺便发现一堆看似相似的函数:sendL1FundedUnsignedTransaction
、 sendUnsignedTransactionToFork
、[sendContractTransaction
](https://github.com/OffchainLabs/ nitro-contracts/blob/399790bb6660486fadf958017efc4eec99be3dd2/src/bridge/AbsInbox.sol#L191C14-L191C37)、[sendL1FundedContractTransaction
](https://github.com/OffchainLabs/nitro-contracts/blob/399790bb6660486fadf958017efc4eec99be3dd2/src/bridge/Inbox .sol#L84C14-L84C45),一样没有文件告诉你这些函数的区别、该怎么用、参数该怎么填,连Natpsec 都没有。
WTF? src
你抱着姑且一试的心态来试填参数并送出交易,想用trial and error 的方式看能不能找出正确的用法,但发现这些函数全都会把你的L1 地址做Address Aliasing,导致最终L2 上的交易发起人根本是不一样的地址,于是你的L2 地址一动也不动。
后来偶然点开Google 搜寻页面中的某个链接才发现原来Arbitrum 自己有一个Tutorial 代码库,里面有脚本示范怎么[从L1 送L2 交易](https://github.com/OffchainLabs/arbitrum-tutorials/ blob/fa46b16aa537992e5896f7bae9ec9c411745c707/packages/delayedInbox-l2msg/scripts/normalTx.js#L82)(也就是Force Inclusion 的意思),然后它戳的函数完全不是上面提到的任何一个,而是一个叫sendL2Message
的[函数](https://github.com/OffchainLabs/ nitro-contracts/blob/399790bb6660486fadf958017efc4eec99be3dd2/src/bridge/AbsInbox.sol#L150),而且message 参数要带入的竟然是[签完名的Arbitrum L2 交易](https://github.com/OffchainLabs/arbitrum- tutorials/blob/fa46b16aa537992e5896f7bae9ec9c411745c707/packages/delayedInbox-l2msg/scripts/normalTx.js#L78-L82)? ? ?谁会知道要「送给 L2 的信息」竟然会是一笔「签完名的 L2 交易」? ?而且再一次,没有任何文件及 Natspec 解释什么时候用及如何使用这个函数。
结论:要手动产生一个 Arbitrum 的强制收入交易比较麻烦,建议就照着官方 Tutorial 跑 Arbitrum SDK 呗。 Arbitrum 不像其他 Rollup 有清楚的开发者文件及源代码注释,许多函数的用途和参数缺乏说明,导致开发者得花费比预期多更多的时间来接入和使用。我也在 Arbitrum Discord 上询问 Arbitrum 的人,但并没有得到令人满意的答案。
注:在Discord 上询问,对方也只会叫我去看sendL2Message
,没有想要[解释其他函数](https: //discord.com/channels/585084330037084172/859511259183448084/1232139827915522148)(甚至是Force Inclusion 文件里提到的sendUnsignedTransaction
)是什么用途、怎么用、什么时候用。
JUST DO IT. src
很遗憾地,StarkNet 目前还没有 Force Inclusion 机制。只有两篇在官方论坛上讨论到Censorship 及[Force Inclusion](https://community.starknet.io/t/starknet -escape-hatch-research/1108) 的文章。
原本是因为 StarkNet 的零知识证明系统没办法证明一笔失败的交易,所以不能允许 Force Inclusion。因为如果有人恶意(或无意) Force Include 一笔失败、无法被证明的交易,那 StarkNet 就会直接卡住:因为交易被强制收入后,Prover 就必须证明该笔失败交易,但它却没办法证明。
而StarkNet 预期在v0.15.0 版引入证明失败交易的功能,之后应该就可以进一步实现Force Inclusion 机制。
zkSync 的L1->L2 信息传送以及Force Inclusion 机制都是通过MailBox 合约的requestL2Transaction
函数进行,使用者指定要调用的L2 地址、call data、带上的ETH 数量、L2 Gas Limit 值等,requestL2Transaction
会将这些参数组合成一个L2 交易然后[放进优先队列(Priority Queue) 中](https://github.com/matter-labs/era-contracts/blob/4aa7006153ad571643342dff22c16eaf4a70fdc1/l1-contracts/contracts /zksync/facets/Mailbox.sol#L328-L334),Sequencer 会在交易打包上传到L1 时([commitBatches
函数](https://github.com/matter-labs/era-contracts/blob/ 4aa7006153ad571643342dff22c16eaf4a70fdc1/l1-contracts/contracts/zksync/facets/Executor.sol#L193-L196))指定[要顺便从优先队列中拿出多少笔交易一起收入](https://github.com/matter-labs /era-contracts/blob/4aa7006153ad571643342dff22c16eaf4a70fdc1/l1-contracts/contracts/zksync/interfaces/IExecutor.sol#L82)。
zkSync 在Force Inclusion 介面上和Optimism 很像,都是以使用者的L2 地址去调用,并填入相关资料(被调用者、calldata 等等),而不是像Arbitrum 一样是填一笔签完名的L2 交易;但在设计上则是和Arbitrum 一样都是在L1 维护一个实体的Queue,并由Sequencer 从Queue 中拿出交易写入交易历史中。
使用者通过 requestL2Transaction 插入 L2 交易到优先队列中,Sequencer 在 commitBatches 时可以从优先队列中顺便拿出交易
如果你通过zkSync 的官方桥去Deposit ETH,像是这笔交易,它便是去调用MailBox 合约的requestL2Transaction
函数,它会将这个Deposit ETH 的L2 交易放进优先队列中并触发 一个NewPriorityRequest
事件。因为合约把L2 交易资料编码成一串bytes 字串所以不易读,改成看这笔L1 交易的参数的话,会看到参数中L2 的接收方也是交易的发起人(因为是Deposit 给自己),所以过一阵子这笔L2 交易被Sequeuncer 从优先队列拿出并收入进交易历史中时,它会在L2 上被转换成一笔[自己转帐给自己的交易](https://era.zksync.network /tx/0x3629ad52886217ae73201fd3be7c3ea4101148c307913548ad538beadbf3dd7a),而转帐的金额就是交易发起人在L1 的Deposit ETH 交易所带上的ETH 金额。
L1 Deposit 交易中,交易发起者和接收者都是 0xeDc1…6909,金额是 0.03 ETH,call data 为空
L2 上会出现一笔 0xeDc1…6909 自己转帐给自己的交易,交易类型(Txn Type)是 255,也就是系统交易
接着我直接照本宣科调用requestL2Transaction
函数,发送了一笔自己调用自己的交易:没有带任何ETH,call data 带入「force inclusion」字串的HEX 编码。 接着它被转换成 L2 上一笔自己调用自己的交易,call data 里是「force inclusion」字串:0x666f72636520696e636c7573696f6e
。
当 Sequencer 把交易从 Priority Queue 拿出来并写进交易历史中,在 L2 上就会转换成相对应的 L2 交易
通过 requestL2Transaction
函数,使用者可以以 L2 地址在 L1 请求 L2 交易,指定 L2 接收方、带上的 ETH 金额以及 call data。如果使用者要调用其他合约、带不同Data,那一样就是将参数一一填入requestL2Transaction
函数,只要记得是要用L2 地址来去L1 上执行,如此到时候L2 交易发起者才会是该L2 地址。
虽然L2 交易放到优先队列中会顺便计算出[这笔L2 交易要被Sequencer 收入的期限](https://github.com/matter-labs/era-contracts/blob/4aa7006153ad571643342dff22c16eaf4a70fdc1/l1-contracts/ contracts/zksync/libraries/PriorityQueue.sol#L12),但目前zkSync 设计中并没有让使用者能自己执行的Force Inclusion 相关函数,等于是做半套。也就是虽然有「收入有效期限」,但实际上还是「看 Sequencer 要不要收入」:Sequencer 可以等到过期后才收入,也可以永远不再收入优先队列中任何交易。未来 zkSync 应该要加入相关函数,让使用者可以在收入有效期过了但都还没被 Sequeuncer 收入时,能强制收入,如此才是真正有效的 Force Inclusion 机制。
[2024/07/07 更新] zkSync 的 Force Inclusion 机制目前没办法让使用者转出 L2 地址的 ETH!
可以注意到requestL2Transaction
函数,它的参数_l2Value
指的是要从L1 储值到L2 的ETH,如果要从L1 储值到L2 地址就是填这个_l2Value
,并且msg.value
要带上足够的ETH。但如果使用者是要从 L2 地址转出 ETH 到其他地址呢?要填哪个参数?
答案是没办法!像是Optimism 的depositTransaction
函数的_value
参数指的就是L2 地址要转出的ETH 数量,而msg.value
则是要从L1 储值到L2 的ETH 数量,因此使用者可以通过这两个方式决定要储值多少或提走多少ETH。但 zkSync 的 requestL2Transaction
函数则只有一个参数:储值多少 ETH,因此使用者没办法通过 Force Inclusion 机制从 L2 地址提走 ETH!
目前 zkSync 已经在着手修复这个问题。
L1 靠为数众多的验证者们来确保网络的「安全性」及「抗审查能力」,而Rollup 则是靠「L1 的抗审查能力」来获得「和L1 一样的安全性」,但不包含Rollup 的抗审查能力
相反地,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 称为 Delayed Inbox;zkSync 称为 Priority Queue
但 zkSync 送出 L2 交易的方式和 Optimism 比较像,都是以 L2 地址去 L1 上发送,如此 L2 交易的发起人才会是该 L2 地址。 Optimism 送 L2 交易的函数称为 depositTransaction
;zkSync 称为 requestL2Transaction
。而 Arbitrum 则是制作一笔完整的 L2 交易并签名,然后通过 sendL2Message
函数送出,Arbitrum 在 L2 上会通过签名还原签章者来作为 L2 交易的发起人
StarkNet 目前还没有Force Inclusion 机制;zkSync 则是像做了半套的Force Inclusion — 有Priority Queue 且每个Queue 里的L2 交易都有收入有效期限,但这个有效期限目前只是装饰用,实际上Sequencer可以选择完全不再收入任何Priority Queue 里的L2 交易
https://docs.optimism.io/stack/protocol/outages#bypassing-the-sequencer
https://docs.optimism.io/stack/protocol/deposit-flow#l2-processing
https://specs.optimism.io/protocol/deposits.html#user-deposited-transactions
https://docs.arbitrum.io/arbos/l1-to-l2-messaging#address-aliasing
https://github.com/OffchainLabs/nitro-contracts/blob/v1.2.1/src/bridge/SequencerInbox.sol#L284
https://l2beat.com/scaling/projects/zksync-era?ref=bankless.ghost.io#force-transactions
https://docs.zksync.io/zk-stack/concepts/transaction-lifecycle.html#overview
https://docs.zksync.io/build/developer-reference/l1-l2-interop.html
https://github.com/zkSync-Community-Hub/zksync-developers/discussions/523#discussioncomment-9886147
Special thanks to Chih-Cheng Liang and Kimi Wu for reviewing and improving this post
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!