zksync Era 官方文档抽象账户部分中文翻译

  • nilliol
  • 更新于 2024-07-19 15:26
  • 阅读 472

对 zksyncEra 官方文档中抽象账户部分按照自己的个人理解进行中文翻译,对于存在理解错误的地方,欢迎指正

zksync Era 中的抽象账户与 EIP-4337 的抽象账户

ZKsync 和以太坊 EIP 4337 的原生账户抽象旨在增强账户的灵活性和用户体验,但它们在以下关键方面有所不同:

  1. 实现层级

    • ZKsync 的账户抽象在协议层级(我的理解是从底层)进行集成;但EIP-4337 避开了协议级别的实现(众所周知,EIP-4337期望在应用层实现)。
  2. 账户类型

    • 在 ZKsync Era中,智能合约账户和paymasters都是第一类型账户。在底层中,所有的账户(包括 EOA)的行为也和智能合约账户类似(都是合约函数的执行),同时所有的账户都支持使用paymasters
  3. 交易处理

    • EIP-4337为智能合约账户引入了单独的交易流程:通过依赖于单独的内存池以及Bundlers节点(能够打包用户操作,并将其发送给EntryPoint合约 )进行用户的操作,这使得出现了两个独立交易流程(一个是正常以太坊上的交易流程,一个是依靠Bundlers等建立的交易流程)
    • ZKsync Era 上存在一个统一的内存池给账户(包括 EOA 和智能合约账户)交易使用。ZKsync Era 中 Operator (定序器)承当了Bundlers的角色,对于用户(EOA 和合约账户一样)发起的交易将被发送到Bootloader(类似于EntryPoint合约的角色),从而产生一个mempool与交易流。
  4. Paymasters 支持:

    • ZKsync Era 通过单一的交易流程(不再像EIP-4337中分为两个部分),允许 EOA 和智能合约账户从paymasters中受益
    • EIP-4337中对 EOA 而言并不支持paymasters,因为paymasters仅在与智能合约交互中实现。

介绍

以太坊中有两种类型的账户:

  • 外部拥有账户(EOA,即用户使用私钥直接控制进行交易的账户)
  • 合约账户(拥有代码的账户,不存在私钥,所以用户不能直接控制进行交易)

在 ZKsync Era 中实现这样的账户:可以直接发起一笔交易(如 EOA 一般),也能在其中实现一些逻辑(即放入智能合约代码),这种账户称为抽象账户(AA 账户)。

由于用户的账户可以是 AA 账户,所以可以对自己的账户进行编程,然后用户自己调用自己账户上的函数进行交易,因此,用户可以自定义签名算法、多签、支出限制等。同时,用户可以通过paymasters来帮助交易,即交易的gaspaymasters付出(前提是用户在paymasters有存款或授权一定数量的 ERC20 代币)。这使得用户在区块链上能有更好的体验了。

设计

ZKsync 上的帐户抽象协议与 EIP-4337非常相似,但为了效率和更好的用户体验,仍然有所不同。

Nonce 的唯一性

  • 当前的模型不允许自定义钱包同时发送多笔交易并保持确定的顺序
  • 对于 EOA,Nonce 的计数会依次增长(每笔交易都会递增);对于自定义账户,无法保证交易顺序
  • 未来计划切换到可以在顺序或任意 Nonce 之间选择的模型

每个区块中,对于每个交易都有一个重要的不变量,唯一的交易哈希值。虽然账户可以接收多个完全相同的交易,但对于抽象账户而言并不容易。尽管这些交易在技术上是合规的,但索引器和其他工具很难处理违反了哈希唯一的情况。

需要有一种的协议级别的方法,来保证交易哈希不重复的方法,最简单又便宜的方法就是让让交易对(发送者、Nonce)始终唯一。

就采用了以下协议:

  • 每个交易开始前,系统会通过NonceHolder(会为每个账户记录使用过的 Nonce)检查 Nonce 是否已被使用
  • 如果 Nonce 尚未使用,则开始对交易的验证(验证交易是否满足执行条件),在验证期间,提供的随机数被标记为used
  • 验证完成后,系统检查 Nonce 是否标记为已使用。

用户可以使用一个任意的 256 bit 的数字作为 Nonce,并且可以在系统合约中对应的 key 下放置任意的非零值。但仅有协议层面支持,但在 server 端并不支持这么做,目前仅支持通过顺序使用 Nonce 值(使用incrementMinNonceIfEquals方法,重新标记能使用的最低 Nonce)。

标准化交易哈希

在未来,计划在 ZKsync 上支持高效的交易包含证明。这需要我们在Bootloader中计算交易哈希。因为这种计算需要消耗gas,所以将计算交易哈希交给 AA 方法的接口才公平(防止账户可能由于某些原因需要该值),这也是为何下面描述的IAccountIPaymaster接口的所有的方法都包含交易哈希以及推荐的签名摘要(由 EOA 对本次交易进行签名的摘要)。

Interface

IAccount interface

这个就是建议每个账户都需要实现的 IAccount interface。它包含五个接口:

个人认为下面的”系统“通常指Bootloader,但又有时候表示更大的概念,即整个 zksync 系统

  • validateTransaction:是必须实现的,系统需要它来确定 AA 逻辑是否同意继续进行交易。当交易不合规(如签名错误),该方法将会revert;如果调用成功,则认为已实现的账户逻辑已同意交易,系统将继续进行交易。
  • executeTransaction:是必须实现的,当用户支付费用(应该就是gas费)后,系统将会调用。该函数会执行交易的实施。
  • payForTransaction:是可选实现的,当交易没有paymaster时,系统将会调用它。这个方法是由当前账户支付交易费用,如果当前账户永远不会支付任何费用,并且始终都会依赖paymaster进行费用支付,那么可以不实现。这个方法将会发送至少tx.gasprice * tx.gasLimit的 ETH 给Bootloader(如果被调用的话)。
  • prepareForPaymaster:是可选实现的,如果交易有paymaster代为支付交易费用,那么系统将会调用它。这个方法用来准备和paymaster进行交互(最著名的示例就是使用 ERC20 让paymaster代为支付交易费用)。
  • executeTransactionFromOutside:是可选实现的,但强烈推荐实现,因为在 priority 模式下的某些情况时(如 operator 没有响应),需要从你那来自”外部“的账户开始交易(即从 EOA 开始一个智能合约的交易,如以太坊上一样)。

IPaymaster interface

EIP-4337相同,ZKsync 同样支持paymaster:用于代付其他账户交易执行费用的账户。每个paymaster都需要实现 IPaymaster interface,其包含以下两个方法:

  • validateAndPayForPaymasterTransaction:是必须实现的,系统需要使用它来确认paymaster是否同意代付这笔交易。如果同意,则这个方法至少发送tx.gasprice * tx.gasLimit的 ETH 给 operator 。它需要返回context来作为postTransaction方法的调用参数之一。
  • postTransaction:是可选实现的,在交易执行后被调用。与EIP-4337不同,并不能保证会调用此方法,尤其是交易因为out of gas错误而失败。其需要四个参数:

    还有一个变量_suggestedSignedHash不知道为啥官方文档里不讲,表示交易哈希,由 EOA 进行签名

    • _contextvalidateAndPayForPaymasterTransaction的返回
    • _txHash:交易本身
    • _txResult:指示交易是否执行成功
    • _maxRefundedGas:可以退还给paymastergas的最大数量上限

Transaction 结构体的保留字段

对于上面的每个方法都会接收 Transaction 结构,虽然大多数字段不言自明,但仍旧有 6 个reserved字段(在下面结构中貌似被单独命名了出来,并没有如文档所讲放在reserved中),不命名的原因在于:未来的某些交易类型中可能不需要他们。目前为:

  • reserved[0]:是 Nonce
  • reserved[1]:是随交易传递的msg.value
struct Transaction {
    // The type of the transaction.
    uint256 txType;
    uint256 from;
    uint256 to;
    uint256 gasLimit;
    uint256 gasPerPubdataByteLimit;
    uint256 maxFeePerGas;
    uint256 maxPriorityFeePerGas;
    uint256 paymaster;
    uint256 nonce;
    uint256 value;
    uint256[4] reserved;
    bytes data;
    bytes signature;
    bytes32[] factoryDeps;
    bytes paymasterInput;
    bytes reservedDynamic;
}

交易流程

每笔交易都会有如下流程处理

验证

验证过程就是:系统确定交易是否能够执行。如果这笔交易在任何验证点失败,则不会向用户收取任何费用,并且交易不会包含在区块中(应该是指这笔交易不会有任何的记录,即相当于没有这笔交易)。

验证过程的步骤:

  1. Nonce 验证:验证交易的 Nonce 是否被使用过
  2. 交易验证:调用账户上的validateTransaction方法,检查交易是否合规。方法执行成功且没有 revert 则进行下一步
  3. 标记 Nonce:验证通过后,标记交易的 Nonce 已经使用
  4. 费用处理:这个部分分为两种(根据是否使用paymaster,但结果应该都是向Bootloader支付费用):

    • Standard Transactions:在账户上使用payForTransaction方法(账户自己付款),当此方法没有 revert ,则继续
    • Paymaster Transactions:首先交易发送者调用prepareForPaymaster,成功执行后调用(应该是由系统调用)paymastervalidateAndPayForPaymasterTransaction函数。如果两个函数都没有 revert,则继续
  5. 资金验证:系统确定Bootloader至少收到tx.gasPrice * tx.gasLimit的 ETH。如果所需的资金已经保存了,则交易被视为已经验证,准备进行下一步

执行

执行步骤负责执行实际的交易操作,并将任何为使用的gas退还给用户,即使此步骤中出现 revert 情况,交易人就包含在区块中。

执行过程步骤:

  1. 交易执行:调用账户上的executeTransaction方法,执行交易
  2. paymaster的交易后处理(仅在涉及paymaster时适用):调用paymasterpostTransaction方法。这个方法通常将未使用的gas退还给发送者(应该就是当前账户了),尤其是paymaster采用的是 ERC20 代币作为代付费用(即向当前账户收取 ERC20 ,然后代付gas)。

费用

不同协议之间处理对于交易费的处理不同,如 EIP-4337 和 ZKsync 之间。

EIP-4337 的 Gas Limit

EIP-4337定义了三种Gas Limit来管理不同交易阶段的成本(应该是指将不同阶段的gas费用分开统计):

  • verificationGas:包含交易验证所需的gas
  • executionGas:交易执行所需的gas
  • preVerificationGas:在主要验证前所需的gas(这个不太能理解,但看下面的描述,是转账 ERC20 的费用?)

ZKsync Era 中统一的 Gas Limit

在 ZKsync Era 中,使用单个Gas Limit字段处理所有与交易相关的成本,所以这个统一的Gas Limit需要包含:

  • 交易验证
  • 代支付逻辑费用(包括任意 ERC20 转账的费用)
  • 交易本身的执行

估算 Gas

默认情况下,estimateGas函数计算需要的gas量,并包含一个常数。这个常数用于 EOA 交易的支付费用和签名验证(没看懂这句话)。

使用 SystemContractsCaller 库

为了安全起见,NonceHolder(管理账户 Nonce)和ContractDeployer(负责部署合约)的系统合约都只能在拥有isSystem的特殊标志时调用。想要拥有此特殊标志来进行调用,需要使用 SystemContractsCaller 库中的systemCallsystemCallWithPropagatedRevertsystemCallWithReturndata(后面两种内部都是调用了systemCall)方法。

当开发自定义的账户时,基本上都必定使用这个库,因为这是调用NonceHolder系统合约的非视图的唯一方法。此外,如果想允许用户自己部署自己的合约,则必须使用这个库。可以使用 EOA 账户的实现(默认账户实现,所有没有包含代码的账户默认继承的)作为参考。

扩展 EIP-4337

主要是 EIP-4337对 ZKsync 原生帐户抽象的扩展概述。

为了向 operator 提供 Dos 保护,EIP-4337对账户的验证步骤施加了多项限制(如不允许访问能够变化的信息,如当前块时间、数字、哈希)。其中大多数,尤其是那些和禁止操作码相关的内容,仍然具有意义,但为了用户有更好的体验,其中的一些限制已经取消

拓展允许的操作码

允许使用已部署的call/delegateCall/staticcall合约。和以太坊不同,我们无法编辑已部署的代码或通过自毁删除合约,所以我们可以确保合约执行期间的代码是相同的。

扩展属于用户的 slots 集

这一部分就没看懂

在原本的EIP中,AA 的validateTransaction步骤(检查交易是否合规)只允许读取自己存储的 slots 。但有些 slots 在语义上属于该用户,但实际上位于另一个合约的地址上,例如 ERC20 的余额。

此限制通过确保各个账户用于验证的 slots 不重叠来提供 DDos 安全性,因此他们不需要实际上属于账户的存储空间中。

为了能够在验证步骤中读取用户的 ERC20 余额或授权额度,在验证步骤中地址为 A 的账户将允许使用以下类型的 slots:

  • 属于地址 A 的 slots
  • 任何其他地址上的 slots A(这个不懂)
  • 然和其他地址上的keccak256(A || X)类型的 slots(包阔mapping(address => value),通常用于 ERC20 代币中的余额)

未来会允许什么?

未来可能允许有时限的交易,例如允许检查block.timestamp <= value是否返回false等等。这将需要部署此类可信方法的库,但它将大大增强账户的功能。

建立智能账户

想在 zksync 上构建自定义账户(应该就是合约账户,带有合约的抽象账户),开发人员必须实现特定的接口,并遵循推荐的账户部署和管理的最佳实践。

接口实现

每个自定义账户都应该实现 IAccount interface(这个可以在 DefaultAccount.sol 中找到示例)。

当外部地址调用时,此实现会返回空值,这可能不是您的自定义帐户所希望的行为。

EIP-1271 集成

对于智能钱包,强烈鼓励实施EIP-1271签名验证方案(让合约能够和EOA账户一样能够对消息进行签名,同时可以验证签名)。该标准得到了 ZKsync 团队的认可,并且是签名验证库的组成部分。

部署流程

部署账户的逻辑与部署标准智能合约的过程类似,但为了区分不打算被视为账户的智能合约,需要使用ContractDeployer系统合约的createAccount/create2Account方法,而不是使用create/create2(从代码中看没啥区别,因为create中调用了createAccount)。

使用 zksync-ethers SDK 的示例 (v5)

import { ContractFactory } from "zksync-ethers";

const contractFactory = new ContractFactory(abi, bytecode, initiator, "createAccount");
const aa = await contractFactory.deploy(...args);
await aa.deployed();

验证步骤限制

目前自定义账户验证规则尚未完全强制执行,将来可能变化:

  • 账户只能与属于他们自己的 slots 进行交互
  • 账户逻辑中禁止使用上下文变量(如block.number
  • 账户必须将 Nonce 加一,以保持哈希冲突的抵抗力

    这些限制尚未在 电路/VM 级别强制执行,并且不适用于 L1->L2 事务。

Nonce 管理

交易和部署的 Nonce 都被整合到NonceHolder中进行优化。使用incrementMinNonceIfEquals函数安全的增加账户的 Nonce。

发送交易

目前仅支持EIP-712(对结构化数据进行哈希和签名的标准)格式的交易从自定义账户发送。交易必须指定from字段作为账户地址,并在customData中包含customSignature

交易提交示例

import { utils } from "zksync-ethers";

// Here, `tx` is a `TransactionRequest` object from `zksync-ethers` SDK.
// `zksyncProvider` is the `Provider` object from `zksync-ethers` SDK connected to the ZKSync network.
tx.from = aaAddress;
tx.customData = {
  ...tx.customData,
  customSignature: aaSignature,
};
const serializedTx = utils.serialize({ ...tx });

const sentTx = await zksyncProvider.sendTransaction(serializedTx);

Paymasters

Paymasters时专门设计用来资助用户交易费用的账户,增强协议的可用性和灵活性,方便用户使用 ERC20 代币而不是 ETH 支付费用。

与 Paymasters 交互

要使用paymaster,用户必须在其EIP-712交易中指定非零的paymaster地址,并在paymasterInput字段中指定相关数据。

Paymasters 验证规则

  • 验证规则尚未完全强制执行
  • 不遵守这些规则的paymaster将来可能会停止正常运行

为了减少恶意paymaster潜在的 Dos 攻击,使用了类似于EIP-4337的声誉评分系统。但与EIP-4337不同,zksync 中的paymaster可以与任何存储 slots 交互,并且在特定条件下不会受到限制,如自上次成功验证以来经过的时间或连续的(consistent,可能也可以翻译为一致的) slots 访问模式

内置 Paymasters 流程

paymaster可以自动操作或需要用户交互,这取决于其设计。例如将 ERC20 代币兑换成 ETH 的paymaster将要求用户授予必要限额。

账户抽象协议本身是通用的,允许账户和paymaster实现任意交互。但默认账户(EOA)的代码不变,但人就希望他们能够参与自定义账户和paymaster的生态中。这就是为什么对交易的paymasterInput字段进行了标准化,以涵盖大多数常见的paymaster功能用例。

你的账户可以自由选择实现或不实现这些流程的支持,但强烈建议保持 EOA 和自定义账户的接口相同。

一般 Paymasters 流程

paymaster不需要用户执行任何初始操作时,使用此流程:

paymasterInput字段必须为具有以下接口的函数的调用进行编码:

function general(bytes calldata data);

对于 EOA 账户,此输入通常不起作用,但paymaster可以根据需求进行说明(不太清楚这个函数具体有什么作用)。

基于授权的 Paymasters 流程

当用户必须为paymaster设置 token 限额时,此流程十分重要。paymasterInput字段必须为对具有以下签名的函数的调用进行编码:

function approvalBased(
    address _token,
    uint256 _minAllowance,
    bytes calldata _innerInput
);

EOA 将确保支付给paymaster_token的限额至少设为_minAllowance_innerInput参数是一个额外的有效负载,可以发送给paymaster,以实现任意逻辑(如可以由paymaster验证的额外签名或密钥)。

如果正在开发paymaster,则不应该相信交易的发送者会诚实行事(如通过approvalBased流程提供所需的限额)。这些流程主要作为对 EOA 的指示,而paymaster应始终仔细检查这些要求。

测试网 Paymasters

为保证用户在测试网上体验到paymaster,同样继续支持使用 ERC20 代币支付费用,Matter Labs 团队提供了测试网paymaster,可以将 ERC20 代币与 ETH 以 1:1 的汇率(1 ERC20 代币等于 1 wei 的 ETH)支付费用。

paymaster仅支持基于授权的paymaster流程,并要求token参数等于正在交换的token,并且minAllowance等于tx.maxFeePerGas * tx.gasLimit。此外测试网paymaster不适用_innerInput参数,因此不应提供任何内容(空bytes)。

与 Paymasters 交互时估算 Gas 费用

由于额外的计算和操作,与paymaster的交互通常比标准交易消耗更多的gas,导致gas增加的主要原因是:

  1. 内部计算:包括paymastervalidateAndPayForPaymasterTransactionpostTransaction的内部操作。
  2. 资金转账:paymaster将资金发送到bootloader时消耗的gas
  3. ERC20 token 限额的管理:是可选项产生的gas,如果用户使用 ERC20 代币补偿paymaster,管理代币的限额会消耗额外的gas
  • 用于内部计算的gas一般是最少的,具体取决于paymaster的实现
  • 转移资金的成本与用户能为类似交易支付的费用相当
  • 管理 ERC20 限额会显著影响gas的使用量,尤其是用户第一次设置限额。这个过程可能需要发布一个 32 byte 的存储密钥标识符,可能会以 50 gwei 的 L1 gas 价格使用多达 400k 的gas。需要注意,虽然交易流程在执行结束时,将存储 slot 清空(因此”授予X限额+paymaster花费所有限额),但初始成本是在执行期间预先收取的,只有在交易结束时 slot 归零后才会退款给用户。

准确估计 Gas 的重要性

准确的估计gas至关重要,尤其是对于涉及大量pubdata的操作,例如写入存储。你应该在估算过程中包含paymasterInput来保证准确考虑了paymaster的参与。以下代码片段来自定义的paymaster教程,演示了如何进行此估算:

const gasLimit = await erc20.estimateGas.mint(wallet.address, 5, {
  customData: {
    gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
    paymasterParams: paymasterParams,
  },
});

此处paymasterParams包括paymaster的地址和他的输入。然而paymasterInput通常包含难以提前预测的参数,例如用户所需的确切的代币数量。

此外,paymaster可能需要验证价格数据或转换率,可能需要服务端的签名。

处理复杂的依赖关系

对于如涉及依赖交易内容的这些签名的这种复杂依赖关系,产生了挑战:

  • validateAndPayForPaymasterTransaction中返回magic = 0可以模拟有效签名验证的gas消耗。这确保了尽管交易会由于magic = 0而在主网上失败,但仍旧可以估计出正确的gas数量。
  • gas的估算本质上时对防止交易失败的最低gas量的二分搜索。如果验证始终失败,那么gas估计也会失败,因为系统将不断尝试增加gas limit

提供授权估算的策略

  1. 粗略估计:如果对于涉及的资金有一个大概的认识,用它来进行估计。由于缓冲已经包含在估计中,所以微小的差异通常不会导致交易失败。但如果用户的余额在估计和交易执行之间发生意外的变化,可能会出现差异
  2. 单独估计限额的设置:或者估算用户为交易的gas单独设置的限额。将此估算添加到原始交易的估算的成本中。这种方法考虑了 Nonce 的更改和一般验证逻辑,但可能会带来明显的开销。

每种方法都有其优点和缺点,选择正确的方法取决于交易的具体情况和paymaster的需求。

签名验证

推荐的签名验证方法。如果项目准备支持原生 AA ,那么强烈建议这么做,这将允许自主的成千上万用户(许多新钱包都默认是智能账户,为用户提供更加流畅的体验)。我们预计未来会有更多用户转向智能钱包。

建立的各种不同类型的帐户之间最显着的区别是不同的签名方案。我们希望帐户支持EIP-1271标准。

@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol库提供了一种验证不同账户实现的签名方法。当需要检查账户签名是否正确时,强烈建议使用这个库。

将库添加到项目中

npm add @openzeppelin/contracts

使用库的示例

pragma solidity ^0.8.0;

import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";

contract TestSignatureChecker {
    using SignatureChecker for address;

    function isValidSignature(
        address _address,
        bytes32 _hash,
        bytes memory _signature
    ) public pure returns (bool) {
        return _address.isValidSignatureNow(_hash, _signature);
    }
}

验证 AA 签名

不建议使用ethers.js库来验证用户签名。官方 SDK 通过utils提供了两种方法来验证账户的签名:

export async function isMessageSignatureCorrect(address: string, message: ethers.Bytes | string, signature: SignatureLike): Promise<boolean>;

export async function isTypedDataSignatureCorrect(
  address: string,
  domain: TypedDataDomain,
  types: Record<string, Array<TypedDataField>>,
  value: Record<string, any>,
  signature: SignatureLike
): Promise<boolean>;

目前这些方法仅支持验证 ECDSA 签名,但很快将支持EIP-1271签名验证。这两种方法都会返回truefalse,具体取决于消息签名是否正确。

参考文章

ZKsync Era Protocol

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
nilliol
nilliol
0xbe3e...29A9
web3 的学习者,寻找实习机会中。 博客地址:https://llwh2333.github.io