# zkSync Era 交易的工作流程：从生成到最终确定

By [ZkSync |Chinese Community](https://paragraph.com/@zksync-chinese-community) · 2023-11-01

---

原文：[Quarkslab](https://blog.quarkslab.com/zksync-transaction-workflow.html)  
翻译：[zkSync CN Community](https://twitter.com/zkSync_cn)  
校对：Berri、Derivatives

这篇博客文章介绍了在 zkSync Era 上交易的完整工作流程。zkSync Era 是一个 Zk Rollup Layer 2 区块链，它使用零知识证明在以太坊区块链上执行交易并证明其执行。

* * *

**介绍**
------

> 特别感谢 Matter Labs 团队的 [@vladbochok1](https://twitter.com/vladbochok1) 在发布前对这篇博客文章进行了审核。

[zkSync Era](https://zksync.io/) 是一种使用零知识加密来扩展以太坊吞吐量的 Layer 2 协议。它被称为 Zk Rollup。该协议旨在使去中心化应用程序更易于访问和加速区块链技术的大规模采用。截至今天，已有超过 4 亿美元的资金被桥接到 zkSync Era。

在这篇博文中，我们将介绍第 2 层交易的整个工作流程。交易的不同状态将被详细说明，从生成到最终确定。为了说明该过程，我们将重点关注和理解一个示例交易。

![ zkSync 交易的完整工作流程 ](https://storage.googleapis.com/papyrus_images/28a5b4923a691e8251e006865b320990f857120f5e29fc0c23576b2a05e17faf.png)

zkSync 交易的完整工作流程

_为了便于阅读，本博文中将使用以下缩写：_

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

**背景**
------

自 2015 年正式推出以来，以太坊已成为智能合约的区块链领导者，这主要得益于其通用性、安全性和去中心化。然而，这些强大的品质是以有限的扩展性为代价的。

这种限制如此重要，以至于为此创造了一个特殊的概念：**区块链三角困难**。区块链三角困难是说，设计同时具有去中心化、安全性和可扩展性的区块链是非常困难（甚至不可能）的。在过去，以太坊牺牲了可扩展性，将网络限制在每秒约 12 笔交易。多年来，区块链一直处于满负荷状态，导致用户之间争夺打包权，这意味着费用更高（在最拥塞的时期每笔交易高达 100 美元）。

其中一种解决方案是 Layer 2 区块链，利用以太坊的安全性和去中心化来提出高度可扩展的区块链。

Zk Rollups 是利用零知识 (ZK) 技术来扩展以太坊吞吐量而不损害以太坊安全性的 Layer 2 解决方案。

**创建交易**
--------

![交易的创建 ](https://storage.googleapis.com/papyrus_images/04a5ed38304a867eaf9f20e9adfdf922fe49938c7de292faf6fb1dff6ccb150f.png)

交易的创建

以下是示例交易的场景：当 Alice 想向著名的 Bob 转账 1 ETH。以太坊区块链上的交易费用对 Alice 来说太高了，她想降低这些费用。她决定使用 Zk Rollup 来转移价值，同时节省费用，因此她使用了 zkSync Era。

**生成交易**
--------

首先，与所有区块链一样，用户负责创建自己的交易。Alice 根据目标区块链格式生成她的交易，并使用她的密钥对对其进行签名。

**以太坊交易字段**
-----------

zkSync Era 交易基于[以太坊格式](https://ethereum.org/en/developers/docs/transactions/)。因此，Alice 将创建一个包含以下字段的交易：

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

> **注意**：最后 3 个值与 gas 相关，并取决于网络状态。如果这些值设置得太低，交易要么失败，要么直到网络费用变得更负担得起才被验证者纳入。

Alice 生成交易后，她将其发送到 L2 节点。在 zkSync Era 中，L2 节点称为 “operator”。通过调用 `eth_sendRawTransaction` 端点将交易发送到 L2 operator。zkSync Era 支持标准的 Ethereum JSON-RPC API，并提供自己的 L2 特有功能。

一旦 Alice 生成了交易，她就会将其发送到 L2 节点。在zkSync时代的背景下，L2节点被称为“运营商”。交易通过调用端点发送到 L2 操作员`eth_sendRawTransaction`。zkSync Era 支持标准的[以太坊 JSON-RPC API](https://ethereum.org/en/developers/docs/apis/json-rpc/)，并且还提供了自己的[L2 特定功能](https://era.zksync.io/docs/api/api.html)。

**zkSync时代交易字段**
----------------

zkSync Era 使用自己的特定字段来表示交易。以下是查询 zkSync 节点 JSON-RPC API 中的 [L2 交易详细信息](https://era.zksync.io/docs/api/api.html#zks-gettransactiondetails)时提供的字段：

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

根据交易类型，可以向交易添加其他字段，但我们在此不予考虑。我们将在整个博客文章中使用 `status` 字段来跟踪 Alice 的交易演变。

**账户和地址**
---------

在经典的 EVM 中，帐户由地址表示（160 位标识符，通常以十六进制形式表示）。 `0x29DF43F75149D0552475A6f9B2aC96E28796ed0b`是以太坊上使用的真实地址的示例。并存在两种类型的账户：外部拥有账户（EOA）和智能合约账户。

**外部账户 (EOA)**
--------------

在以太坊上，EOA 地址是公钥创建的。它对应于 secp256k1 公钥的 Keccak-256 哈希的后 20 个字节。可以用以下公式表示： `address = keccak256(secp256k1_pubkey)[12:]` zkSync Era 使用相同的 EOA 地址。这样，大多数钱包（例如 MetaMask）可以直接开箱即用地支持 zkSync Era。

**智能合约**
--------

在以太坊上，智能合约是一个部署了字节码的地址，这意味着该账户的代码大小非零。所有非零代码大小的账户都被视为智能合约，所有零代码大小的账户都被视为 EOA。智能合约地址是根据字节码哈希、部署者地址和其他数据（例如随机数或非随机数和静态标识符）确定的。

在 zkSync Era 生态系统中，所有账户都被定义为智能合约。对于所有大于 `2^16 -1=0xFFFF` 且未定义字节码的地址，都附加了一个默认账户代码。这个默认账户代码主要用于验证和执行 EOA 的交易。小于 `2^16` 的地址用于系统合约。**它们是内置**的一部分，并启用了一些高级功能，例如向 L1 发送消息和管理 L2 ETH 余额。在下一节中将详细介绍一些系统合约，因为它们被 Alice 的交易使用。

用户可以在 zkSync Era 上部署自己的智能合约。该解决方案旨在提供对现有 Solidity 代码库的兼容性。zkEVM 字节码可以像在以太坊上部署 EVM 字节码一样在 zkSync Era 上部署，但有[一些细微的差别](https://era.zksync.io/docs/reference/architecture/differences-with-ethereum.html)。智能合约地址的创建方式也不同，尤其是为了避免跨链攻击。zkSync Era 的 zkEVM 的内部工作原理与 EVM 非常不同，但使用抽象层和[专用工具](https://era.zksync.io/docs/tools/compiler-toolchain/overview.html)允许大多数 Solidity 代码库开箱即用。[此外，zkSync Era（ Harhat](https://era.zksync.io/docs/tools/hardhat/)、[Foundry](https://github.com/matter-labs/foundry-zksync) ）支持最常用的 Solidity 开发工具。

**待处理交易**
---------

操作员收到 Alice 的交易后，会将其添加到交易内存池中。交易的状态被设置**pending**。此状态在操作员收到交易后几乎立即设置。

![待处理交易](https://storage.googleapis.com/papyrus_images/877cf6f343982aa06fa3822b648ce3ae1e4867e72c9bfff20bdadd128cf4d131.png)

待处理交易

**区块和批处理**
----------

操作员将交易内存池切分成名称为“L2区块”的区块。L2区块是一个包含特定元数据的交易列表。在 zkSync Era 的情况下，这些 L2 块只用于 L2 区块链，它们不会以这种形式包含在 L1 以太坊区块链中。L2 块背后的原理是为用户体验提供快速的验证，因为钱包会在交易被包含在 L2 块中后立即向用户显示验证。当前的 L2 块生产平均时间不到一秒。

一旦有多个 L2 块可用，它们就会合并成一个“L1 批处理”。L1 批处理包含所有交易，从第一个 L2 块到批处理中的最后一个 L2 块。L1 批处理稍后会在以太坊上提交和证明，这允许将提交 gas 成本分摊到所有分批交易中。这是降低 gas 成本和提高以太坊吞吐量的主要机制。

![zkSync时代的L2区块和L1批次](https://storage.googleapis.com/papyrus_images/bd531b6d6422dc5468907cb38fbf330fafbec828caa297a4eb3e9d894fd6be2d.png)

zkSync时代的L2区块和L1批次

**已包含交易**
---------

一旦操作员将交易包含在 L2 块中，交易的状态就会被设置为已包含。这目前在操作员收到交易后不到一秒钟的时间内就可以实现。大多数钱包使用此状态向用户确认 L2 交易的执行。

![包含交易](https://storage.googleapis.com/papyrus_images/644e6d48d1b4d921fa9cb604e1cb1220a51c7da339e9d87dc0ee2f6bb77e8c15.png)

包含交易

**交易执行**
--------

L1 批处理中的所有交易都在 zkSync Era 的专用虚拟机中执行：一个称为 **EraVM** 的 zkEVM。这个 zkEVM 的内部工作原理很复杂，所以我们只关注 Alice 的交易使用的组件。

L1 批处理被提供给引导加载程序 L2 合约。这个智能合约是一个特殊的合约：它是 EraVM 的入口点，负责执行提供的所有 L1 批处理中的交易。在执行之前，它的内存映射被初始化。根据 zkSync，这种内存初始化是为了方便和效率。它是唯一的不确定性点：“_引导加载程序从其预先填充了操作员想要的所有数据的内存开始_ ”。引导加载程序从调用 `SystemContext` 系统合约开始，设置多个上下文变量，如 L1 批处理时间戳、其索引和上一个 L1 批处理的哈希值。一旦变量被设置，它就会执行交易。对于每个交易，工作流程如下：

1.  引导加载程序检查交易是否格式正确。
    
2.  引导加载程序使用 `validateTransaction` 函数调用发送者（即 Alice）的账户来验证交易。
    
    1.  调用系统`NonceHolder`合约来增加账户随机数。
        
    2.  附加到发送者账户的默认账户代码检查交易签名的有效性。
        
3.  如果交易有效，引导加载程序将调用发送者的帐户，使用该`payForTransaction`函数来支付交易执行费用。
    
    1.  _为了便于阅读，下图中未包含此步骤。_
        
4.  如果交易费用已支付，引导加载程序使用 `executeTransaction` 函数调用发送者账户来执行交易。
    
    1.  默认帐户代码执行交易。
        
5.  引导加载程序将未消耗的 Gas 退还给发送者的帐户。
    
    1.  _为了便于阅读，下图中未包含此步骤。_
        

在 Alice 的交易中，交易的执行将从 Alice 转账给 Bob。在以太坊上，这将通过直接调用 Bob 的地址来完成，`value = 1_000_000_000_000_000_000` (即 1 ETH)，`input data = ""`。 但 EraVM 的工作方式有所不同。要使用某个值调用地址，将使用 `MsgValueSimulator` 系统合约。该系统合约将修改 `L2EthToken` 系统合约中 Alice 和 Bob 的 Ether 余额。一旦余额更新，它将调用 Bob 的账户，同时将 Alice 账户设置为发送者。这是使用 EraVM 特有的 opcode `mimic_call` 完成的，该 opcode 允许通过更改任何交易的 `msg.sender` 值来模拟其他账户。幸运的是，这个危险的 opcode 只能由位于内核空间中的系统合约使用。它不适用于用户智能合约，因为它将是一个严重的安全漏洞。

![执行Alice的交易](https://storage.googleapis.com/papyrus_images/b851c1e9213b69adde96733e1b67886f2bcb8578e7a125f85a31d4e01d60a2db.png)

执行Alice的交易

**公开数据输出**
----------

EraVM 中交易的执行会产生公开数据。这些数据存储在 L1 (以太坊) 上，并允许重建 zkSync Era 的完整状态。zkSync 团队开发了自己的新解决方案来管理公开数据，该解决方案是 [Boojum](https://zksync.mirror.xyz/HJ2Pj45EJkRdt5Pau-ZXwkV2ctPx8qFL19STM5jdYhc) 升级中引入的证明系统的一部分。该解决方案允许在将公开数据存储到 L1 区块链之前对其进行压缩。公开数据压缩主要用于在以太坊上存储数据时优化 gas 费用。

zkSync 的公开数据分为 4 类：

*   **L2 到 L1 日志**：从 L2 到 L1 通信的“可证明”部分。它们与 EraVM 执行证明相关联（将在本博客文章后面解释）。
    
*   **L2 到 L1 消息**：不能在日志中发送的长消息。每个消息都与 L2 到 L1 日志相关联。
    
*   **智能合约字节码**：部署在 L2 智能合约中的字节码。每个字节码都与 L2 到 L1 日志相关联。
    
*   **存储写入**：这些数据涉及 L2 智能合约的存储槽状态（请记住，zkSync 上的所有账户都是智能合约）。
    

回到 Alice 的交易，pubdata 中有趣的类别是**Storage writes**，也称为**storage diffs**。`L2EthToken`更准确地说，我们知道当Alice向Bob发送1个ETH时，修改后的存储槽位是系统合约中Alice的余额和Bob的余额。

zkSync Era 被设计为“基于statediff”的汇总。因此，它以确保数据可用性的方式在 L1 上发布状态更改。众所周知，以太坊有一个[2 层树](https://ethereum.org/en/developers/docs/data-structures-and-encoding/patricia-merkle-trie/) ，它将存储槽数据附加到帐户地址（树的第一层），然后附加到存储槽编号（第二层）。与以太坊不同，zkSync 的树被定义为“扁平”。它是一棵 1 层树，将数据存储到槽的派生密钥。派生密钥是通过对存储槽号和该槽的账户地址进行散列计算得出的：`H(Slot number, Account)`。

![zkSync Era 和以太坊在存储槽数据树上的差异](https://storage.googleapis.com/papyrus_images/320be24ddf7b2a3698ea429d7a53eaea7af292c6027dfbcdb85e1172bc1f31a3.png)

zkSync Era 和以太坊在存储槽数据树上的差异

在 zkSync 的公开数据解决方案中，初始写入和重复写入的处理方式不同。初始写入将创建派生键，并将一个顺序 ID 附加到该派生键上。该对`derived key, value`将在 L1 上发布，一旦顺序 ID 已附加到派生键上，它将用于未来的写入，称为重复写入。因此，对于重复写入（即对现有存储槽值的修改），将使用附加到该槽的顺序 ID。此 ID 称为 `enumeration_index`，每个 ID 都永久分配给一个存储槽。与以太坊一样，在 zkSync Era 上读取空的存储槽将返回一个零值。

一旦所有 L1 批处理公开数据被压缩，它就会被提交到 `L1Messenger` 系统合约。该合约验证公开数据是否一致且已正确压缩。如果数据有效，则使用 EraVM 特有的 opcode to\_l1 将压缩数据存储在 zkSync 的 L1 智能合约中 。

此步骤称为“L1 批处理提交”，它是通过调用 L1 上的 `commitBatches` 函数执行的。此 L1 交易的哈希将由 zkSync 的节点报告为与 L2 交易详细信息（如前）相关的 `eth_commit_tx_hash` 字段的值。一旦 L1 批处理数据存储在 L1 上，批处理中的所有交易将保持已包含状态，并且 L1 批处理将设置为**已提交状态**。

![Boojum 的 L1 批处理的公开数据已提交到以太坊](https://storage.googleapis.com/papyrus_images/30dc5dadf080f4fb333ff5dd08c0c110f93caf465abed8039f9aeae97fef07b3.png)

Boojum 的 L1 批处理的公开数据已提交到以太坊

**已验证交易**
---------

Alice 交易工作流程的下一步是交易验证。当包含交易的 L1 批次已在 L1 智能合约上得到验证时，交易就被称为“已验证”。

![已验证交易](https://storage.googleapis.com/papyrus_images/48270ec184c7f941746abf2ab29ef98b813526707db512c91793c89a38162f0e.png)

已验证交易

**zkSync证明系统**
--------------

zkSync Era 是一个 ZK Rollup。因此，它使用零知识证明 (Zero-Knowledge Proof, ZKP) 系统来简洁地证明 L2 交易的执行。简洁性是 ZK Rollup 中使用 ZKP 的主要原因。ZKP 的验证过程允许验证成千上万笔交易的正确计算。有几种 ZKP 系统。zkSync Era 开发了自己的新 ZKP 系统，它是 Boojum 升级的一部分。

Boojum[证明系统可以被视为实现ZK电路](https://zksync.mirror.xyz/HJ2Pj45EJkRdt5Pau-ZXwkV2ctPx8qFL19STM5jdYhc)(ZK circuit) 工具箱。这些 ZK 电路用于执行任意计算。它们对于 Boojum 等证明系统生成计算证明是必不可少的。这些证明可以被非交互式地验证，证明 ZK 电路被正确地执行了。

EraVM 是一个 zkEVM。为 EraVM 开发了特定的 ZK 电路以适应 EVM 行为。它们用于证明VM的正确执行。每次处理 L1 批次时，EraVM 都会生成一个 ZK 见证。

该见证人允许操作员（即证明者）证明 L1 批次的计算正确。一旦 L1 批次提交给 zkSync 的 L1 智能合约，就可以向该智能合约提供见证人，以证明 EraVM 正确计算了 L1 批次。然后，L1 智能合约将验证委托给专门的智能合约（部署在 L1 上）充当 ZKP 系统中的验证者。

**执行证明**
--------

![在 EraVM 中生成 L1 批量执行的 ZK 证明](https://storage.googleapis.com/papyrus_images/6280d31120f9cf08a15c5495ebdde526226c5bd37082733e8520d753064a8ec6.png)

在 EraVM 中生成 L1 批量执行的 ZK 证明

此验证步骤是通过调用`proveBatches`L1 智能合约上的函数来完成的。此 L1 交易的哈希值将由 zkSync 的节点报告为与 L2 交易详细信息关联的字段的值`eth_prove_tx_hash`（如前所述）。L1批次数据在L1上验证完成后，该批次的所有交易都会被设置为**已验证**状态。L1 批次也设置为**已验证**状态。

**L1 智能合约**
-----------

zkSync 的 L1 智能合约是一个[钻石](https://eips.ethereum.org/EIPS/eip-2535)代理。该系统用于允许渐进式智能合约更新，并消除智能合约附带的 24 KB 字节码大小限制。

**钻石架构**
--------

核心思想是将功能与存储分开，并将其分成几个相关的智能合约。在 zkSync Era 的当前状态下，这些是：

![ ](https://storage.googleapis.com/papyrus_images/86f0c82d9ac44a456f1157275309ed78a50cf325f0ccff7182882f8cf7c3492c.png)

> 请注意，治理和 DiamondCut 功能在部署时是分开的。

要了解 Alice 的交易是如何在 L1 上发布和验证的，我们将重点关注 `Executor facet`。

在此级别上，zkSync 仅处理整个批次的 L2 交易。这使得能够在 L2 上对交易进行压缩，并通过在批次的交易之间共享交易费用来降低交易费用。因此，Alice 的交易被压缩并与其他交易捆绑在一起。该批次不包含完整的 L2 交易列表，而只包含重建 L2 区块链状态所需的变化列表，以确保数据可用性（请参阅“公共数据输出”部分）。

该批次首先使用 `commitBatches` 函数提交到 L1 智能合约，该函数仅供验证者组访问。该函数确保批次有效、按顺序提交且基于有效的已知状态，但它不保证更改集的有效性。此时，Alice 的交易被简化为其影响，并简单地发布到 L1。这对应于“已包含”状态。

接下来是证明，当调用 `proveBatches` 函数（仅供验证者调用）时。验证者提供批次的零知识证明，证明 EraVM 在公共输入集（包括 Alice 的交易）上的良好执行。一个称为验证器的专用智能合约负责验证证明。该特殊合约应使用协议升级与 EraVM 的升级一起更新。

一旦批次被证明，需要执行跨链操作（存储在可通过 Mailbox facet 访问的优先级队列中）。由于 Alice 转移的资金仍保留在 L2 上，因此此交易不需要任何操作。在执行所有操作后，批次可以在调用 executeBatches 时转换为“已执行”状态。Alice 的交易现在被视为已**最终确定。**

![交易完成](https://storage.googleapis.com/papyrus_images/1f89fea0072cd9cc463bdaa799ee4ec60553bf7f675efd42f997aab5766a94eb.png)

交易完成

**交易完成**
--------

一旦一批被验证，其中的所有交易的状态都会被**验证**。在交易最终**确定**之前，还有最后一步需要执行。

**L1批量执行**
----------

多个L1批次已在L1智能合约上**提交**和**验证**。工作流程的最后一步是执行 L1 批次。这意味着L1批次之后获得的状态必须成为L1合约中L2的正式状态。

为了优化以太坊上的 Gas 成本，单个 L1 交易将用于执行多个 L1 批次。这些交易由 zkSync 运营商发送。截至目前，L2 高度中心化，zkSync 运营商在 L2 拥有很大的权力。

![在 L1 智能合约上执行 L1 批次](https://storage.googleapis.com/papyrus_images/581b4ea6fdbf2334f7114273958bd4848e015c87b65467e9e9e7189bb1477f2c.png)

在 L1 智能合约上执行 L1 批次

最后一步是通过调用 `executeBatches` L1 菱形中的函数来完成的。此 L1 交易的哈希值将由 zkSync 的节点报告为与 L2 交易详细信息关联的字段的值 `eth_execute_tx_hash`（如前所述）。实际上，状态最终确定是通过为每个 L1 批次导入 L2 日志 Merkle 树来完成的。

完成此步骤后，L1 批次将标记为**Finalized**。此时，无法取消 L2 交易。L2 交易可以被认定为**最终确定**。

**交易流程总结**
----------

zkSync Era 上的 L2 交易会经过多个步骤。交易会在 L2 层快速确认，并且用户会收到交易有效的通知。但是，这些给用户钱包的确认只能确保交易在 L2 上的执行。为了确保协议值得信赖，还需要进行更低级别的检查。例如，需要证明 L2 虚拟机的执行是正确的，以确保执行的交易是有效的。

通过使用零知识证明，zkSync Era 能够证明其 EraVM 的正确执行以及该执行的输出是有效的。在链上生成和验证一个正确执行的证明，使 zkSync Era 能够将其 L2 提升到“以太坊的安全性”。

ZKP 是解决以太坊可扩展性问题的强大工具。可扩展性通过将许多交易捆绑在一起并只提交一个证明来提高。验证者不需要计算整个交易集来验证它们的正确执行，而可以只关注交易的影响。在 zkSync Era 的情况下，数百笔交易被验证为正确的，只需使用一个验证证明的以太坊交易。根据 [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844)，ZK Rollup 的费用比以太坊的费用低约 40-100 倍。这种降低是通过 ZKP 实现的。

但 EraVM 的实现可能存在漏洞。例如，它可能将无效交易执行为有效交易，反之亦然。这就是为什么 zkSync 团队组织了一个奖金池为 110 万美元的 Code4rena 比赛。这场审计比赛旨在寻找 zkSync Era 中的漏洞，以便在发布之前修复这些漏洞，并避免在区块链生态系统中经常造成重大影响的生产漏洞。

**额外的背景信息**
-----------

现在我们已经在 Alice 的交易生命周期中说明了 zkSync，让我们讨论一下我们之前忽略的一些兴趣点。

### **zkSync 的 Diamond 实现分析**

作为提醒，Diamond 智能合约是专门为颗粒度的可升级性和无限的功能而设计的。

要实现这一点，外部可访问的函数使用其 Solidity 风格的选择器进行注册[1](https://blog.quarkslab.com/zksync-transaction-workflow.html#fn:def-selector) 每个选择器都映射到包含相关逻辑的 `Facet` 。当调用时，Diamond 合约的 `fallback` 函数会检索适当的 Facet，使用其 `calldata` 执行 `DELEGATECALL` 并转发 Facet 的返回状态。使用 `DELEGATECALL` 意味着 Facet 对 Diamond 智能合约存储具有完全的控制权，因此需要仔细控制注册的 Facet 和函数。

要更新 Diamond 的注册函数，必须使用 `diamondCut` 内部函数[2](https://blog.quarkslab.com/zksync-transaction-workflow.html#fn:rec-diamondCut) 。在 zkSync 的当前设计中，这只有在部署时（在构造函数中）和使用 Admin Facet 的 executeUpgrade 函数（由 onlyGovernor 保护）才可能。

Facet 没有关联的存储。相反，它通过 DELEGATECALL 直接访问 Diamond 的存储空间。为了实现 Facet 之间的互操作性并防止存储空间冲突，该标准[提出了](https://eips.ethereum.org/EIPS/eip-2535#storage)使用两种模式：[Diamond Storage](https://eips.ethereum.org/assets/eip-2535/storage-examples/DiamondStorage.sol) 和 [AppStorage](https://eips.ethereum.org/assets/eip-2535/storage-examples/AppStorage.sol)。zkSync 使用两种模式：

*   一个`DiamondStorage` 结构，负责与 Diamond 相关的功能，位于`keccak256("diamond.standard.diamond.storage") - 1`,
    
*   一个 `AppStorage` 结构，保存应用程序的状态并位于 `Base` 智能合约的第一个槽中，由所有 Facet 作为第一个父级继承。
    

总之，zkSync 对 Diamond 标准的实现使用了一组包含相关功能的 Facet，并将它们聚合在一个智能合约中。存储仅包含在两个结构中：`DiamondStorage`和`AppStorage`。功能分布在 4 个方面。

### **Diamond 库及其代理**

尽管 [EIP-2535](https://eips.ethereum.org/EIPS/eip-2535) 允许 Diamond 智能合约具有原生的外部可访问函数，但 zkSync 目前没有这个功能，它将所有内容委托给其 fallback 函数中的 Facet，包括 Diamond 相关的功能。这就是为什么它被称为 Diamond _Proxy_ 的原因。实际的 _Diamond_ 相关功能位于只由 Proxy、`Admin` Facet 和 `Getters` Facet 使用的 Diamond 库中。

`Diamond` 的核心对象是 `selectorToFacet` 映射。它用于知道哪个 Facet 应该被调用来处理特定的函数选择器。

尽管理论上只有 `mapping(bytes4 => address)` 就足够了（因此在标准的示例中使用了它），但 zkSync 选择使用更复杂的数据结构来允许更高效的内省和更新，以及冻结功能。作为交换，该库需要特别注意确保内部数据的一致性和准确的簿记。

Diamond 实现了一个 Facet 地址和选择器之间双向的一对多映射：

*   每个 Facet 地址都映射到 `facetToSelectors` 中的选择器列表，和
    
*   每个选择器都映射到 `selectorToFacet` 中的单个 Facet 地址。
    

此外，它还在 `address[] facets` 中跟踪已用 Facet 地址的列表，并在 `bool isFrozen` 中跟踪全局冻结状态。

zkSync 的 Diamond 实现的概念实体关系图

![zkSync 的 Diamond 实现的概念实体关系图](https://storage.googleapis.com/papyrus_images/c9dd846f2ddd29cd0d95ee67e60d95431eac726ce40d0b4fbf0fbd2369327ec7.png)

zkSync 的 Diamond 实现的概念实体关系图

主映射使用专门的对象来保存其元素的额外信息：

SelectorToFacet 有以下字段（请注意，所有这些值都由 Solidity 打包到一个槽中，从而提高了整体的 gas 消耗）：

*   `SelectorToFacet`有以下字段（请注意，所有这些值都由 Solidity 打包到一个槽中，从而提高了整体的 gas 消耗）[3](https://blog.quarkslab.com/zksync-transaction-workflow.html#fn:prec-SelectorToFacet)：
    
    *   `address facetAddress` (data),
        
    *   `bool isFreezable` (metadata) and
        
    *   `uint16 selectorPosition` (bookkeeping).
        
    *   `FacetToSelectors` has these fields:
        
    *   `bytes4[] selectors` (data), and
        
    *   `uint16 facetPosition` (bookkeeping)
        

双向映射中的一对多关系在 `FacetToSelectors` 中由选择器数组表示，在 `SelectorToFacet` 中由选择器在相应数组中的位置表示。这意味着这些值需要由库保持同步，并且每个 Facet 最多可以注册 `2^16` 个选择器（这应该足够了）。这种簿记的优势在于，库不需要执行（可能代价高昂的）搜索来找到如何更新其内部数据结构，但代价是更多的预先计算和稍微更高的存储空间占用。

选择器的哈希值是 4 字节长的，可以用于快速查找选择器对应的 Facet。

EIP-2535 建议此函数为 external，但不要求这样做。这意味着需要在此函数的级别实现访问控制。

zkSync 原本可以使用 uint88 并在单个槽中保留结构。

* * *

1.  函数签名的 4 字节长哈希值。请参阅[官方文档](https://docs.soliditylang.org/en/v0.8.12/abi-spec.html#function-selector) 和[4byte.directory](https://www.4byte.directory/)以获得在线彩虹表。 [↩](https://blog.quarkslab.com/zksync-transaction-workflow.html#fnref:def-selector)
    
2.  EIP [\-2535](https://eips.ethereum.org/EIPS/eip-2535) `external` 建议但不要求 使用此功能。这将需要在此功能级别实现访问控制。 [↩](https://blog.quarkslab.com/zksync-transaction-workflow.html#fnref:rec-diamondCut)
    
3.  zkSync 可以使用最多`uint88`，同时将结构保留在单个插槽内。 [↩](https://blog.quarkslab.com/zksync-transaction-workflow.html#fnref:prec-SelectorToFacet)
    

* * *

---

*Originally published on [ZkSync |Chinese Community](https://paragraph.com/@zksync-chinese-community/zksync-era)*
