本文深入探讨了ERC-4337账户抽象的技术细节,详细解析了智能合约账户、UserOperation、EntryPoint、Bundler、Paymaster等关键组件,以及它们如何协同工作以实现更灵活和用户友好的以太坊交易体验。文章还通过步骤分解,阐述了UserOperation从创建到链上执行的完整流程。
在本系列文章的第一部分中,我们从表面上讨论了账户抽象,涵盖了它解决的问题以及为什么它是以太坊社区的一个重要里程碑。现在,是时候深入技术细节,探索 ERC-4337 的各个组成部分以及它们是如何连接的。
这是账户抽象系列的第二篇文章,在本文中,我们将深入技术层面,深入研究 ERC-4337 标准。我们将探索其架构,并将不同的链下和链上组件之间的点连接起来,以了解是什么使这个强大的系统得以工作。具体来说,我们将研究 智能合约账户、UserOperation、EntryPoint、Bundler、Paymaster 等,以了解它们在幕后是如何协同工作的。
显示 ERC-4337 组件及其主要职责的表格
(对于那些想要跟进源代码的人,可以在 GitHub 上找到 ERC-4337 标准的实现: eth-infinitism/account-abstraction )
ERC-4337 最有趣的事情之一是它的模块化设计。可以把它想象成一个组织良好的工具包,每个工具都有非常具体的工作。这种结构允许难以置信的灵活性和安全性,而无需更改以太坊的核心规则。
ERC-4337 的核心是 智能合约账户 (SCA)。这是用户的可编程钱包。与由单个私钥控制的传统 EOA 不同,SCA 由其自己的代码控制。这是从网络中抽象控制权并将其直接置于开发人员和用户手中的基本构建块。
关键区别在于 SCA 可以包含自定义逻辑。可以将其想象成从基本的钥匙和锁保险箱(EOA)升级到高科技可编程金库。这个金库可以有自己的规则,例如需要多个钥匙、时间延迟,甚至在打开之前进行生物识别扫描。
为了实现这一点,创建这些钱包的开发人员必须实现 IAccount
接口中的 validateUserOp
函数。此函数是帐户的安全卫士。
// 智能合约账户的核心接口
interface IAccount {
function validateUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external returns (uint256 validationData);
}
eth-infinitism/account-abstraction 的实现试图尽可能地保持简单,因此 account 实现 需要 EOA 所有者,并在其 validateUserOp
函数中使用 ECDSA 签名。更高级的实现将涉及更高级的身份验证方法,如 Passkey/WebAuthn。
EntryPoint 合约(我们稍后会介绍)调用 validateUserOp
来检查交易请求是否真实,主要通过验证其签名和 nonce。
对于交易的实际执行,EntryPoint 只是将 callData
发送到智能账户。
可选地,账户可以实现 IAccountExecute 接口,但许多账户只是有它们自己的 execute
或 executeBatch
函数,userOperation 的 callData
以它们为目标。
那么,这些智能账户最初是如何创建的呢?那是 账户工厂合约 的工作。
当用户想要第一次使用钱包时,他们的账户尚未在链上存在。工厂负责部署它。此过程由用户第一个 UserOperation 中的 initCode
字段触发,从而创建钱包并在一个无缝的步骤中执行其第一个交易。
为了在用户智能合约地址部署到链上之前使其可预测,工厂使用 CREATE2
操作码。这允许在部署 之前 计算钱包的地址。这就像被分配一个邮政信箱号码,即使在安装实际的信箱之前,你也可以开始在那里接收邮件。
为了安全起见,工厂必须确保只有官方的 EntryPoint 才能调用其 createAccount
函数。
// 工厂 createAccount 函数的简化示例
function createAccount(address owner, uint256 salt) public returns (SimpleAccount ret) {
// 只有受信任的 EntryPoint 的创建者才能调用此函数
require(msg.sender == address(senderCreator), "only callable from SenderCreator");
// 获取确定性地址
address addr = getAddress(owner, salt);
// 如果尚未部署账户,则创建它
if (addr.code.length == 0) {
ret = SimpleAccount(payable(new ERC1967Proxy{salt : bytes32(salt)}(
address(accountImplementation),
abi.encodeCall(SimpleAccount.initialize, (owner))
)));
} else {
// 否则,只需返回现有地址
return SimpleAccount(payable(addr));
}
}
UserOperation 是 ERC-4337 的核心指令表。它不是真正的以太坊交易,而是一个巧妙地打包用户意图的“伪交易”对象。
为什么我们需要这种特殊的结构?因为标准的以太坊交易太死板了。为了启用强大的功能,如 gas 赞助、自定义安全规则和即时钱包创建,我们需要携带更多的信息。UserOperation 专门为此而设计,包含系统处理来自智能账户的请求所需的一切。
struct UserOperation {
address sender; // 用户的智能账户。
uint256 nonce; // 账户的反重放 nonce。
bytes initCode; // 用于通过工厂创建账户的代码(如果需要)。
bytes callData; // 要执行的操作(例如,在链上对智能合约的函数调用)。
uint256 callGasLimit; // 执行阶段的 gas 限制。
uint256 verificationGasLimit; // 验证阶段的 gas 限制。
uint256 preVerificationGas; // 用于补偿 Bundler 开销的 gas。
uint256 maxFeePerGas; // 每个 gas 的最大费用(如 EIP-1559)。
uint256 maxPriorityFeePerGas; // 最大优先级费用(如 EIP-1559)。
bytes paymasterAndData; // 可选:用于 gas 赞助。
bytes signature; // 授权操作的签名。
}
虽然 maxFeePerGas
和 maxPriorityFeePerGas
与传统交易中的相同,但 callGasLimit
、verificationGasLimit
和 preVerificationGas
需要使用 bundler 的 RPC 端点进行估计
preVerificationGas
是分配给 Bundler 的 gas,用于补偿在实际 UserOperation 链上验证开始之前产生的开销。
verificationGasLimit
和 callGasLimit
的分离至关重要。它确保 Bundler 即使在执行部分稍后失败,也能因验证操作而获得报酬。这可以保护他们免受拒绝服务 (DoS) 攻击。
EntryPoint 是单个、全局且受信任的智能合约,它充当整个 ERC-4337 系统的中央协调器。可以将其视为指挥所有 UserOperation 的控制器。
这种“单例”设计是一种有意的权衡。它创建了一个信任的中心点,简化了在标准上构建的每个人的逻辑。但是,这也意味着 EntryPoint 必须非常安全且经过良好审计,因为任何缺陷都可能影响整个生态系统。
当 Bundler 提交一批操作时,EntryPoint 的 handleOps
函数会启动一个两阶段的过程:
validateUserOp
,以验证其签名、nonce 和支付 gas 的能力。callData
分派到目标智能账户来执行它。Bundler 是 ERC-4337 的链下快递服务。它们是专门的节点,用于监视专用的“alt-mempool”中传入的 UserOperation。
Bundler 的工作是:
UserOperation
。Bundler 受到经济因素的驱动,通常优先考虑提供最高费用的操作。开发人员使用 RPC 方法(如 eth_sendUserOperation
)与它们交互。
// 向 Bundler 发送 UserOperation 的概念示例
async function sendUserOperation(userOp, bundlerProvider) {
try {
// Bundler 公开一个 RPC 方法来接受 UserOperation
const userOpHash = await bundlerProvider.send("eth_sendUserOperation", [\
userOp,\
entryPointAddress\
]);
console.log("UserOperation sent. Hash:", userOpHash);
return userOpHash;
} catch (error) {
console.error("Error sending UserOperation:", error);
}
}
如果用户没有 ETH 来支付 gas 费用,或者 dApp 想要代表其用户支付 gas 费用怎么办?Paymaster 在这里发挥作用。Paymaster 是 ERC-4337 的一个可选组件,它可以同意代表用户赞助 gas 费用。
这开启了强大的用例:
通常,paymaster 是链下服务器和链上智能合约的组合。对于想要使用 gas 赞助的用户/dApp,他们必须先与服务器交互以获取将在 paymasterAndData
字段中设置的数据。然后将地址和获取的数据设置在其 UserOperation 的 paymasterAndData
字段中。然后 EntryPoint 调用 Paymaster 的 validatePaymasterUserOp
函数以确认赞助。
// 简化的 Paymaster 合约
contract SimplePaymaster is IPaymaster {
address public immutable entryPoint;
// ... 构造函数和其他函数 ...
function validatePaymasterUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 maxCost
) external override returns (bytes memory context, uint256 validationData) {
// 确保调用者是受信任的 EntryPoint
require(msg.sender == entryPoint, "paymaster: not EntryPoint");
// 确保 paymaster 在 EntryPoint 中存入了足够的 ETH
require(entryPoint.getDeposit(address(this)) >= maxCost, "paymaster: insufficient deposit");
// 自定义赞助逻辑在这里!
// 例如,检查特定的 NFT,或验证链下签名。
return ("", 0);
}
function postOp(...) external override {
// 交易后的逻辑,例如,从用户那里扣除 ERC-20 token。
}
}
Aggregator 是可选的帮助器合约,可以提高签名验证的效率,特别是对于使用高级方案(如 BLS 签名)的钱包。Aggregator 可以从多个 UserOperation 中获取签名,将它们组合成一个,并在链上验证单个聚合签名,从而节省大量的 gas。
这是一个 Aggregator
接口:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "./PackedUserOperation.sol";
/**
* 聚合签名验证器。
*/
interface IAggregator {
/**
* 验证聚合签名。
* 如果聚合签名与给定的操作列表不匹配,则恢复。
* @param userOps - 用于验证签名的 UserOperation 数组。
* @param signature - 聚合签名。
*/
function validateSignatures(
PackedUserOperation[] calldata userOps,
bytes calldata signature
) external;
/**
* 验证单个 userOp 的签名。
* 在 EntryPointSimulation.simulateValidation() 返回此账户使用的 aggregator 之后,bundler 应该调用此方法。
* 首先,它验证 userOp 上的签名。然后,它返回用于创建 handleOps 的数据。
* @param userOp - 从用户收到的 userOperation。
* @return sigForUserOp - 调用 handleOps 时要放入 userOp 的签名字段中的值。
* (通常为空,除非账户和 aggregator 支持某种“多重签名”。
*/
function validateUserOpSignature(
PackedUserOperation calldata userOp
) external view returns (bytes memory sigForUserOp);
/**
* 将多个签名聚合为单个值。
* 此方法在链下调用,以计算与 handleOps() 一起传递的签名
* bundler 可以使用优化的自定义代码来执行此聚合。
* @param userOps - 用于从其中收集签名的 UserOperation 数组。
* @return aggregatedSignature - 聚合签名。
*/
function aggregateSignatures(
PackedUserOperation[] calldata userOps
) external view returns (bytes memory aggregatedSignature);
}
bundler 会调用链下的 aggregator.aggregateSignatures()
来聚合 userOperation 签名
如果使用了 aggregator,bundler 会调用 handleAggregatedOps
而不是 handleOps
如果在 UserOperation
中指定了 Aggregator
,则在验证循环期间,EntryPoint
会调用 aggregator.validateSignatures()
。
现在让我们把它们联系起来。这是一个单一 UserOperation 的旅程,从其创建到链上执行。
eth_sendUserOperation
) 将此 UserOperation 发送到备用内存池。EntryPointSimulations
) 安全地运行 simulateValidation
并确认操作有效,并且可以在不冒任何实际资金风险的情况下支付自己的 gas 费用。handleOps
函数,并从自己的 EOA 中支付 gas 费用。validateUserOp
,独立地重新验证捆绑包中的 每个 UserOperation。这是至关重要的链上安全检查。callData
分派到其目标智能账户。callData
并执行预期的逻辑 - 交换 token、铸造 NFT 等。ERC-4337 代表着工程设计的杰作,从根本上重新定义了用户与以太坊交互的方式,而无需进行破坏性的硬分叉。通过创建用于交易处理的并行链下系统,该标准抽象了 Web3 中最痛苦的部分:私钥焦虑、严格的 gas 支付以及缺乏恢复机制。
这种模块化架构,及其智能账户、Bundler 和 Paymaster 的独特角色,提供了构建下一代用户友好 dApp 所需的灵活和安全的基础。
为了更好地理解这个 ERC,在下一篇文章中,我们将通过构建一个使用账户抽象的 dApp 来进行实践。
我希望你觉得这篇文章有帮助!不要忘记点赞并与他人分享。如果你有任何问题或想更多地讨论这个话题,请随时在下面留言。
你可以随时在 X 上与我联系:@0xAdek
- 原文链接: blog.blockmagnates.com/a...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!