对 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.valuestruct 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:包含交易验证所需的gasexecutionGas:交易执行所需的gaspreVerificationGas:在主要验证前所需的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时消耗的gasgas,如果用户使用 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,具体取决于消息签名是否正确。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!