对 zksyncEra 官方文档中抽象账户部分按照自己的个人理解进行中文翻译,对于存在理解错误的地方,欢迎指正
ZKsync 和以太坊 EIP 4337 的原生账户抽象旨在增强账户的灵活性和用户体验,但它们在以下关键方面有所不同:
实现层级
EIP-4337
避开了协议级别的实现(众所周知,EIP-4337
期望在应用层实现)。账户类型
paymasters
都是第一类型账户。在底层中,所有的账户(包括 EOA)的行为也和智能合约账户类似(都是合约函数的执行),同时所有的账户都支持使用paymasters
交易处理
EIP-4337
为智能合约账户引入了单独的交易流程:通过依赖于单独的内存池以及Bundlers
节点(能够打包用户操作,并将其发送给EntryPoint
合约 )进行用户的操作,这使得出现了两个独立交易流程(一个是正常以太坊上的交易流程,一个是依靠Bundlers
等建立的交易流程)Bundlers
的角色,对于用户(EOA 和合约账户一样)发起的交易将被发送到Bootloader
(类似于EntryPoint
合约的角色),从而产生一个mempool
与交易流。Paymasters 支持:
EIP-4337
中分为两个部分),允许 EOA 和智能合约账户从paymasters
中受益EIP-4337
中对 EOA 而言并不支持paymasters
,因为paymasters
仅在与智能合约交互中实现。以太坊中有两种类型的账户:
在 ZKsync Era 中实现这样的账户:可以直接发起一笔交易(如 EOA 一般),也能在其中实现一些逻辑(即放入智能合约代码),这种账户称为抽象账户(AA 账户)。
由于用户的账户可以是 AA 账户,所以可以对自己的账户进行编程,然后用户自己调用自己账户上的函数进行交易,因此,用户可以自定义签名算法、多签、支出限制等。同时,用户可以通过paymasters
来帮助交易,即交易的gas
由paymasters
付出(前提是用户在paymasters
有存款或授权一定数量的 ERC20 代币)。这使得用户在区块链上能有更好的体验了。
ZKsync 上的帐户抽象协议与 EIP-4337
非常相似,但为了效率和更好的用户体验,仍然有所不同。
每个区块中,对于每个交易都有一个重要的不变量,唯一的交易哈希值。虽然账户可以接收多个完全相同的交易,但对于抽象账户而言并不容易。尽管这些交易在技术上是合规的,但索引器和其他工具很难处理违反了哈希唯一的情况。
需要有一种的协议级别的方法,来保证交易哈希不重复的方法,最简单又便宜的方法就是让让交易对(发送者、Nonce)始终唯一。
就采用了以下协议:
NonceHolder
(会为每个账户记录使用过的 Nonce)检查 Nonce 是否已被使用used
。用户可以使用一个任意的 256 bit 的数字作为 Nonce,并且可以在系统合约中对应的 key 下放置任意的非零值。但仅有协议层面支持,但在 server 端并不支持这么做,目前仅支持通过顺序使用 Nonce 值(使用incrementMinNonceIfEquals
方法,重新标记能使用的最低 Nonce)。
在未来,计划在 ZKsync 上支持高效的交易包含证明。这需要我们在Bootloader
中计算交易哈希。因为这种计算需要消耗gas
,所以将计算交易哈希交给 AA 方法的接口才公平(防止账户可能由于某些原因需要该值),这也是为何下面描述的IAccount
与IPaymaster
接口的所有的方法都包含交易哈希以及推荐的签名摘要(由 EOA 对本次交易进行签名的摘要)。
这个就是建议每个账户都需要实现的 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 开始一个智能合约的交易,如以太坊上一样)。与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 进行签名
_context
:validateAndPayForPaymasterTransaction
的返回_txHash
:交易本身_txResult
:指示交易是否执行成功_maxRefundedGas
:可以退还给paymaster
的gas
的最大数量上限对于上面的每个方法都会接收 Transaction 结构,虽然大多数字段不言自明,但仍旧有 6 个reserved
字段(在下面结构中貌似被单独命名了出来,并没有如文档所讲放在reserved
中),不命名的原因在于:未来的某些交易类型中可能不需要他们。目前为:
reserved[0]
:是 Noncereserved[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;
}
每笔交易都会有如下流程处理
验证过程就是:系统确定交易是否能够执行。如果这笔交易在任何验证点失败,则不会向用户收取任何费用,并且交易不会包含在区块中(应该是指这笔交易不会有任何的记录,即相当于没有这笔交易)。
验证过程的步骤:
validateTransaction
方法,检查交易是否合规。方法执行成功且没有 revert 则进行下一步费用处理:这个部分分为两种(根据是否使用paymaster
,但结果应该都是向Bootloader
支付费用):
payForTransaction
方法(账户自己付款),当此方法没有 revert ,则继续prepareForPaymaster
,成功执行后调用(应该是由系统调用)paymaster
的validateAndPayForPaymasterTransaction
函数。如果两个函数都没有 revert,则继续Bootloader
至少收到tx.gasPrice * tx.gasLimit
的 ETH。如果所需的资金已经保存了,则交易被视为已经验证,准备进行下一步执行步骤负责执行实际的交易操作,并将任何为使用的gas
退还给用户,即使此步骤中出现 revert 情况,交易人就包含在区块中。
执行过程步骤:
executeTransaction
方法,执行交易paymaster
的交易后处理(仅在涉及paymaster
时适用):调用paymaster
的postTransaction
方法。这个方法通常将未使用的gas
退还给发送者(应该就是当前账户了),尤其是paymaster
采用的是 ERC20 代币作为代付费用(即向当前账户收取 ERC20 ,然后代付gas
)。不同协议之间处理对于交易费的处理不同,如 EIP-4337
和 ZKsync 之间。
EIP-4337
定义了三种Gas Limit
来管理不同交易阶段的成本(应该是指将不同阶段的gas
费用分开统计):
verificationGas
:包含交易验证所需的gas
executionGas
:交易执行所需的gas
preVerificationGas
:在主要验证前所需的gas
(这个不太能理解,但看下面的描述,是转账 ERC20 的费用?)在 ZKsync Era 中,使用单个Gas Limit
字段处理所有与交易相关的成本,所以这个统一的Gas Limit
需要包含:
默认情况下,estimateGas
函数计算需要的gas
量,并包含一个常数。这个常数用于 EOA 交易的支付费用和签名验证(没看懂这句话)。
为了安全起见,NonceHolder
(管理账户 Nonce)和ContractDeployer
(负责部署合约)的系统合约都只能在拥有isSystem
的特殊标志时调用。想要拥有此特殊标志来进行调用,需要使用 SystemContractsCaller 库中的systemCall
或systemCallWithPropagatedRevert
或systemCallWithReturndata
(后面两种内部都是调用了systemCall
)方法。
当开发自定义的账户时,基本上都必定使用这个库,因为这是调用NonceHolder
系统合约的非视图的唯一方法。此外,如果想允许用户自己部署自己的合约,则必须使用这个库。可以使用 EOA 账户的实现(默认账户实现,所有没有包含代码的账户默认继承的)作为参考。
主要是 EIP-4337
对 ZKsync 原生帐户抽象的扩展概述。
为了向 operator 提供 Dos 保护,EIP-4337
对账户的验证步骤施加了多项限制(如不允许访问能够变化的信息,如当前块时间、数字、哈希)。其中大多数,尤其是那些和禁止操作码相关的内容,仍然具有意义,但为了用户有更好的体验,其中的一些限制已经取消
允许使用已部署的call
/delegateCall
/staticcall
合约。和以太坊不同,我们无法编辑已部署的代码或通过自毁删除合约,所以我们可以确保合约执行期间的代码是相同的。
这一部分就没看懂
在原本的EIP
中,AA 的validateTransaction
步骤(检查交易是否合规)只允许读取自己存储的 slots 。但有些 slots 在语义上属于该用户,但实际上位于另一个合约的地址上,例如 ERC20 的余额。
此限制通过确保各个账户用于验证的 slots 不重叠来提供 DDos 安全性,因此他们不需要实际上属于账户的存储空间中。
为了能够在验证步骤中读取用户的 ERC20 余额或授权额度,在验证步骤中地址为 A 的账户将允许使用以下类型的 slots:
keccak256(A || X)
类型的 slots(包阔mapping(address => value)
,通常用于 ERC20 代币中的余额)未来可能允许有时限的交易,例如允许检查block.timestamp <= value
是否返回false
等等。这将需要部署此类可信方法的库,但它将大大增强账户的功能。
想在 zksync 上构建自定义账户(应该就是合约账户,带有合约的抽象账户),开发人员必须实现特定的接口,并遵循推荐的账户部署和管理的最佳实践。
每个自定义账户都应该实现 IAccount interface(这个可以在 DefaultAccount.sol 中找到示例)。
当外部地址调用时,此实现会返回空值,这可能不是您的自定义帐户所希望的行为。
对于智能钱包,强烈鼓励实施EIP-1271
签名验证方案(让合约能够和EOA账户一样能够对消息进行签名,同时可以验证签名)。该标准得到了 ZKsync 团队的认可,并且是签名验证库的组成部分。
部署账户的逻辑与部署标准智能合约的过程类似,但为了区分不打算被视为账户的智能合约,需要使用ContractDeployer
系统合约的createAccount
/create2Account
方法,而不是使用create
/create2
(从代码中看没啥区别,因为create
中调用了createAccount
)。
import { ContractFactory } from "zksync-ethers";
const contractFactory = new ContractFactory(abi, bytecode, initiator, "createAccount");
const aa = await contractFactory.deploy(...args);
await aa.deployed();
目前自定义账户验证规则尚未完全强制执行,将来可能变化:
block.number
)账户必须将 Nonce 加一,以保持哈希冲突的抵抗力
这些限制尚未在 电路/VM 级别强制执行,并且不适用于 L1->L2 事务。
交易和部署的 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
时专门设计用来资助用户交易费用的账户,增强协议的可用性和灵活性,方便用户使用 ERC20 代币而不是 ETH 支付费用。
要使用paymaster
,用户必须在其EIP-712
交易中指定非零的paymaster
地址,并在paymasterInput
字段中指定相关数据。
paymaster
将来可能会停止正常运行为了减少恶意paymaster
潜在的 Dos 攻击,使用了类似于EIP-4337
的声誉评分系统。但与EIP-4337
不同,zksync 中的paymaster
可以与任何存储 slots 交互,并且在特定条件下不会受到限制,如自上次成功验证以来经过的时间或连续的(consistent,可能也可以翻译为一致的) slots 访问模式
paymaster
可以自动操作或需要用户交互,这取决于其设计。例如将 ERC20 代币兑换成 ETH 的paymaster
将要求用户授予必要限额。
账户抽象协议本身是通用的,允许账户和paymaster
实现任意交互。但默认账户(EOA)的代码不变,但人就希望他们能够参与自定义账户和paymaster
的生态中。这就是为什么对交易的paymasterInput
字段进行了标准化,以涵盖大多数常见的paymaster
功能用例。
你的账户可以自由选择实现或不实现这些流程的支持,但强烈建议保持 EOA 和自定义账户的接口相同。
当paymaster
不需要用户执行任何初始操作时,使用此流程:
paymasterInput
字段必须为具有以下接口的函数的调用进行编码:
function general(bytes calldata data);
对于 EOA 账户,此输入通常不起作用,但paymaster
可以根据需求进行说明(不太清楚这个函数具体有什么作用)。
当用户必须为paymaster
设置 token 限额时,此流程十分重要。paymasterInput
字段必须为对具有以下签名的函数的调用进行编码:
function approvalBased(
address _token,
uint256 _minAllowance,
bytes calldata _innerInput
);
EOA 将确保支付给paymaster
的_token
的限额至少设为_minAllowance
。_innerInput
参数是一个额外的有效负载,可以发送给paymaster
,以实现任意逻辑(如可以由paymaster
验证的额外签名或密钥)。
如果正在开发paymaster
,则不应该相信交易的发送者会诚实行事(如通过approvalBased
流程提供所需的限额)。这些流程主要作为对 EOA 的指示,而paymaster
应始终仔细检查这些要求。
为保证用户在测试网上体验到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
)。
由于额外的计算和操作,与paymaster
的交互通常比标准交易消耗更多的gas
,导致gas
增加的主要原因是:
paymaster
的validateAndPayForPaymasterTransaction
和postTransaction
的内部操作。paymaster
将资金发送到bootloader
时消耗的gas
gas
,如果用户使用 ERC20 代币补偿paymaster
,管理代币的限额会消耗额外的gas
。gas
一般是最少的,具体取决于paymaster
的实现gas
的使用量,尤其是用户第一次设置限额。这个过程可能需要发布一个 32 byte 的存储密钥标识符,可能会以 50 gwei 的 L1 gas 价格使用多达 400k 的gas
。需要注意,虽然交易流程在执行结束时,将存储 slot 清空(因此”授予X
限额+paymaster
花费所有限额),但初始成本是在执行期间预先收取的,只有在交易结束时 slot 归零后才会退款给用户。准确的估计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
。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);
}
}
不建议使用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
签名验证。这两种方法都会返回true
或false
,具体取决于消息签名是否正确。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!