# 进入未知

By [白开水](https://paragraph.com/@baikaishui) · 2022-05-20

---

![](https://storage.googleapis.com/papyrus_images/67079326630a726e36b08db3e365f3ff49db1aa8763af82a651feceb6e2f14be.png)

对构建 zkSync 2.0 挑战的详细技术见解
------------------------

在这篇文章中，我们将分享尝试的不是一个，而是两个以前从未做过的事情的研发过程，zkSync 2.0 的重新设计如何更高效和兼容，以及使用具有独特架构的 LLVM 的挑战作为 zk EVM。

### zkSync 2.0 的设计目标

zkSync 2.0的设计涉及到两件以前从未做过的事情：

*   zk EVM：对 SNARK 友好的 EVM 堆栈操作操作码（例如 popN、dup）准备数据并且不执行有意义的计算，但它们通常构成交易的相当大的成本。这种语义对于 zkSNARKs 来说是低效的，所以我们的 zkEVM 使用了不同的操作数寻址方法，但是从一个到另一个的转换需要在编译器端进行额外的工作。
    

通过编译成对 SNARK 友好的 zkEVM，我们可以为操作码提供更通用的操作数寻址，降低交易成本，减少证明的延迟，并提供额外的好处，例如一个块中许多交易的摊销公共数据使用。

*   不同的每账户数据可用性政策 在同一个地址空间中为 zkRollup 和 zkPorter 账户进行交互设计可以比作 ETH 2.0 分片但具有同步执行。此外，如果其中一个“碎片”不可用，则两者之间的交互必须可证明无法交互。
    

除了高效执行 Solidity 智能合约之外，让协议看起来像以太坊也是一项非常复杂的任务。

重新设计以提高效率和兼容性 我们之前讨论过我们的新设计更高效并且与以太坊兼容。以下是我们所做的一些更改：

现在可以仅使用我们的 Web3+ API 和 Ethers+ SDK 与我们的网络进行交互： 对于读取请求：

*   任何语言的任何兼容 web3 的框架都可以开箱即用，我们还将提供额外的 zkSync L2 特定功能 对于写请求：由于 L1 和 L2 之间的根本区别，您将不得不编写一些额外的代码（例如，zkSync 支持以任何代币支付费用，因此发送交易将涉及选择代币支付费用）
    
*   更新了内存实现，允许我们在 zkEVM 的每个周期访问更多内存。此外，每个区块对同一合约代码的所有访问现在都被“摊销”了。
    
*   增加动态定价模型：zkSync 中的 gas 很大程度上是由价格固定来证明的（例如 1 个周期 = 1 个单位），但如果用户使用公共数据，则它以单位为单位动态定价，以反映以太坊 gas 价格和以太币价格。
    

LLVM 的挑战和好处
-----------

Yul 前端聚焦：让 Yul 与 zk EVM 的内存模型一起工作
---------------------------------

为了支持 Solidity 智能合约，我们将 Yul IR 转换为 LLVM IR。我们通过使所有 Yul 指令与 LLVM 框架和目标虚拟机兼容来构建 Yul 前端：zk EVM。_主要挑战是我们必须按原样翻译 Yul（并在以后优化结果！）；我们不能以任何方式改变它_。

虽然 Yul 语法最小且易于解析，但一些指令和模式需要变通方法才能与我们的 zkEVM 兼容。让我们深入研究几个有趣的例子：

1.  未对齐的内存访问
    

*   特例 A：处理呼叫数据
    
*   特例 B：适配构造函数
    

2\. 保留执行流程

**未对齐的内存访问**

在 EVM 中，内存使用字节偏移量进行寻址：

*   mload(12)：从区域 12..44 加载 32 个字节
    
*   mstore(16, X)：将 X 存储在区域 16..48
    
*   calldataload(4)：加载第一个 calldata 参数
    

另一方面，在 zkEVM 中，内存是通过单元偏移来寻址的，其中每个单元仍然由 32 个字节组成，但每个单元是原子的，只能作为一个整体进行读取或写入：

*   mov r1, 4：将寄存器r1的值存储到单元格4，即字节128..160
    
*   mov 4, r1：将单元格 4 中的值加载到寄存器 r1
    

为了翻译像 mload(4) 这样的指令，我们必须从单元 1 加载 28 个字节，然后从单元 2 加载 4 个字节，并使用按位运算将它们合并成一个 32 字节的值。

幸运的是，Yul 很少使用不是 32 倍数的偏移量。通常情况如下：

*   calldata 布局：每个参数使用签名哈希移动 4 个字节
    
*   错误消息编码：代码和描述以小于 32 字节的块形式出现
    
*   作为映射键的字符串文字通常不会占用整个 32 字节块
    

上述所有情况都需要单元拆分和合并，对于非常量地址，每条内存访问指令的复杂度都会增加数倍。有一些建议可以针对我们的 zk EVM 需求优化 Yul 生成器的行为，例如，将外部合约条目签名哈希存储在其自己的 32 字节块中，以及在将错误消息块写入内存之前在 Yul 中合并它们。

**特例 A：处理呼叫数据**

在 Solidity 合约 ABI 中，calldata 的前 4 个字节是入口签名哈希。然后，每个参数存储在偏移量 4+32n 处。

在 zkEVM 合约 ABI 中，我们向 calldata 添加了一个 252 字节的标头以及一些附加信息：

*   单元格中的调用数据大小
    
*   返回单元格中的数据大小
    

然后，我们将条目签名哈希和参数从区域 252.. 开始，并在偏移量 256、288、320 等处使用对齐的内存访问加载每个参数。

这是通过检查 calldataload(A) 指令中的地址来实现的。如果地址为零，我们从区域 224..256 加载条目签名哈希，否则，我们从区域 A+252..A+252+32 加载参数。

**特例 B：适配构造函数**

在 Solidity 中，构造函数参数与最后的合约字节码一起部署。在 zk EVM 中，构造函数是一个普通的合约入口，只能调用一次；在第一次执行结束时会在存储中设置一个保护标志。

**保留执行流程**

许多 Yul 指令已被存根以保留预期的执行流程。例如，Yul 在进行外部调用之前使用 \`extcodesize\` 指令检查外部合约大小。我们在调用之前不做这样的检查，所以我们总是返回 MAX\_VALUE 来通过代码大小检查。其他存根指令包括 pc、callvalue、msize、balance 等。

zkEVM 后端：支持 LLVM 中的 EVM 和 zk EVM 的特性
------------------------------------

我们的架构有一些特性，我们不得不花一些时间将其集成到 LLVM 框架中：

*   zkEVM 使用 256 位宽的寄存器，但 LLVM 在许多情况下本身并不支持 256 位整数
    
*   256 位是内存中的最小寻址区域，但 LLVM 硬编码假设内存寻址为 8 位字节
    
*   我们在堆栈中缺少绝对地址：所有地址都必须与堆栈指针相关，并且 LLVM 中没有这样的架构，因此我们不得不引入自定义通道来处理它（但是，我们计划在初始版本之后引入绝对地址)
    
*   VM 不提供显式读取或写入堆栈指针的能力，只能通过 push 和 pop 指令
    
*   出于安全原因，zkEVM 不支持跳转到任意目的地，仅支持代码中的静态已知地址
    
*   zkEVM 不支持有符号算术，我们为它实现了自定义降低
    

测试，测试，测试！
---------

使用 LLVM 的主要优势之一是我们受益于他们庞大的测试套件存储库：

*   回归测试检查编译器生成合理的输出和优化是否按预期工作。核心 LLVM 存储库中有 36,000 个公共测试可用，我们从其中的 20,000 个中受益。（其他 16,000 个检查不同的 LLVM 后端）
    
*   可执行测试或集成测试运行编译器，但也运行编译器生成的二进制文件。在我们使用的 LLVM 测试套件存储库中，至少有 3,000 个这样的测试是公开可用的。
    
*   为了检查我们的整个编译器工作流程，我们实现了自己的可执行测试套件，目前有 1,000 个测试，使用所有支持的语言编写，包括 IR：Solidity、Zinc、Yul、LLVM IR 和 zkEVM 程序集。
    

结论
==

我们希望您喜欢这个窗口，了解我们在编译器和 zkSync 2.0 的研发过程中所面临的挑战！

如果您对此感兴趣，我们正在[招聘](https://boards.eu.greenhouse.io/matterlabs)！如果您有任何问题，请在我们的[Discord](https://discord.gg/6xyhfjxHyG)中提问！

---

*Originally published on [白开水](https://paragraph.com/@baikaishui/e6Sjon3JCtHzPAwBL4X0)*
