本文深入探讨了 ZKStack 的跨链架构,重点介绍了通过在 Gateway 上进行结算来降低结算成本,并保持安全保证。文章详细解释了 L2 到 L1 日志包含证明的构建过程,以及如何通过 Gateway 验证在 L1 上进行结算的 ZKChain 的日志包含。
FullRootHash
欢迎来到我们深入研究 ZKStack 跨链架构的第二部分。这篇分为两部分的文章探讨了即将推出的 ZKStack 协议升级的技术结构,该升级引入了一个 ZKChain 生态系统,该生态系统允许轻松创建新的 rollup ZKChain,内置的跨链功能,以及通过在另一个 ZKChain 而不是 L1 上结算来降低结算成本。
在第一部分中,我们描述了三个关键的 Merkle 树实现,这些实现构成了 ZKChain 生态系统中安全跨层通信的支柱:L2ToL1LogsTree
、ChainTree
和 SharedTree
。 我们探讨了这些树的结构、它们的叶子和根,以及它们如何共同实现分层结算架构,从而支持 ZKStack 的跨链能力。
第二部分通过说明这种树层次结构的最重要用例,将这种理论基础付诸实践:一个 ZKChain 如何构建其用于跨链代币转账的递归证明,该 ZKChain 在一个特殊的白名单 ZKChain 上结算,该 ZKChain 在 ZK Stack 中被称为 Gateway
(而不是直接在 L1 上结算)。 我们将演练从代币存款到提款完成的完整过程,展示 Gateway
如何充当中间结算层,同时保持最终追溯到 L1 的安全保证。
通过具体的日志包含证明示例,我们将展示第一部分中描述的树结构如何实现无缝的跨链通信,无论是在 L2 到 L1 层还是 L2 到 L2 层之间,同时保持向后兼容性并降低结算成本。
通常,任何允许其他 ZKChain 在其上结算的 ZKChain 都被称为结算层。 在撰写本文时,除了 L1 之外,Gateway
是唯一可用的结算层。
本系列描述了截至撰写本文时 ZKChain 生态系统的当前状态及其内部程序。 随着生态系统的发展,所描述的某些功能将来可能会更新。
当创建一个 L2 ZKChain 时,一组智能合约会部署在 L1 上,统称为 ZKChain 的 DiamondProxy
。 这组智能合约执行关于 ZKChain 状态转换的关键操作(最值得注意的是,批量提交和验证),以及存储关于此链的任何状态和配置。 默认情况下,L1 托管所有新创建的 ZKChain 的 DiamondProxy
,并且是任何新创建的 L2 ZKChain 的结算层。
在之后的任何时候,L2 ZKChain 都有可能将其结算层“迁移”到 Gateway
。 在这种情况下,ZKChain 的新 DiamondProxy
部署在 Gateway
上,它使用来自 L1 的状态进行初始化,并且 ZKChain 现在与 Gateway
就批量发布进行交互。 请注意,反向过程也是可能的,本质上是将 ZKChain 的结算层从 Gateway
迁移到 L1。
即使 Gateway
是某些 ZKChain 的结算层,L1 Ethereum 网络也是唯一保证所有 ZKChain 安全性的链。 本质上,Gateway
本身在 L1 上结算,并且它的每个批次还包含对所有在其之上结算的 ZKChain 批次的承诺。 因此,所有 ZKChain 最终都会在 L1 上结算。 区别在于,某些链在 L1 上发布其所有数据,而其他链通过 Gateway 的批次将数据的聚合承诺发布到 L1。
FullRootHash
ZKChain 的交易以批次形式发布到其结算层。 特别是,在结算层上结算一批意味着在结算层的 DiamondProxy
上提交、证明和执行这些批次,无论是 L1 还是 Gateway
。
为了提交一批,每个 ZKChain 都使用 fullRootHash
,它本身不是一个 merkle 根,而是两个单独的 merkle 根的哈希值:
L2ToL1LogsTree
的 localLogsRootHash
:表示批次中包含的已完成或已启动的跨链交易;ZKChain 的 SharedTree
的 aggregatedRoot
:它的值取决于 ZKChain 是否是结算层:
chainId
的空叶子初始化,在所有批次中保持相同的值。Gateway
:一个根哈希,它提交到所有在 Gateway
上结算的 ZKChain 的 chainRoots
。这种通用的结算方案不仅保留了三个 Merkle 树的结构,而且保持了相同的 Rollup-To-SettlementLayer 通信风格,而不管所涉及的层,即无论是 L2 到 L2 的承诺还是 L2 到 L1 的承诺,从而保持了向后兼容性。
我们在本部分的主要目的是揭示 L1 上的 L2 日志包含证明。 具体来说,考虑一个在 Gateway 上结算的 ZKChain 的 L2 到 L1 的日志,该日志包含在 L1 上的已结算批处理中,作为 ZKChain 的 L2ToL1LogsTree
中的一个叶子。 由于此日志已提交给 Gateway 的批处理,因此用户应该能够构建一个包含证明来证明其存在。 例如,日志可能是代币转账的日志,用户应该证明其存在才能在 L1 上解锁资金。
请记住,跨链代币转账遵循以下一般原则:
即使传输日志始终通过 fullRootHash
提交,包含证明的格式也取决于生成日志的 ZKChain 是否在 L2 或 L1 上结算。 你能明白为什么吗?
下面,我们将按照 L1 代币提款的用例来说明这两种情况下的包含证明结构。 为了完整起见,并为希望更深入研究代码库的读者提供一些额外的指针,我们首先描述提款之前的两个初步步骤:i) 将资金从 L1 桥接到 ZKChain,以及 ii) 在 ZKChain 上启动提款。
想象一个用户将 ERC20 代币从 L1 转移到 ZKChain。 这可以通过调用 Bridgehub
合约的 requestL2TransactionTwoBridges
函数来实现。 此函数直接从 ZKChain 的 DiamondProxy
面发送一个 rollup 优先级交易请求。
根据目标是结算在 L1 上的 ZKChain 还是 Gateway,将遵循不同的流程:
Gateway
,然后由 Gateway
负责将其转发到目标 ZKChain。它们有不同的路由,因为目前,ZKChain 仅接受来自其结算层的跨链交互。
假设用户在一段时间后想将其资金取回 L1。 用户通过调用 ZKChain 上 L2AssetRouter
合约的 withdraw
函数来启动提款,该函数将首先在 ZKChain 上销毁资金,然后简单地通过 L1Messenger
合约向 L1 发送一条消息。
请注意,此消息将成为一个 L2ToL1Log
叶子,该叶子构成了提交给 L1 或 Gateway
的批次的 fullRootHash
。
为了在 L1 上完成提款,用户需要使用以下信息调用 L1AssetRouter
合约上的 finalizeWithdrawal
函数:
chainId
:ZKChain 的 chainId,提款已在此处启动,l2BatchNumber
,l2TxNumberInBatch
:批号和批次内的交易号,用于验证提款,l2MessageIndex
:L2ToL1LogsTree
的叶子索引,稍后将与 merkleProof
结合使用,message
:bytes
消息本身,用于形成 L2ToL1LogsTree
中包含的相应日志,merkleProof
:包含证明,用于验证来自此 message
的 L2Log
是否位于来自具有 chainId
的 ZKChain 的批号为 l2BatchNumber
的 L2ToL1LogsTree
的索引 l2MessageIndex
处。merkleProof
的结构取决于 ZKChain 是在 L1 上还是在 Gateway 上结算。 让我们分别分析这两种情况。
L2 链在 L1 上结算。 因此,为了验证 L2 链代币提款,需要证明 L2 日志包含在 L1 上链的 DiamondProxy
上。
当 L2 ZKChain 在 L1 上结算一批时,它会将日志根,即 fullRootHash
,存储在链的相应 DiamondProxy
中。
回想一下,fullRootHash
组合了 L2ToL1LogsTree
和 SharedTree
的根,即 fullRootHash = hash(localLogsRootHash, aggregatedRoot)
。
因此,merkleProof
应该包含一个路径,该路径从索引 l2MessageIndex
处的 L2ToL1Log
一直哈希到 localLogsRootHash
(L2ToL1LogsTree
的根),以及一个额外的元素 aggregatedRoot
,以到达已提交的 fullRootHash
。 这使得 merkleProof
的路径长度等于 L2ToL1LogsTree
的高度 + 1。
现在让我们考虑一个在 Gateway
上结算的 L2 链,而 Gateway
又在 L1 上结算。 为了验证此链的代币提款,需要在 L1 上 ZKChain 的 DiamondProxy
上证明 L2 日志包含,但是,由于相应的 aggregatedRoot
已与 Gateway
的批次一起提交,因此需要通过 L1 上 Gateway 的 DiamondProxy
。
回想一下,在这种情况下,用户的提款日志(即 L2 日志)位于 L1 提交的 fullRootHash
的右子树中,如下所示:
L2ToL1LogsTree
的 L2ToL1Log
叶子。L2ToL1LogsTree
的根 LocalLogsRootHash
与一个空的 SharedTree
(即默认的 aggregatedRoot
)一起哈希,以计算特定批次的 ZKChain 的 fullRootHash
。 此根在 Gateway
上结算。fullRootHash
附加到 ZKChain 的 chainTree
,从而导致更新的 chainRoot
。SharedTree
中的新 chainRoot
叶子,从而导致更新的 aggregatedRoot
。aggregatedRoot
哈希其自身的 LocalLogsRootHash
,以检索最终的 fullRootHash
,该哈希与批次一起提交到 L1。因此,在这种情况下,merkleProof
应该包含一个从 ZKChain 的 L2ToL1LogsTree
日志叶子开始,一直到 SharedTree
的 aggregatedRoot
的路径。
证明验证包括以下 3 个步骤:
fullRootHash
。 与先前描述的 L2 日志包含类似,使用 LocalLogsTree
merkle 路径从索引为 l2MessageIndex
的 L2ToL1Log
叶子开始重建 LocalLogsRootHash
,最后与链的默认 aggregatedRoot
进行哈希。fullRootHash
叶子开始的 ChainTree
merkle 路径重建 ZKChain 的 ChainTree
的 chainRoot
。ChainRoot
叶子开始的 SharedTree
merkle 路径重建 aggregatedRoot
。通过聚合 merkleProof
中上述 3 个步骤的 merkle 路径,我们获得了一条通往 aggregatedRoot
的路径。 为了形成最终的 Gateway fullRootHash
提交到 L1,还有一个最后的技巧。
请注意,fullRootHash = hash(LocalLogsRootHash, aggregatedRoot)
,其中 aggregatedRoot
在右侧哈希,而 Gateway 的 LocalLogsRootHash
在左侧。 为了将 Gateway 的 LocalLogsRootHash
附加到最终聚合的 mekleProof
的左侧,需要特别制作步骤 3 中 chainRoot
的索引。 具体来说,chainRoot
的 index
变为 index + 2^N
,其中 N
是 SharedTree
的高度。 这确保了最终哈希将与哈希右侧的 ChainRoot
一起进行。
这完成了递归证明构造,以验证 Gateway 结算的 ZKChain 在 L1 上的日志包含。
通过利用 Gateway
ZKChain 作为中间结算层,并设计这种递归证明方案,ZK Stack 保持了一致的 Rollup-to-SettlementLayer 通信风格,确保了向后兼容性,并允许可扩展和高效的跨链交易和证明机制。
- 原文链接: blog.openzeppelin.com/in...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!