导语:
本文是 Arbitrum 前技术大使 及 智能合约自动化审计公司 Goplus Security 前联合创始人罗奔奔对 Arbitrum One 的技术解读。
在上一篇文章《Arbitrum 的组件结构解读(上)》,我们介绍了 Arbitrum 核心组件中的 排序器、Validator、Sequencer Inbox 合约、Rollup Block、非交互式欺诈证明的作用,而在今天的文章中,我们将重点讲解 Arbitrum 核心组件中与跨链消息传递及抗审查交易入口相关的组件。
正文:此前的文章中,我们曾提到,Sequencer Inbox 合约专门在 Layer1 上接收排序器发布的交易数据包 Batch。同时,我们指出,Sequencer Inbox 又被称作快箱,与之相对的是慢箱 Delayed Inbox(简称 Inbox)。下面,我们将对 Delayed Inbox 等与跨链消息传递相关的组件进行细致解读。
跨链与桥接的原理
跨链交易可分为 L1 到 L2(充值)与 L2 到 L1(提现)。注意这⾥所说的充值和提现未必与资产跨链相关,可以是不直接附带资产的消息传递。所以这两个词仅仅表示跨链相关行为的两个方向。
跨链交易与纯 L2 交易相比,跨链交易在 L1 和 L2 这两个不同的系统中进行了信息互换,因此过程更复杂。
另外,通常我们说的跨链行为,是在两个毫不相关的网络上,用见证人模式的跨链桥进行的跨链,这种跨链的安全性取决于跨链桥的运营者,历史上基于见证人模式的跨链桥被盗事件频繁发生。
而在 Rollup 与 ETH 主网之间的跨链行为,与上述跨链有本质不同,因为 Layer2 的状态是由记录在 Layer1 上的数据决定的,只要你使用的是 Rollup 官方的跨链桥,其在运作结构上是绝对安全的。
这也凸显出 Rollup 的本质,它只是在用户角度看,像⼀条独立的链,但实际上所谓的「Layer2」只是 Rollup 对用户敞开的快速展示窗口,它的真实链式结构还是刻录在 Layer1 上。所以,我们可以认为 L2 算半条链,或者说是「在 Layer1 上创造出的一条链」。
可重试票据 Retryables
需要注意,跨链都是异步和非原子性的,它不可能像在一条链上一样做完一笔交易确认后就知道结果,也不能保证另一侧一定会在某个时间点发生某些事。因此跨链有可能因为一些软性问题而失败,但只要使用正确的手段,诸如可重试票据(Retryable Ticket),就不会发生资金卡住等硬性问题。
可重试票据是通过 Arbitrum 官方桥充值时,用到的基本工具,ETH 和 ERC20 的充值都会使⽤到。其生命周期分为三步:
1. 在 L1 上提交票据。在 Delayed Inbox 合约中使用 createRetryableTicket() 方法创建充值票据,并提交。
2. L2 上自动兑付。大部分情况下,排序器可以自动帮用户兑付票据,无需后续的手动操作。
3. L2 上手动兑付。部分边缘情况,如 L2 上 gas 价格突然激增,票据上预付的 gas 不够,则无法自动兑付。此时需要用户手动操作。
注意,如果自动兑付失败,需要在 7 日内手动兑付票据,否则要么票据将会被删除(资金会永久损失),要么需要为票据的保存支付一定费用来续租。
另外,对于 Arbitrum 官方桥的提现流程,虽然和充值行为在流程上有一定对称相似性,但并没有 Retryables 这个概念,一方面可以从 Rollup 协议本身理解,另一方面我们可以从一些区别进行理解:
·提现的过程中不存在自动兑付,因为 EVM 没有定时器或自动化,而 L2 上可以实现自动兑付,是排序器帮忙实现的,所以 L1 上用户要手动与 Outbox 合约交互,以 Claim 取回资产。
·提现也不存在票据过期的问题,只要过了挑战期,可以在任意时间领取。
ERC-20 资产跨链 Gateway
ERC-20 资产的跨链是复杂的。我们可以思考几个问题:
·一个在 L1 上部署的代币,它在 L2 上要如何部署?
·它的 L2 对应合约需要预先手动部署,还是系统可以自动为跨过来的、但尚未部署合约的代币 自动部署资产合约?
·L1 上的 ERC-20 资产,在 L2 对应的合约地址是什么?是否该和 L1 一致?
·在 L2 上原生发行的代币,如何跨链至 L1?
·拥有特殊功能的代币,如可调整数量的 Rebase 型代币,自增长生息代币,如何跨链?
我们不打算全部回答这些问题,因为展开太过复杂。这些问题仅是用来说明 ERC20 跨链的复杂性。
目前非常多扩容方案使用的都是白名单+手动清单的方案,来规避各种复杂的问题和边界情况。
Arbitrum 使用了 Gateway 系统,解决了大部分 ERC20 跨链的痛点,具有以下特性:
·Gateway 组件在 L1 和 L2 成对出现。
·Gateway Router 负责维护 Token L1<->Token L2 之间的地址映射,以及 some token<->some gateway 之间的映射。
·Gateway 本身可分为 StandardERC20 gateway,Generic-custom gateway,Custom gateway 等等,用以解决不同类型的和功能 ERC20 的桥接问题。
我们以比较简单的 WETH 跨链为例,来说明自定义 gateway 的必要性。
WETH 是一种 ETH 的 ERC20 等价物。Ether 作为主币,很多 dApp 中的复杂功能是无法实现的,因此需要一个 ERC20 的等价物。向 WETH 合约内转入一些 ETH,它们会被锁在合约内,并生成出相同数量的 WETH。
同理,也可以销毁 WETH,取出 ETH。显然,流通的 WETH 和锁仓的 ETH 数量永远是 1:1 的。
如果现在把 WETH 直接跨链到 L2 上,我们会发现一些奇怪的问题:
·无法在 L2 上把 WETH 进行 Unwrap 变成 ETH,因为 L2 上并没有锁仓对应的 ETH。
·Wrap 功能可以使用,但这些新生成的 WETH 如果跨回到 L1,也无法在 L1 上解封装为 ETH,因为 L1 和 L2 上的 WETH 合约不是「对称的」。
显然这违反了 WETH 的设计原理。那么 WETH 在跨链时,不论是充值还是提现,都需要先 Unwrap 成 ETH 后,再跨到对面,然后 Wrap 成 WETH。这个也就是 WETH Gateway 的作用。
其他有更复杂逻辑的代币同理,需要更复杂和精心设计的 Gateway 才能正常在跨链环境下工作。Arbitrum 的自定义 Gateway 继承了普通 Gateway 的跨链通信逻辑,并允许开发者自定义与代币逻辑相关的跨链行为,可满足大部分需求。
慢收件箱 Delayed Inbox
与快箱也即 SequencerInbox 相对应的是慢箱 Inbox(全称 Delayed Inbox)。为什么要有快慢之分呢?因为快箱是专门接收排序器发布的 L2 交易 Batch 的,所有未经排序器在 L2 网络内预处理的交易,都不该出现在快箱合约中。
慢箱的第⼀点作用是,处理 L1 到 L2 的充值行为。用户通过慢箱进行充值,排序器监听到后再反映在 L2 上,最终这笔充值记录会被排序器包含进 L2 的交易序列中,并提交至快箱合约 Sequencer Inbox。
在这个例子中,用户直接向快箱提交充值交易是不合适的,因为提交到快箱 Sequencer Inbox 中的交易,会干扰到 Layer2 正常的交易排序,然后会影响到排序器的工作。
慢箱的第⼆个作用,是抗审查。用户直接提交至慢箱合约中的交易,排序器⼀般会在 10 分钟内归集到快箱中。但如果排序器恶意忽略你的请求,慢箱还有⼀个强制归集 force inclusion 功能:
如果交易被提交至 Delayed Inbox 中,经过 24 小时,慢箱中的交易仍未被排序器包含至交易序列中,用户可以在 Layer1 上手动触发 force inclusion 函数,把被排序器忽略掉的交易请求,强制归集到快箱 Sequencer Inbox 中,之后就会被全体 Arbitrum One 节点监听到,会被强制包含进 Layer2 交易序列里。
我们刚才提到过,快箱⾥的数据就是 L2 的历史数据实体。所以在被恶意审查的情况下,通过慢箱可以让交易指令最终包含进 L2 账本中,这涵盖了强制提款等逃离 Layer2 的场景。
由此可以看出,对任何⼀个方向和层次的交易,排序器最终都无法永久审查你。
慢箱 Inbox 的几个核心函数:
·depositETH(),最简单的充值 ETH 的函数。
·createRetryableTicket(),可用于 ETH 和 ERC20 以及消息的充值。相较 depositETH() 而言,有更高的灵活性,例如可指定充值后 L2 的收款地址等。
·forceInclusion(),也即强制归集功能,任何⼈都可以调⽤。该函数会校验,提交至慢箱合约中的某笔交易,是否过了 24 小时还没被处理。如果条件满足,则将对消息进行强制归集。
不过需要注意,force Inclusion 函数实际上位于快箱合约中,只是为了⽅便理解,我们将其放在慢箱这⾥⼀起讲解。
出站箱 Outbox
出站箱 Outbox 只与提现有关,可以理解为提现行为的记录和管理系统:
·我们知道,Arbitrum 官方桥的提现需要等待约 7 天的挑战期结束,Rollup Block 最终敲定后,提款行为才可以实施。⽤户在挑战期结束后,向 Layer1 上的 Outbox 合约提交相应的 Merkle Proof,它再与其他职能的合约通信(如解锁其他合约中锁定的资产),最终完成提现。
·OutBox 合约会记录哪些 L2 到 L1 的跨链消息已经被处理过,以防止有人反复提交执行过的提现请求。它通过mapping(uint256 => bytes32) public spent,记录提现请求的 spent Index 与信息对应关系,如果 mapping[spentIndex] != bytes32(0) 则该请求已被提现过。原理类似于防止重放攻击的交易计数器 Nonce。
下面我们将以 ETH 为例完整讲解充值与提现的流程。ERC20 与之不同的仅仅是走了 Gateway,就不再赘述。
ETH 充值
1. 用户调用慢箱的 depositETH() 函数。
2. 该函数会继续调用 bridge.enqueueDelayedMessage(),在 bridge 合约中记录该消息,并将 ETH 发送往 bridge 合约。所有的 ETH 充值资金,都保管在 bridge 合约中,相当于一个充值地址。
3. 排序器监听到慢箱中的充值消息,将充值操作反映⾄L2 数据库中,⽤户可以在 L2 网络看到自己充进来的资产。
4. 排序器将该笔充值记录包含进交易批次 batch,提交给 L1 上的快箱合约。
ETH 提现
1. 用户在 L2 上调用 ArbSys 合约的 withdrawEth() 函数,在 L2 上销毁相应数量的 ETH。
2. 排序器将该提现请求发送至快箱。
3. Validator 节点根据快箱中的交易序列,创建新的 Rollup Block,其中会包含上述提款交易。
4. Rollup Block 度过了挑战期并被确认后,⽤户可以在 L1 上调用 Outbox.execute Transaction() 函数,证明参数由前面提到的 ArbSys 合约给出。
5. Outbox 合约确认无误后,解锁 bridge 中相应数额的 ETH 发送给用户。
快速提现
使用乐观 Rollup 官方桥提现就会出现等待挑战期的问题。我们可以用私营的第三方跨链桥来规避这个问题:
·原子锁交换。这种方式只是在双方在各自链内进行了资产的互换,并且具有原子性,只要⼀方提供了 Preimage,双方⼀定可以得到应有的资产。但问题是流动性比较稀缺,需要点对点地寻找对手方。
·见证人跨链桥。⼀般类型的跨链桥都属于见证人桥。用户提交自己的提现请求,提现目的地指向第三方桥的运营者或流动性池。见证人发现跨链交易已提交到 L1 的快箱合约后,就可以直接在 L1 端向用户转账。这种方式本质上是用另⼀套共识系统来监视 Layer2,并根据其已提交至 Layer1 上的数据进行操作。问题是,这种模式下的安全系数不如 Rollup 官方桥高。
强制提现
force Inclusion() 强制归集功能用于对抗定序器的审查,任何 L2 本地交易、L1 到 L2 交易和 L2 到 L1 交易,都可以使用该功能实现。定序器的恶意审查严重影响了交易体验,大部分情况下我们会选择提现离开 L2,因此下面以强制提现为例介绍 forceInclusion 的用法。
回顾在 ETH 提现步骤中,只有步骤 1、2 是涉及到定序器审查的,所以只需要更改这两步:
·调用 L1 上慢箱合约中的 inbox.sendL2Message(),输入参数就是在 L2 上调用 withdrawEth() 时需要输入的参数。该消息会共享给 L1 上的 bridge 合约。
·等待 24 小时的强制归集等待期后,调用快箱中的 force Inclusion() 进行强制归集,快箱合约会检视 bridge 中是否有对应消息。
最终用户可以在 Outbox 中提现,其余步骤由同正常的提现相同。
另外,arbitrum-tutorials 中也有使用 Arb SDK 的详细教程去指导用户如何通过 forceInclusion() 去进行 L2 本地交易和 L2 到 L1 交易。