在 Arbitrum 推出 Nitro 之际,我们回顾一下 Arbitrum 的架构。Arbitrum 作为乐观证明的先行者,其架构部分有很多值得学习的地方。
Arbitrum 是一个 EVM 兼容的乐观证明 L2。虽然 EVM 兼容,但是在一些变量上会有所区别。例如我们在 L2 获取 block.number
时,我们得到的是 Arbitrum 上的区块高度,而不是以太坊上的区块高度。
下图是 Arbitrum 的框架图。基本分为 L2 和一系列部署在 L1 上的智能合约。
Arbitrum 部署在 L1 上的桥组件负责 L1 和 L2 的沟通。这个组件包含 4 个智能合约:Inbox,Outbox,Rollup 和 Dispute。在这里可以找到这些合约的代码。
在这篇文章中我们将关注 L1 和 L2 的通讯,也就是 Inbox
和 Outbox
两个合约。
L1 和 L2 的通信由桥组件基于消息传递实现。消息分为两种:L1 消息,L2 消息。L1 上的智能合约可以直接读取 L1 消息。L2 消息给 L2 来读取。我们使用包含 L2 消息编码的 L1 消息来将信息从 L1 传递到 L2。目前一共有六种信息:
ETH_TRANSFER
: 以太坊转账
L2_MSG
: L2 上的消息
L1MessageType_L2FundedByL1
: 包含 L2 信息编码的 L1 信息,由 L1 支付手续费
L1MessageType_submitRetryableTx
: L1 上的 Retryable Ticket
L2MessageType_unsignedEOATx
: L2 上未签名的用户交易
L2MessageType_unsignedContractTx
: L2 上未签名的合约交易L2
有两种方法可以将信息从 L1 发送到 L2。第一种是直接调用 Send 相关的函数。用这种方式发送信息延迟较低,并且使用起来比较简单。
第二种方式是创建 Retryable Ticket。接着用户在 L2 兑换这张 Retryable Ticket。这种方式方式的设计初衷是为了弥补第一种方式会因为 Gas 不够而失败。下图展示了三种具体发送信息的方法。
L1 发送到 L2 的消息会被放在 Inbox,例如将以太坊存入 L2。任何人都可以将消息放入 Inbox 中。但是当信息在 L2 被执行时,用户无法得知具体执行结果。
下图展示了信息如何流动的。 用户将消息发送给桥组件。在未来24小时,只有 Sequencer 可以执行这些消息。24 小时后,任何人都可以执行这些消息,并放到 Main Seqeuncer Inbox。这个机制有效防止 Sequencer 作恶和审查。
如果消息是直接由 Sequencer 提交的,Sequencer 可以立即将消息提交至 Sequencer Inbox。
可以在这找到 Arbitrum Inbox 的源代码。代理合约部署在了 0x4dbd4fc535ac27206064b68ffcf827b0a60bab3f。Inbox 合约部署在了0xc23e3f20340f8ef09c8861a724c29db43ba3eed4。
Inbox 拥有以下 Interfaces:
sendL2MessageFromOrigin()
sendL2Message()
sendL1FundedUnsignedTransaction()
sendUnsignedTransaction()
sendContractTransaction()
depositEth()
createRetryableTicket()
目前使用最多的函数是 depositETH()
。这个交易就调用了 depositETH()
。这笔交易发送了信息 L1MessageType_submitRetryableTx
,并且释放了两个事件:
MessageDelivered
InboxMessageDelivered
让我们放大这张图,仔细看看用户的信息是如何被提交到 Bridge 合约的。
消息相关的数据从 Inbox.sol
流到 Bridge.sol
。数据最后被存在了 Delayed Inbox Accumulator 的末尾。
在这个生命周期,所有事件中,有两个事件包含了消息状态的转变。一个标志消息被放到 Inbox,另一个标志信息被加入队列。