系统合约/引导加载程序描述 (VM v1.5.0)

本文档详细描述了 Matter Labs 的 ZKsync Era 的系统合约和启动加载器的架构与功能。

系统合约/启动加载器描述 (VM v1.5.0)

返回 readme

启动加载器

在标准的以太坊客户端上,执行区块的工作流程如下:

  1. 选择一笔交易,验证交易并收取手续费,执行它
  2. 收集状态更改(如果交易没有回滚),将它们应用到状态。
  3. 如果区块 gas 限制尚未超过,则返回步骤 (1)。

然而,在 ZKsync 上采用这样的流程(即,逐个处理交易)效率太低,因为我们必须为每个单独的交易运行整个证明工作流程。这就是我们需要 启动加载器 的原因:与其分别运行 N 个交易,不如我们将整个批次(一组区块,更多内容可以在这里找到)作为一个单独的程序运行,该程序接受交易数组以及一些其他的批次元数据,并在一个大的“交易”中处理它们。思考 bootloader 的最简单方式是考虑 EIP4337 中的 EntryPoint:它也接受交易数组并促进账户抽象协议。

启动加载器的代码哈希存储在 L1 上,并且只能作为系统升级的一部分进行更改。请注意,与系统合约不同,启动加载器的代码没有存储在 L2 上的任何地方。这就是我们有时将启动加载器的地址称为形式地址的原因。它仅仅是为了向 this / msg.sender / 等提供一些值而存在。当有人调用启动加载器地址(例如,支付费用)时,实际上调用的是 EmptyContract 的代码。

系统合约

虽然大多数原始的 EVM 操作码可以开箱即用(即,零值调用、加法/乘法/内存/存储管理等),但有些操作码默认情况下不受 VM 支持,它们是通过“系统合约”实现的——这些合约位于特殊的 内核空间 中,即地址空间在 [0..2^16-1] 范围内,并且它们具有用户合约不具备的一些特殊权限。这些合约在创世之初就已预部署,并且只能通过从 L1 管理的系统升级来更新其代码。

每个系统合约的用途将在下面解释。

预部署合约

某些合约需要在创世之初进行预部署,但它们不需要内核空间权限。为了给它们最小的权限,我们将它们预部署在紧随 2^16 之后的连续地址上。这些将在以下各节中描述。

zkEVM 内部原理

zkEVM 的完整规范超出了本文档的范围。然而,本节将为你提供理解 L2 系统智能合约以及 EVM 和 zkEVM 之间基本差异所需的大部分细节。

寄存器和内存管理

在 EVM 上,在交易执行期间,可以使用以下内存区域:

  • memory 本身。
  • calldata 父内存的不可变切片。
  • returndata 最近一次调用另一个合约返回的不可变切片。
  • stack 存储局部变量的地方。

与堆栈机 EVM 不同,zkEVM 有 16 个寄存器。zkEVM 不是从 calldata 接收输入,而是通过在其第一个寄存器中接收一个 指针 开始(基本上是一个包含 4 个元素的压缩结构体:内存页 id,切片的开始和长度,它指向父级的 calldata 页)。类似地,交易可以在程序开始时在其寄存器中接收一些其他的附加数据:交易是否应该调用构造函数(更多关于部署的信息在这里),交易是否具有 isSystem 标志等。每个标志的含义将在本节中进一步扩展。

指针 是 VM 中的单独类型。只有以下操作是可能的:

  • 读取指针内的某个值。
  • 通过减少指针指向的切片来缩小指针。
  • 接收指向 returndata/作为 calldata 的指针。
  • 指针只能存储在堆栈/寄存器上,以确保其他合约无法读取它们不应该访问的合约的内存/returndata。
  • 指针可以转换为表示它的 u256 整数,但整数不能转换为指针以防止未经允许的内存访问。
  • 不可能返回指向 id 小于当前页面的 id 的内存页面的指针。这意味着只能 return 指向当前帧内存或当前帧的子调用返回的指针之一的指针。
zkEVM 中的内存区域

对于每个帧,分配以下内存区域:

  • (与以太坊上的 memory 扮演相同的角色)。
  • 辅助堆 (auxiliary heap)。它具有与堆相同的属性,但它被编译器用来编码 calldata/从系统合约的调用中复制 returndata,以避免干扰标准的 Solidity 内存对齐。
  • 。与以太坊不同,栈不是获取操作码参数的主要场所。zkEVM 和 EVM 上栈的最大区别在于,在 ZKsync 上,栈可以在任何位置访问(就像内存一样)。虽然用户不为栈的增长付费,但栈可以在帧结束时完全清除,因此开销很小。
  • 代码。VM 从中执行合约代码的内存区域。合约本身无法读取代码页,它只能由 VM 隐式完成。

此外,如前一节所述,合约接收指向 calldata 的指针。

管理 returndata & calldata

每当合约完成其执行时,父帧会收到一个 指针 作为 returndata。此指针可能指向子帧的堆/辅助堆,甚至可能是子帧从其某些子帧收到的同一个 returndata 指针。

calldata 也是如此。每当合约开始执行时,它会收到指向 calldata 的指针。父帧可以提供任何有效的指针作为 calldata,这意味着它可以是指向父帧内存(堆或辅助堆)切片的指针,也可以是父帧之前作为 calldata/returndata 接收的某个有效指针。

合约只是记住执行帧开始时的 calldata 指针(这是编译器的设计),并记住最近收到的 returndata 指针。

其中一些重要的含义是,现在可以执行以下调用而无需任何内存复制:

A → B → C

其中 C 接收 B 接收的 calldata 的切片。

返回数据也是如此:

A ← B ← C

如果 B 返回 C 返回的 returndata 的切片,则无需复制返回的数据。

请注意,你 不能 使用你通过 calldata 收到的指针作为 returndata(即,在执行帧结束时返回它)。否则,returndata 可能会指向活动帧的内存切片并允许编辑 returndata。这意味着在上面的示例中,C 无法在没有内存复制的情况下返回其 calldata 的切片。

请注意,上面的规则是通过“不可能返回内存页面的 id 低于当前堆的内存页面 id 的数据切片”的原则来实现的,因为 id 较小的内存页面只能在调用之前创建。这就是为什么用户合约通常可以安全地返回先前返回的 returndata 的切片(因为它保证具有更高的内存页面 id)。但是,系统合约可以免除上述规则。特别是需要 CodeOracle 系统合约的正确功能。你可以在此处阅读更多相关信息。因此,经验法则是永远不要传递来自 CodeOracle 的 returndata。

其中一些内存优化可以在 EfficientCall 库中看到,该库允许在重用帧已经拥有的 calldata 切片的同时执行调用,而无需内存复制。

Returndata & 预编译

以太坊上的一些操作是操作码,但在 ZKsync 上已成为对某些系统合约的调用。最值得注意的例子是 Keccak256SystemContext 等。请注意,如果天真地完成,以下代码行将在 ZKsync 和以太坊上以不同的方式工作:

pop(call(...))
keccak(...)
returndatacopy(...)

因为调用 keccak 预编译会修改 returndata。为了避免这种情况,我们的编译器不会在调用此类类似操作码的预编译后覆盖最新的 returndata 指针。

ZKsync 特定的操作码

虽然某些以太坊操作码不直接支持,但添加了一些新的操作码以方便系统合约的开发。

请注意,此列表的目的不是具体说明内部原理,而是解释 SystemContractHelper.sol 中的方法

仅限内核空间

这些操作码仅允许内核空间(即系统合约)中的合约使用。如果在其他地方执行,它们会导致 revert(0,0)

  • mimic_call。与普通 call 相同,但它可以更改交易的 msg.sender 字段。
  • to_l1。向以太坊发送一个系统 L2→L1 日志。可以在此处查看此日志的结构。
  • event。向 ZKsync 发出一个 L2 日志。请注意,L2 日志不等同于以太坊事件。每个 L2 日志可以发出 64 字节的数据(实际大小为 88 字节,因为它包括发射器地址等)。单个以太坊事件由多个 event 日志构成。此操作码仅由 EventWriter 系统合约使用。
  • precompile_call。这是一个接受两个参数的操作码:表示其打包参数的 uint256 以及要燃烧的 ergs。除了预编译调用本身的价格外,它还会燃烧提供的 ergs 并执行预编译。它执行的操作取决于执行期间的 this
    • 如果它是 ecrecover 系统合约的地址,它将执行 ecrecover 操作
    • 如果它是 sha256/keccak256 系统合约的地址,它将执行相应的哈希操作。
    • 否则,它什么也不做(即,仅燃烧 ergs)。它可以用于燃烧 L2→L1 通信或在链上发布字节码所需的 ergs。
  • setValueForNextFarCall 为下一个 call/mimic_call 设置 msg.value。请注意,这并不意味着该值将真正转移。它只是设置相应的 msg.value 上下文变量。ETH 的转移应通过使用此参数的系统合约以其他方式完成。请注意,此方法对 delegatecall 没有影响,因为 delegatecall 继承了先前帧的 msg.value
  • increment_tx_counter 递增 VM 中交易的计数器。交易计数器主要用于 VM 内部跟踪事件。仅在每个交易结束后在启动加载器中使用。
  • decommit 将返回一个指向具有相应字节码哈希原像的切片的指针。如果此字节码之前已解压缩,则将重用已解压缩它的内存页面。如果它从未解压缩过,它将被解压缩到当前堆中。

请注意,目前我们无法在 VM 中访问 tx_counter(即,目前可以递增它,它将自动用于诸如 event 之类的日志以及由 to_l1 生成的系统日志,但我们无法读取它)。我们需要读取它以发布 用户 L2→L1 日志,因此 increment_tx_counter 始终伴随对 SystemContext 合约的相应调用。

有关系统日志和用户日志之间差异的更多信息,请参见此处

通常可访问

以下是任何合约通常可以访问的操作码。请注意,虽然 VM 允许访问这些方法,但这并不意味着它很容易:编译器可能尚未对某些用例提供方便的支持。

  • near_call。它基本上是一个“成帧的”跳转到你的合约代码的某个位置。near_call 和普通跳转之间的区别在于:
    1. 可以为其提供 ergsLimit。请注意,与“far_call”s(即,合约之间的调用)不同,63/64 规则不适用于它们。
    2. 如果 near call 帧发生 panic,则由其进行的所有状态更改都将被撤消。请注意,内存更改将 不会 被撤消。
  • getMeta。返回 ZkSyncMeta 结构的 u256 打包值。请注意,这不是紧密打包。该结构由以下 rust 代码形成。
  • getCodeAddress - 接收执行代码的地址。这与 this 不同,因为在 delegatecall 的情况下,this 被保留,但 codeAddress 没有。
调用的标志

除了 calldata 之外,还可以在执行 callmimic_calldelegate_call 时向被调用方提供其他信息。被调用合约将在执行开始时在其前 12 个寄存器中收到以下信息:

  • r1 — 指向 calldata 的指针。
  • r2 — 带有调用标志的指针。这是一个掩码,只有在为调用设置了某些标志时,才会设置每个位。目前,支持两个标志:第 0 位:isConstructor 标志。此标志只能由系统合约设置,并表示该帐户是否应执行其构造函数逻辑。请注意,与以太坊不同,构造函数和部署字节码之间没有分隔。有关更多信息,请参见此处。第 1 位:isSystem 标志。该调用是否打算调用系统合约的功能。虽然大多数系统合约的功能相对无害,但仅使用 calldata 访问某些功能可能会破坏以太坊的不变量,例如,如果系统合约使用 mimic_call:没有人期望通过调用合约,可能会以调用者的名义执行某些操作。只有在被调用方在内核空间中时,才能设置此标志。
  • 其余的 r3..r12 寄存器只有在设置了 isSystem 标志时才非空。可能会传递任意值,我们称之为 extraAbiParams

编译器实现是,这些标志由合约记住,并且可以通过特殊的模拟在执行期间稍后访问。

如果调用方提供不适当的标志(即,尝试在被调用方不在内核空间时设置 isSystem 标志),则将忽略这些标志。

onlySystemCall 修饰符

某些系统合约可以代表用户行事,或者对帐户的行为产生非常重要的影响。这就是我们想要明确用户不能通过执行简单的类似 EVM 的 call 来调用潜在危险的操作的原因。每当用户想要调用我们认为危险的某些操作时,他们都必须提供“isSystem”标志。

onlySystemCall 标志检查调用是否已通过提供的“isSystemCall”标志完成,或者该调用是否由另一个系统合约完成(因为 Matter Labs 完全了解系统合约)。

通过我们的编译器进行模拟

将来,我们计划推出我们的“扩展”版本的 Solidity,其中包含比原始版本更多的受支持的操作码。但是,目前团队的能力还不足以做到这一点,因此为了表示访问 ZKsync 特定的操作码,我们使用带有某些常量参数的 call 操作码,这些参数将由编译器自动替换为 zkEVM 本地操作码。

例:

function getCodeAddress() internal view returns (address addr) {
  address callAddr = CODE_ADDRESS_CALL_ADDRESS;
  assembly {
    addr := staticcall(0, callAddr, 0, 0xFFFF, 0, 0)
  }
}

在上面的示例中,编译器将检测到静态调用已完成到常量 CODE_ADDRESS_CALL_ADDRESS,因此它将使用用于获取当前执行的代码地址的操作码替换它。

操作码模拟的完整列表可以在此处找到。

我们还使用类似 verbatim 的语句来访问启动加载器中 ZKsync 特定的操作码。

我们 Solidity 代码中所有模拟的用法都在 SystemContractHelper 库和 SystemContractsCaller 库中实现。

模拟 near_call (仅限 Yul)

为了使用 near_call,即调用一个本地函数,同时提供此函数可以使用的 ergs(gas)限制,使用以下语法:

该函数应在其名称中包含 ZKSYNC_NEAR_CALL 字符串,并且至少接受 1 个输入参数。第一个输入参数是 near_call 的打包 ABI。目前,它等于要与 near_call 一起传递的 ergs 数量。

每当 near_call 发生 panic 时,都会调用 ZKSYNC_CATCH_NEAR_CALL 函数。

重要提示: 编译器的行为方式是,如果启动加载器中存在 revert,则不会调用 ZKSYNC_CATCH_NEAR_CALL 并且父帧也会被回滚。仅回滚 near_call 帧的唯一方法是触发 VM 的 panic(可以通过无效的操作码或 gas 不足错误触发)。

重要提示 2: 63/64 规则不适用于 near_call。此外,如果为 near call 提供了 0 gas,那么实际上所有可用的 gas 都将用于它。

安全注意事项

为防止意外替换,编译器需要传递 --system-mode 标志才能使上述替换工作。

请注意,在较新版本的编译器中,--system-mode 已重命名为 enable_eravm_extensions(这可以在例如我们的 foundry.toml 中看到)

字节码哈希

在 ZKsync 上,字节码哈希以以下格式存储:

  • 第 0 个字节表示格式的版本。目前,使用的唯一版本是“1”。
  • 第 1 个字节对于已部署合约的代码为 0,对于正在构造的合约代码为 1
  • 第 2 个和第 3 个字节以大端 2 字节数字表示合约长度,以 32 字节字为单位。
  • 接下来的 28 个字节是合约字节码的 sha256 哈希的最后 28 个字节。

字节以小端顺序排序(即,与 bytes32 的方式相同)。

字节码有效性

如果字节码满足以下条件,则它是有效的:

  • 其字节长度可被 32 整除(即,由整数个 32 字节字组成)。
  • 长度小于 2^16 个字(即,其字长度适合 2 个字节)。
  • 字长度为奇数(即,第 3 个字节是奇数)。

请注意,它不必仅由正确的操作码组成。如果 VM 遇到无效的操作码,它将简单地回滚(类似于 EVM 处理它们的方式)。

无法证明对具有无效字节码的合约的调用。这就是为什么 至关重要 的是,任何具有无效字节码的合约都不能部署在 ZKsync 上。这是 KnownCodesStorage 的工作,以确保系统中所有允许的字节码都是有效的。

帐户抽象

ZKsync 的另一个重要功能是支持帐户抽象。强烈建议在此处阅读有关我们的 AA 协议的文档:https://docs.zksync.io/zk-stack/concepts/account-abstraction

帐户版本控制

每个帐户还可以指定他们支持哪个版本的帐户抽象协议。这是需要在未来允许对协议进行重大更改。

目前,支持两个版本:None(即,它是一个简单的合约,它永远不应该用作交易的 from 字段)和 Version1

Nonce 排序

帐户还可以向操作员发出信号,表明它应该期望从这些帐户中获取哪个 nonce 排序:SequentialArbitrary

Sequential 表示 nonce 的排序方式应与 EOA 中的排序方式相同。这意味着,例如,操作员将始终等待 nonce 为 X 的交易,然后再处理 nonce 为 X+1 的交易。

Arbitrary 表示 nonce 可以按任意顺序排序。服务器目前支持它,即,如果存在具有任意 nonce 排序的合约,则其交易很可能会因 nonce 不匹配而被拒绝或卡在内存池中。

请注意,这不会以任何方式由系统合约强制执行。可能存在一些健全性检查,但允许帐户做任何他们喜欢的事情。这更多的是向操作员建议如何管理内存池。

返回的 Magic Value

现在,帐户和 paymaster 都需要在验证时返回一个特定的 magic value。在主网上将强制执行此 magic value 的正确性,但在费用估算期间将忽略它。与以太坊不同,签名验证 + 费用收取/nonce 递增不包含在交易的内在成本中。这些费用作为执行的一部分支付,因此需要作为交易成本估算的一部分进行估算。

通常,建议帐户执行尽可能多的正常验证期间的操作,但最终只返回无效的 magic。这将允许正确(或至少尽可能正确)地估算帐户验证的价格。

启动加载器

启动加载器是接受交易数组并执行整个 ZKsync 批次的程序。本节将扩展其不变量和方法。

Playground bootloader vs 已证明的 bootloader

为了方便起见,我们在主网批次和模拟 ethCall 或其他测试活动中使用相同的启动加载器实现。只有 已证明的 启动加载器才用于构建批次,因此本文档仅描述它。

批次的开始

ZKPs 强制执行启动加载器的状态等同于具有空 calldata 的合约交易的状态。唯一的区别是它以预先分配所有可能的内存(以避免内存扩展的成本)开始。

为了提高效率(以及为了我们的方便),启动加载器在其内存中接收其参数。这是非确定性的唯一点:启动加载器 以预先填充了操作员想要的任何数据的内存开始。这就是它负责验证其正确性的原因,并且它不应依赖于内存的初始内容是正确且有效的。

例如,对于每个交易,我们都会检查它是否已正确 ABI 编码,并且交易一个接一个地进行。我们还确保交易不超过允许交易使用的内存空间的限制。

交易类型及其验证

虽然主要交易格式是内部 Transaction format,但它是一个用于表示各种交易类型的结构体。它包含大量 reserved 字段,这些字段可用于取决于未来交易类型,而无需 AA 更改其合约的接口。

交易的确切类型由交易类型的 txType 字段标记。目前支持 6 种类型:

  • txType:0。这意味着此交易属于旧版交易类型。强制执行以下限制:
    • maxFeePerErgs=getMaxPriorityFeePerErg,因为它是 pre-EIP1559 tx 类型。
    • reserved1..reserved4 以及 paymaster 均为 0。paymasterInput 为零。
    • 请注意,与类型 1 和类型 2 交易不同,reserved0 字段可以设置为非零值,表示此旧版交易与 EIP-155 兼容,并且其 RLP 编码(以及签名)应包含系统的 chainId
  • txType:1。这意味着该交易的类型为 1,即交易访问列表。ZKsync 不以任何方式支持访问列表,因此不会提供满足此列表的任何好处。假定访问列表为空。强制执行与类型 0 相同的限制,但 reserved0 也必须为 0。
  • txType:2。它是 EIP1559 交易。应用与类型 1 相同的限制,但现在 maxFeePerErgs 可能不等于 getMaxPriorityFeePerErg
  • txType:113。它是 ZKsync 交易类型。此交易类型旨在支持 AA。适用于此交易类型的唯一限制:字段 reserved0..reserved4 必须等于 0。
  • txType:254。它是一种用于升级 L2 系统的交易类型。这是唯一允许以内核空间中合约的名义启动交易的交易类型。
  • txType:255。它是来自 L1 的交易。对此类交易几乎没有明确施加任何限制,因为启动加载程序在其执行结束时会发送已执行的优先级交易的滚动哈希。L1 合约确保哈希确实与 L1 上优先级交易的哈希匹配。

你还可以在此处阅读有关 L1->L2 交易和升级交易的更多信息。

但是,如前所述,启动加载程序的内存是不确定的,操作员可以自由地将任何其想要的内容放入其中。对于上述所有交易类型,以下限制均强制执行(method),它在开始处理交易之前被调用。

启动加载器内存的结构

启动加载器期望以下内存结构(这里我们用字表示 32 个字节,与 EVM 上的机器字相同):

批次信息

前 8 个字是为操作员提供的批次信息保留的。

  • 0 字 — 操作员的地址(交易的受益人)。
  • 1 字 — 上一个批次的哈希。其验证将在后面解释。
  • 2 字 — 当前批次的时间戳。其验证将在后面解释。
  • 3 字 — 新批次的编号。
  • 4 字 — 公平的 pubdata 价格。有关如何计算我们的 pubdata 的更多信息,请参见此处
  • 5 字 — L2 gas 的“公平”价格,即批次的 baseFee 不应低于的价格。目前,它由操作员提供,但在未来可能会变得硬编码。
  • 6 字 — 操作员期望的批次的基本费用。虽然基本费用是确定的,但它仍然提供给启动加载器,只是为了确保操作员拥有的数据与启动加载器提供的数据一致。
  • 7 字 — 保留字。在已证明的批次中未使用。

批次信息槽在批次开始时使用。一旦读取,这些槽可用于临时数据。

用于调试和事务处理目的的临时数据
  • [8..39] – 用于调试目的的保留槽
  • [40..72] – 用于保存当前交易的 paymaster 上下文数据的槽。paymaster 上下文的作用类似于 EIP4337 的作用。你可以在帐户抽象文档中阅读有关它的更多信息。
  • [73..74] – 当前处理的 L2 交易的签名交易和资源管理器交易哈希的槽。
  • [75..142] – 68 个用于 KnownCodesContract 调用的 calldata 的槽。
  • [143..10142] – 10000 个用于交易退款的槽。
  • [10143..20142] – 10000 个用于交易的批次开销的槽。此开销由操作员建议,即启动加载程序仍将仔细检查操作员是否未向用户收取过高费用。
  • [20143..30142] – 操作员提供的“可信” gas 限制的槽。用户的交易将拥有 min(MAX_TX_GAS(), trustedGasLimit),其中 MAX_TX_GAS 是系统保证的常量。目前,它等于 8000 万 gas。将来,此功能将被删除。
  • [30143..70146] – 用于存储每个交易的 L2 区块信息的槽。你可以在此处阅读有关 L2 区块和批次之间差异的更多信息。
  • [70147..266754] – 用于压缩字节码的槽,每个字节码都采用以下格式:
    • 32 字节字节码哈希
    • 32 个零(但随后启动加载程序会对其进行修改,使其包含 28 个零,然后包含 BytecodeCompressorpublishCompressedBytecode 函数的 4 字节选择器)
    • 到字节码压缩器的 calldata(不带选择器)。
  • [266755..266756] – 存储当前优先级操作的哈希和数量的槽。有关更多信息,请参见优先级操作section
L1Messenger Pubdata
  • [266757..1626756] – 最终批次 pubdata 的提供槽,由 L2DAValidator 验证。

简而言之,此空间用于 L1Messenger 的 publishPubdataAndClearState 函数的 calldata,该函数接受 L2DAValidator 的地址以及要检查的 pubdata。L2DAValidator 是一个负责确保 在处理 pubdata 时提高效率的合约。通常,calldata L2DAValidator 会包括字节码、L2->L1 消息、L2->L1 日志等的未压缩原像请注意,虽然可以在一个批次中发布的实际 加粗 pubdata 数量约为 780kb,但由于 L1Messenger 的 calldata 大小可能要大得多,因为它也接受原始的未压缩状态差异条目。这些数据不会发布到 L1,但会用于验证压缩的正确性。

批次中状态差异数量的“最坏情况”之一是,当 780kb 的 pubdata 用于重复写入时,这些写入都会被清零。在这种情况下,差异数量为 780kb / 5 = 156k。这意味着它们将容纳 42432000 字节的 calldata 用于未压缩的状态差异。在此基础上增加 780kb,我们得到大约需要 43212000 字节的 calldata。需要 1350375 个插槽来容纳这些数据。为了以防万一,我们将其向上舍入到 1360000 个插槽。

但在理论上,可以使用更多的 calldata(例如,如果 1 个字节用于枚举索引)。 运营商有责任确保它可以为 L1Messenger 形成正确的 calldata。

交易的元描述
  • [1626756..1646756] words — 10000 个交易元描述的 20000 个插槽(它们的结构在下面解释)。

出于与引导加载程序内存某些内容相关的零知识证明未来可能集成的内部原因,交易数组不是作为交易数组的 ABI 编码传递的,而是:

  • 我们有一个恒定的最大交易数量。在撰写本文时,这个数字是 10000。
  • 然后,我们有 10000 个交易描述,每个描述都以 ABI 编码为以下结构:
struct BootloaderTxDescription {
  // 存储 ABI 编码的交易数据的偏移量
  uint256 txDataOffset;
  // 交易执行的辅助数据。在我们内部版本的引导加载程序中
  // 它可能有一些特殊的含义,但对于
  // 主网上使用的引导加载程序,它只有一个含义:是否执行
  // 交易。如果为 0,则不应再执行交易。如果为 1,那么
  // 我们应该执行此交易,并可能尝试执行下一个交易。
  uint256 txExecutionMeta;
}
为支付方 postOp 操作的 calldata 保留的插槽
  • [1646756..1646795] words — 40 个插槽,可用于编码支付方的 postOp 方法的调用。

为了避免为帐户抽象的调用额外复制交易,我们保留了一些插槽,这些插槽可以用来为帐户抽象的 postOp 调用形成 calldata,而无需复制整个交易的数据。

实际的交易描述
  • [1646796..1967599]

从 487312 字开始,实际的交易描述开始。(该结构可以通过此 链接 找到)。引导加载程序强制执行:

  • 它们是上面结构正确的 ABI 编码表示。
  • 它们位于内存中,没有任何间隙(第一个交易从第 653 个字开始,每个交易紧随下一个交易之后)。
  • 当前正在处理的交易(以及稍后将处理的交易)的内容不受影响。请注意,我们允许覆盖已处理交易的数据,因为这有助于通过不必每次都需要对帐户进行调用编码时都复制 Transaction 的内容来保持效率。
VM Hook指针
  • [1967600..1967602]

这些是纯粹用于调试目的的内存插槽(当 VM 写入这些插槽时,服务器端可以捕获这些调用,并为调试问题提供重要的见解信息)。

结果 ptr 指针
  • [1967602..1977602]

这些是用于跟踪交易成功状态的内存插槽。如果编号为 i 的交易成功,则插槽 937499 - 10000 + i 将标记为 1,否则为 0。

引导加载程序执行的一般流程
  1. 在批处理开始时,它读取初始批处理信息 并将有关当前批处理的信息发送到 SystemContext 系统合约。
  2. 它遍历每个交易的描述并检查是否设置了 execute 字段。如果未设置,则结束交易的处理并结束批处理的执行。如果 execute 字段非零,则将执行该交易并转到步骤 3。
  3. 根据交易的类型,它决定该交易是 L1 还是 L2 交易,并相应地处理它们。有关 L1 交易处理的更多信息,请参见此处。有关 L2 交易的更多信息,请参见此处

L2 交易

在 ZKsync 上,每个地址都是一个合约。用户可以从他们的 EOA 帐户开始交易,因为每个在其上没有部署任何合约的地址都隐式地包含在 DefaultAccount.sol 文件中定义的代码。每当有人调用不在内核空间内的合约(即地址 ≥ 2^16)并且没有任何合约代码部署在其上时,DefaultAccount 的代码将用作合约的代码。

请注意,如果你调用一个在内核空间内且没有在那里部署任何代码的帐户,那么现在该交易将还原。

我们根据我们的帐户抽象协议处理 L2 交易:https://docs.zksync.io/build/developer-reference/account-abstraction

  1. 我们扣除交易的预付款,用于区块处理的开销。你可以在费用模型说明中阅读有关其工作原理的更多信息。

  2. 然后,我们根据 EIP1559 规则计算这些交易的 gasPrice。

  3. 我们执行 AA 协议的验证步骤

    • 我们计算交易的哈希值。
    • 如果提供了足够的 gas,我们使用 near_call 调用引导加载程序中的验证函数。它将 tx.origin 设置为引导加载程序的地址,并设置 ergsPrice。它还将交易提供的工厂依赖项标记为已标记,然后调用帐户的验证方法并验证返回的 magic。
    • 调用帐户,如果需要,调用支付方以接收交易的付款。请注意,帐户可能不使用 block.baseFee 上下文变量,因此它们无法知道要支付的确切金额。这就是为什么帐户通常首先发送 tx.maxFeePerErg * tx.ergsLimit,并且引导加载程序退还任何发送的超额资金。
  4. 我们执行交易。请注意,如果发送方是 EOA,则 tx.origin 设置为等于交易的 from 值。在交易执行期间,会发布压缩的字节码:对于每个尚未发布的工厂依赖项,并且其哈希值当前指向引导加载程序的压缩字节码区域,则会调用字节码压缩器。此外,在结束时,会对 KnownCodeStorage 进行调用,以确保所有字节码确实已发布。

  5. 我们退还用户在交易中花费的任何超额资金:

    • 首先,对支付方调用 postTransaction 操作。
    • 引导加载程序要求操作员提供退款。在没有证明的第一次 VM 运行时,提供者直接将退款插入到引导加载程序的内存中。在为已证明的批次运行时,操作员已经知道必须在那里插入哪些值。你可以在费用模型的文档中阅读有关它的更多信息。
    • 引导加载程序退还用户的资金。
  6. 我们通知操作员已授予用户的退款。它将用于在资源管理器中正确显示交易的 gasUsed。

L1->L2 交易

L1->L2 交易是在 L1 上启动的交易。我们假设 from 已经授权了 L1→L2 交易。它还在 L1 上设置了其 L1 pubdata 价格以及 ergsPrice。

省略了 L2 交易执行中的大多数步骤,我们将 tx.origin 设置为 from,并将 ergsPrice 设置为交易提供的价格。之后,我们使用 mimicCall 以发送方帐户的名义提供操作本身。

请注意,对于 L1→L2 交易,reserved0 字段表示应在此交易后在 L2 上铸造的 ETH 数量。reserved1 是退款接收者地址,即接收交易退款以及 msg.value 的地址(如果交易失败)。

有两种 L1->L2 交易:

  • 用户发起的优先级操作(它们的类型为 255)。
  • 可以在系统升级期间发起的升级交易(它们的类型为 254)。

你可以在相应的文档中阅读有关这些差异的更多信息。

批处理结束

在批处理结束时,我们将 tx.origintx.gasprice 上下文变量设置为零,以节省 L1 gas 在 calldata 上,并将整个引导加载程序余额发送给操作员,从而有效地将费用发送给他。

此外,我们设置了虚构的 L2 区块数据。然后,我们调用系统上下文以确保它发布 L2 区块以及 L1 批处理的时间戳。我们还重置 txNumberInBlock 计数器,以避免将其状态差异发布到 L1 上。你可以在 此处 阅读有关 ZKsync 上区块处理的更多信息。

之后,我们发布此批处理中的哈希以及优先级操作的数量。有关此的更多信息,请参见此处

然后,我们调用 L1Messenger 系统合约,以便它组成要发布在 L1 上的 pubdata。你可以在 此处 阅读有关 pubdata 处理的更多信息。

系统合约

有关系统合约执行的实现和要求的更多详细信息,可以在其各自代码库的 doc-comments 中找到。本章仅作为此类合同的高级概述。

所有系统合约的代码(包括 DefaultAccount)都是协议的一部分,只能通过 L1 的系统升级进行更改。

SystemContext

此合约用于支持 VM 默认未包含的各种系统参数,即 chainIdoriginergsPriceblockErgsLimitcoinbasedifficultybaseFeeblockhashblock.numberblock.timestamp.

重要的是要注意,genesis 时为此合约运行构造函数,即在 genesis 时显式设置常量上下文值。值得注意的是,如果将来我们想升级合约,我们将通过 ContractDeployer 来完成,因此将运行构造函数。

此合约还负责确保批处理、L2 区块的有效性和一致性。实现本身非常简单,但为了更好地理解此合约,请查看有关 ZKsync 上区块处理的页面

AccountCodeStorage

帐户的代码哈希存储在此合约的存储中。每当 VM 调用地址为 address 的合约时,它都会检索此系统合约的存储槽 address 下的值,如果此值为非零,则将其用作帐户的代码哈希。

每当调用合约时,VM 都会要求操作员提供帐户代码哈希的 preimage。这就是代码哈希的数据可用性至关重要的原因。

构建代码哈希与非构建代码哈希

为了防止合约在其构建期间能够调用合约,我们将标记(即帐户字节码哈希的第二个字节)设置为 1。这样,VM 将确保每当在没有 isConstructor 标志的情况下调用合约时,将替换默认帐户(即 EOA)的字节码,而不是原始字节码。

BootloaderUtilities

此合约包含一些纯粹为引导加载程序功能所需的函数,但为了方便起见,已从引导加载程序本身移出,而不在 Yul 中编写此逻辑。

DefaultAccount

每当合约同时满足以下条件时:

  • 属于内核空间
  • 部署了任何代码(AccountCodeStorage 中相应存储槽下存储的值为零)

将使用默认帐户的代码。此合约的主要目的是为钱包用户和调用它的合约提供类似 EOA 的体验,即它与 Ethereum 上的 EOA 帐户没有什么区别(除了花费的 gas)。

Ecrecover

ecrecover 预编译的实现。它预计会经常使用,因此使用纯 yul 和自定义内存布局编写。

该合约接受与 EVM 预编译格式相同的 calldata,即前 32 个字节是哈希,接下来的 32 个字节是 v,接下来的 32 个字节是 r,最后 32 个字节是 s。

它还使用与 EVM 预编译相同的规则验证输入:

  • v 应为 27 或 28,
  • r 和 s 应小于曲线阶数。

之后,它进行预编译调用,如果调用失败,则返回空字节,否则返回恢复的地址。

空合约

某些合约被认为具有类似 EOA 的行为,即它们可以始终被调用并获得成功的返回值。此类地址的一个示例是 0 地址。我们还要求引导加载程序可调用,以便用户可以将 ETH 转移到该地址。

对于这些合约,我们在 genesis 时插入 EmptyContract 代码。它基本上是一个 noop 代码,不执行任何操作并返回 success=1

SHA256 和 Keccak256

请注意,与 Ethereum 不同,keccak256 是 ZKsync 上的预编译(不是操作码)。

这些系统合约充当其各自加密预编译实现的包装器。预计它们会经常使用,尤其是 keccak256,因为 Solidity 在其帮助下计算映射和动态数组的存储槽。这就是为什么我们使用纯 yul 编写合约来优化短输入情况。过去,sha256keccak256 都在智能合约中执行填充,但现在情况并非如此,sha256 在智能合约中执行填充,而 keccak256 在 zk 电路中执行填充。然后,在 zk 电路中完成两者的哈希处理。

重要的是要注意,sha256 预编译的加密部分希望处理填充数据。这意味着应用填充中的错误可能会导致无法证明的交易。

EcAdd 和 EcMul

这些预编译模拟 EVM 的 EcAdd 和 EcMul 预编译的行为,并且完全在 Yul 中实现,没有电路对应部分。你可以在 此处 阅读有关它们的更多信息。

L2BaseToken 和 MsgValueSimulator

与 Ethereum 不同,zkEVM 没有任何特殊原生Token的概念。这就是为什么我们必须通过两个合约 L2BaseTokenMsgValueSimulator 模拟使用原生Token(收取费用)的操作。

L2BaseToken 是一个为用户保存原生Token余额的合约。此合约不提供 ERC20 接口。转移原生Token的唯一方法是 transferFromTo。它仅允许某些系统合约代表用户进行转移。这是为了确保接口尽可能接近 Ethereum,即转移原生Token的唯一方法是通过调用具有某些 msg.value 的合约。这就是 MsgValueSimulator 系统合约的用途。

每当有人想要进行非零值调用时,他们需要使用以下内容调用 MsgValueSimulator

  • 调用的 calldata 等于原始 calldata。
  • 在第一个 extra abi params 中传递 value 以及调用是否应标记为 isSystem
  • 在第二个 extraAbiParam 中传递被调用者的地址。

有关 extraAbiParams 的更多信息,请参见 此处

.send/.transfer 的支持

在 Ethereum 上,每当完成非零值调用时,都会从调用者的帧中收取一些额外的 gas,并向被调用者的帧提供 2300 gas 津贴。此津贴通常足以发出一个小事件,但强制规定不能在这些 2300 gas 中更改存储。这也意味着在实践中,一些用户可能会选择提供 0 gas 来进行 call,并依赖于传递给被调用者的 2300 津贴。.call/.transfer 就是这种情况。

虽然通常不建议使用 .send/.transfer,但作为迈向更好 EVM 兼容性的一步,自 vm1.5.0 以来,ZKsync Era 中存在对这些函数的部分支持。它是通过以下方式完成的:

  • 每当调用 MsgValueSimulator 系统合约时,都会从调用者的帧中扣除 27000 gas,并在用户最初提供的任何 gas 之上将其传递给 MsgValueSimulator。选择该数字是为了支付转移余额以及 MsgValueSimulator 的其他固定大小操作的执行费用。请注意,由于实际上将是 MsgValueSimulator 的帧调用被调用者,因此常量还必须包括取消提交被调用者代码的成本。解码任何大小的字节码都将非常昂贵,因此我们仅支持大小高达 100000 字节的被调用者。
  • MsgValueSimulator 确保来自上述津贴的不超过 2300 到达被调用者,从而确保这些函数的重入保护不变量成立。

请注意,与 EVM 不同,此类调用的任何未使用 gas 都将退还。

系统保留以下关于 .send/.transfer 的保证:

  • 被调用方收到的 gas 不超过 2300。请注意,可能传递一个较小但接近的数量
  • 不可能在此津贴中进行任何存储更改。这是通过使冷写入的成本高于 2300 gas 来强制执行的。此外,冷写入成本始终必须在执行存储写入时预付。有关它的更多信息,请参见此处
  • 任何字节码大小高达 100000 的被调用者都可以工作。

系统不保证以下内容:

  • 字节码大小大于 100000 的被调用者可以工作。请注意,恶意操作员可能会导致对具有大字节码的被调用者的任何调用失败,即使它以前已取消提交。有关它的更多信息,请参见此处

总而言之,通常应避免使用 .send/.transfer,但如果无法避免,则应与小型被调用者一起使用,例如实现 DefaultAccount 的 EOA。

KnownCodeStorage

此合约用于存储某个代码哈希是否“已知”,即是否可用于部署合约。在 ZKsync 上,L2 存储合约的代码哈希,而不是代码本身。因此,必须成为协议的一部分,以确保永远不会部署具有未知字节码(即具有未知 preimage 的哈希)的合约。

用户为每个交易提供的工厂依赖项字段包含要标记为已知的合约字节码哈希的列表。我们不能简单地相信操作员“知道”这些字节码哈希,因为操作员可能是恶意的并且会隐藏 preimage。我们通过以下方式确保字节码的可用性:

  • 如果交易来自 L1,即其所有工厂依赖项都已发布在 L1 上,我们可以简单地将这些依赖项标记为“已知”。
  • 如果交易来自 L2,即(工厂依赖项尚未在 L1 上发布),我们将使用户支付与字节码长度成比例的 ergs。之后,我们发送带有合约字节码哈希的 L2→L1 日志。L1 合约有责任验证相应的字节码哈希是否已在 L1 上发布。

ContractDeployer 系统合约有责任仅部署那些已知的代码哈希。

KnownCodesStorage 合约还负责确保所有“已知”的字节码哈希也是有效的

ContractDeployer 和 ImmutableSimulator

ContractDeployer 是一个负责在 ZKsync 上部署合约的系统合约。最好通过合约部署在 ZKsync 上的工作方式来理解它的工作原理。与 Ethereum 不同,在 ZKsync 上,create/create2 是操作码,这些操作码由编译器通过调用 ContractDeployer 系统合约来实现。

为了提高安全性,我们还区分了普通合约和帐户的部署。这就是用户将使用的主要方法是 createcreate2createAccountcreate2Account 的原因,这些方法分别模拟 CREATE-like 和 CREATE2-like 行为,用于部署普通合约和帐户合约。

地址派生

每个支持 L1→L2 通信的 rollup 都需要确保 L1 和 L2 上合约的地址在此类通信期间不会重叠(否则 L1 上的一些恶意代理可能会改变 L2 合约的状态)。通常,rollup 通过两种方式解决此问题:

  • 在 L1→L2 通信期间,将某种常量 XOR/ADD 到地址。这就是更接近完整 EVM 等效性的 rollup 解决它的方式,因为它允许它们在 L1 上维护相同的派生规则,但代价是 L1 上的合约帐户必须重新部署在 L2 上。
  • 具有与 Ethereum 不同的派生规则。这是 ZKsync 选择的路径,主要是因为由于我们的字节码与 EVM 上的字节码不同,因此 CREATE2 地址派生在实践中无论如何都会有所不同。

你可以在 ContractDeployer 的 getNewAddressCreate2/ getNewAddressCreate 方法中看到我们地址派生的规则。

请注意,我们仍然在 L1→L2 通信期间将某个常量添加到地址,以便我们将来能够以某种方式支持 EVM 字节码。

部署 nonce

在 Ethereum 上,相同的 nonce 用于帐户和 EOA 钱包的 CREATE。在 ZKsync 上并非如此,我们使用一个单独的 nonce,称为“deploymentNonce”来跟踪帐户的 nonce。这样做主要是为了与自定义帐户保持一致,并在将来具有多重调用功能。

部署的一般过程
  • 在递增部署 nonce 后,合约部署程序必须确保要部署的字节码可用。
  • 之后,它会将带有特殊构建标记的字节码哈希作为要部署合约地址的代码。
  • 然后,如果通过调用传递了任何值,则合约部署程序将其传递给已部署的帐户,并将下一个的 msg.value 设置为等于此值。
  • 然后,它使用 mimic_call 从帐户名称中调用合约的构造函数。
  • 它解析构造函数返回的不可变项数组(我们稍后将更详细地讨论不可变项)。
  • 调用 ImmutableSimulator 以设置要用于已部署合约的不可变项。

请注意它与 EVM 方法的不同之处:在 EVM 上,部署合约时,它会执行 initCode 并返回 deployedCode。在 ZKsync 上,合约只有已部署的代码,并且可以将不可变项设置为构造函数返回的存储变量。

构造函数

在 Ethereum 上,构造函数只是 initCode 的一部分,该 initCode 在合约部署期间执行并返回合约的部署代码。在 ZKsync 上,已部署代码和构造函数代码之间没有分离。构造函数始终是合约部署代码的一部分。为了保护它免受调用,编译器生成的合约仅在提供 isConstructor 标志时才调用构造函数(该标志仅适用于系统合约)。你可以在 此处 阅读有关标志的更多信息。

执行后,构造函数必须返回一个数组:

struct ImmutableData {
  uint256 index;
  bytes32 value;
}

基本上表示传递给合约的不可变项数组。

不可变项

不可变项存储在 ImmutableSimulator 系统合约中。每个不可变项的 index 的定义方式是编译器规范的一部分。此合约仅将其视为每个特定地址从索引到值的映射。

每当合约需要访问某些不可变项的值时,它们都会调用 ImmutableSimulator.getImmutable(getCodeAddress(), index)。请注意,在 ZKsync 上,可以获取当前执行地址(你可以在 此处 阅读有关 getCodeAddress() 的更多信息)。

部署方法的返回值

如果调用成功,则返回已部署合约的地址。如果部署失败,则错误会冒泡。

DefaultAccount

默认帐户抽象的实现。这是默认用于所有不在内核空间中且没有在其上部署合约的地址的代码。该地址:

  • 包含我们的帐户抽象协议的最小实现。请注意,它支持内置的支付方流程
  • 当任何人(引导加载程序除外)调用它时,它的行为与调用 EOA 的行为相同,即对于来自引导加载程序以外的任何人的调用,它始终返回 success = 1, returndatasize = 0

L1Messenger

一种用于从 ZKsync 向 L1 发送任意长度 L2→L1 消息的合约。虽然 ZKsync 本机支持相当有限数量的 L1→L2 日志,这些日志一次只能传输大约 64 字节的数据,但我们允许使用以下技巧发送几乎任意长度的 L2→L1 消息:

L1 messenger 接收消息,对其进行哈希处理,并通过 L2→L1 日志仅发送其哈希以及原始发送者。然后,L1 智能合约有责任确保操作员已在批处理的承诺中提供此哈希的完整 preimage。

请注意,L1Messenger 调用 L2DAValidator 并在促进 DA 验证协议中发挥重要作用。

NonceHolder

用作我们帐户的 nonces 存储。除了使操作员更轻松地对交易进行排序(即通过读取帐户的当前 nonces)之外,它还具有单独的用途:确保对(地址,nonce)始终是唯一的。

它提供了一个函数 validateNonceUsage,引导加载程序使用该函数来检查 nonce 是否已用于某个帐户。引导加载程序强制在交易的验证步骤之前将 nonce 标记为未使用,并在之后标记为已使用。该合约确保一旦标记为已使用,nonce 就不会设置回复到“未使用”状态。

请注意,nonces 不一定必须是单调的(需要这样做才能支持帐户抽象的更有趣的应用,例如可以自行启动交易的协议、类似于 tornado-cash 的协议等)。这就是有两种方法将某个 nonce 设置为“已使用”的原因:

  • 通过递增帐户的 minNonce(从而使所有低于 minNonce 的 nonces 都已使用)。
  • 通过在 nonce 下设置一些非零值通过 setValueUnderNonce。这样,此键将被标记为已使用,并且不再允许将其用作帐户的 nonce。这样也很有效,因为这 32 个字节可用于存储一些有价值的信息。

创建时,帐户还可以提供他们想要的 nonce 排序类型:顺序(即应期望 nonces 逐个增长,就像 EOA 一样)或任意,nonces 可能具有任何值。系统合约不会以任何方式强制执行此排序,但这更多是向操作员建议如何在 mempool 中对交易进行排序。

EventWriter

一个负责发出事件的系统合约。

它接受其第 0 个 extra abi data param 中的主题数。在其余的 extraAbiParams 中,他接受要发出的事件的主题。请注意,实际上,事件的第一个主题包含帐户的地址。一般来说,用户不应直接与此合约交互,而只能通过 emit-ing 新事件的 Solidity 语法来交互。

Compressor

rollup 最昂贵的资源之一是数据可用性,因此为了降低用户的成本,我们通过以下几种方式压缩已发布的 pubdata:

  • 我们压缩已发布的字节码。
  • 我们压缩状态差异。

该合约提供两种方法:

  • publishCompressedBytecode,用于验证字节码压缩的正确性,并以消息的形式将其发布到 DA 层。
  • verifyCompressedStateDiffs,用于验证我们的标准状态差异压缩的正确性。此方法可由常见的 L2DAValidator 使用,例如,它被 RollupL2DAValidator 使用。

你可以在 [此处](https://github.com/matter-labs/era-contracts/blob/cc1619cfb03cc19adb21a2071c89415cab1479e8/docs/l2_system_该合约负责将 pubdata 分成多个块,每个块都适合一个 4844 blob,并计算所述 blob 的 preimage 的哈希值。如果一个块的大小小于 blob 的总字节数,我们会在右侧用零填充它,因为电路会要求该块具有精确的大小。

该合约可以被 L2DAValidator 使用,例如 RollupL2DAValidator 使用它将 pubdata 压缩成 blob。

CodeOracle

它是一个接受 bytecode 的版本化哈希并返回其 preimage 的合约。它类似于 Ethereum 上的 extcodecopy 功能。

它的工作方式如下:

  1. 它接受一个版本化的哈希,并仔细检查它是否被标记为“已知”,即 operator 必须知道该哈希的 preimage。

  2. 之后,它使用 decommit 操作码,该操作码接受版本化的哈希和要花费的 ergs 数量,这与 preimage 的长度成正比。如果 preimage 之前已经 decommit 过,则请求的成本将退还给用户。

    请注意,decommit 过程不仅使用 decommit 操作码发生,而且在调用合约期间也会发生。每当调用合约时,其代码都会 decommit 到专用于合约代码的内存页中。我们永远不会两次 decommit 相同的 preimage,无论它是通过显式操作码 decommit 的,还是在调用另一个合约期间 decommit 的,之前解包的 bytecode 内存页都将被重用。在 CodeOracle 合约中执行 decommit 时,将首先向用户预先收取最大可能的费用,如果 bytecode 之前已经 decommit 过,则会退还该费用。

  3. decommit 操作码返回 decommit 的 bytecode 的切片。请注意,返回的指针始终具有 2^21 字节的长度,而与实际 bytecode 的长度无关。因此,CodeOracle 系统合约的工作是缩小返回数据的长度。

P256Verify

该合约的行为与 RIP-7212 中的 P256Verify 预编译相同。请注意,由于 Era 具有不同的 gas 调度,因此我们不遵守 gas 成本,但除此之外,接口是相同的。

GasBoundCaller

这不是一个系统合约,但它将被预先部署在一个固定的用户空间地址上。该合约允许用户设置一个子调用可以占用的 pubdata 量的上限,而与每个 pubdata 的 gas 无关。有关 ZKsync 上 pubdata 如何工作的更多信息,请点击此处阅读。

请注意,这是一个经过慎重考虑的决定,不将此合约部署在内核空间中,因为它可能会将调用中继到任何合约,因此可能会打破对所有系统合约都可信的假设。

ComplexUpgrader

通常升级是通过调用 FORCE_DEPLOYER 常量地址的 ContractDeployer 的 forceDeployOnAddresses 函数来执行的。但是,某些升级可能需要更复杂的交互,例如,从合约查询某些内容以确定要进行哪些调用等。

对于这种情况,已经创建了 ComplexUpgrader 合约。假设升级的实现已预先部署,并且 ComplexUpgrader 将 delegatecall 到它。

请注意,虽然 ComplexUpgrader 甚至在之前的升级中就已存在,但它缺乏 forceDeployAndUpgrade 函数。这导致了一些严重的限制。有关 gateway 升级过程的更多信息,请点击此处阅读。

Predeployed 合约

有些合约需要预先部署,但拥有内核空间权限是不可取的。此类合约通常从 2^16 开始按顺序地址预先部署。

Create2Factory

只是一个内置的 Create2Factory。它允许在多个链上确定性地将合约部署到相同的地址。

L2GenesisUpgrade

一个负责促进新创建的链的初始化的合约。这是链创建流程的一部分。

与桥接相关的合约

L2BridgehubL2AssetRouterL2NativeTokenVault 以及 L2MessageRoot

这些合约用于促进跨链通信以及价值桥接。你可以在资产路由器规范中阅读有关它们的更多信息。

请注意,L2AssetRouterL2NativeTokenVault 具有唯一的代码,L2Bridgehub 和 L2MessageRoot 与其 L1 预编译共享相同的源代码,即 L2Bridgehub 具有代码,L2MessageRoot 具有代码。

SloadContract

在 L2GatewayUpgrade 期间,系统合约将需要读取一些其他合约的存储,即使这些合约缺少 getter。有关如何实现的信息,请参阅 SystemContractHelper 合约的 forcedSload 函数。

虽然它仅用于升级,但决定将其保留为预先部署的合约,以供将来使用。

L2WrappedBaseTokenImplementation

虽然尚不支持桥接包装的基础代币(例如 WETH)。它的地址被铭刻在原生代币 vault 中(L1 和 L2 )。为了与其他网络保持一致,我们的 WETH 代币被部署为 TransparentUpgradeableProxy。为了使部署过程更容易,我们预先部署了实现。

待解决的已知问题

该协议在概念上是完整的,但包含一些已知问题,这些问题将在短期到中期内得到解决。

  • Fee 建模还有待改进。有关 fee 模型的更多信息,请参见文档
  • 我们可以为内核空间中的合约添加某种默认实现(即,如果被调用,它们不会 revert,而是表现得像 EOA)。
  • 原文链接: github.com/matter-labs/e...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
matter-labs
matter-labs
江湖只有他的大名,没有他的介绍。