参考资料
- https://github.com/4337Mafia/awesome-account-abstraction?tab=readme-ov-file
- https://eips.ethereum.org/EIPS/eip-4337
ERC4337协议
ERC 4337协议
角色
| 术语 | 描述 |
|---|---|
| user | 发送userOperation |
| sender | 发送userOperation的账户合约 |
| bundler | 一个节点(构建区块的),可以处理userOperations。不是很理解,是参与共识构造区块还是? |
| entryPoint | 用于执行 UserOperations 包的单例合约。Bundlers/客户端将支持的entryPoint列入白名单。 |
| Paymaster | 同意为了交易支付的合约。 |
| Aggregator | 用于验证聚合签名。 |
userOperation
-
为了改变以太坊底层共识,使用userOperation用来抽象用户的操作
Field Type Description sender address 发起操作的账户 nonce uint256 防止重放 factory address 账户工厂,仅用于新建账户 factoryData bytes 账户工厂所需的数据,仅当存在账户工厂时) callData bytes 传递给sender在主要执行调用期间的数据 callGasLimit uint256 为主执行调用分配的气体量 verificationGasLimit uint256 为验证步骤分配的气体量 preVerificationGas uint256 支付给bunder的额外气体费用 maxFeePerGas uint256 每单位气体的最大费用(类似于EIP-1559中的max_fee_per_gas) maxPriorityFeePerGas uint256 每单位气体的最大优先费用(类似于EIP-1559中的max_priority_fee_per_gas) paymaster address 付费代理合约的地址,(或为空,如果账户自己支付费用) paymasterVerificationGasLimit uint256 为付费代理验证代码分配的气体量 paymasterPostOpGasLimit uint256 为付费代理后操作代码分配的气体量 paymasterData bytes 付费代理的数据(仅当存在付费代理时) signature bytes 传递到账户中以验证授权的数据
用户将UserOperation对象发送到专用的用户操作内存池。bundlers监听用户操作内存池,并创建bundle交易。bundle交易将多个UserOperation对象打包成一个对预先发布的全局入口点合约的handleOps调用.
为了防止重放攻击,签名应该依赖于chainid和EntryPoint地址。
EntryPoint
| Field | Type | Description |
|---|---|---|
| sender | address | |
| nonce | uint256 | |
| initCode | bytes | 工厂地址和factoryData的连接 |
| callData | bytes | |
| accountGasLimits | bytes32 | |
| preVerificationGas | uint256 | |
| gasFees | bytes32 | |
| paymasterAndData | bytes | |
| signature | bytes |
核心接口如下:
-
handleOps函数:处理由用户操作组成的数组,并指定交易受益人。functionhandleOps(PackedUserOperation[] calldata ops, address payable beneficiary); -
handleAggregatedOps函数:处理由聚合器分组的用户操作数组,并指定交易受益人。function handleAggregatedOps( UserOpsPerAggregator[] calldata opsPerAggregator, address payable beneficiary );
其中,UserOpsPerAggregator 结构体定义如下:
struct UserOpsPerAggregator {
PackedUserOperation[] userOps; // 用户操作数组
IAggregator aggregator; // 聚合器合约接口
bytes signature; // 聚合操作的签名
}
账户合约接口
interface IAccount {
function validateUserOp
(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds)
external returns (uint256 validationData);
}- 一个账户需要实现的核心接口是
IAccount,它主要负责验证用户操作(UserOperation)的有效性,并与入口点(EntryPoint)交互以确保安全执行。关键点如下: - 核心函数:
validateUserOp接受压缩的用户操作、用户操作哈希(不包含签名)、以及账户资金缺口作为参数,返回验证数据。此数据打包了授权者地址、有效截至时间和有效起始时间。 - 验证要求:
- 账户必须验证调用者是信任的入口点。
- 如果不支持签名聚合,账户必须验证签名是否为用户操作哈希的有效签名;签名不符时应返回错误码
SIG_VALIDATION_FAILED而非直接回滚。 - 账户至少需向入口点支付“missingAccountFunds”以补足资金,可根据需要超额支付以备将来交易,并能通过
withdrawTo取回多余金额。
- 时间戳与授权者:
- 返回值中的“authorizer”字段用来标记签名状态(0表示有效签名,1表示签名失败,其他为授权者合约地址)。
- “validUntil”和“validAfter”分别定义了用户操作的有效期上限和下限。
- 签名聚合支持: 支持签名聚合的账户应在
validateUserOp的返回值中提供其签名聚合器地址,并可选择忽略用户操作中的签名字段。
此外,账户还可以选择实现IAccountExecute接口,包含executeUserOp方法。此方法由入口点调用以执行用户操作,替代直接在账户上执行callData,提供了更高层次的抽象和控制。
nonce机制
在以太坊协议中,交易序号(nonce)作为一个递增的序列值,起到防止交易重放和确定交易区块内顺序的作用,同时也保证了交易哈希的独特性,避免相同发送者和相同nonce的交易被重复纳入链中。
然而,单一的顺序nonce限制了发送者在交易排序和重放保护方面实施自定义逻辑的能力。
为解决这一问题,提出了一种新的Nonce机制,该机制在UserOperation中使用一个uint256类型的nonce值,但将其视为两个部分:
- 192位的“密钥”(key)
- 64位的“序列”(sequence)
这两个值在EntryPoint合约中链上表示,并在EntryPoint接口中定义了如下方法来暴露这些值:
functiongetNonce(address sender, uint192 key)externalviewreturns (uint256 nonce);对于每个“密钥”,其对应的“序列”会在每个UserOperation被EntryPoint验证并顺序单调递增。与此同时,用户可以随时引入具有任意初始值的新“密钥”。
这种机制在协议层面保持了UserOperation哈希唯一性的保证,同时允许钱包通过操作192位的“密钥”字段来实现所需的任何自定义逻辑,且这一设计适配了32字节的词长度限制,提高了灵活性和安全性。
entrypoint要求与流程
在处理和验证Nonce的过程中,客户端在准备UserOperation时,可通过视图调用EntryPoint合约上的特定方法来确定nonce字段的有效值。Bundler(打包者)在验证UserOperation时,也需要先调用getNonce方法以确保交易的nonce字段有效。特别地,若Bundler打算在内存池中接纳同一发送者的多笔UserOperation,它应当追踪这些操作已记录的key和sequence对。
在使用模式上,可以通过不同的策略来适应不同的需求场景,如维持经典的递增Nonce机制,或为特定管理操作定义独立的Nonce-key以区分普通与管理事务。
EntryPoint合约的功能要求包括处理两种类型的操作:无需签名聚合器的handleOps和能处理包含多个聚合器操作的handleAggregatedOps。对于handleOps和handleAggregatedOps,其处理流程包括两个主要阶段:验证循环和执行循环。
- 验证循环:创建不存在的账户,基于验证和调用气体限制计算最大可能费用,确保账户在EntryPoint中的存款足以覆盖最大可能成本,并调用账户的validateUserOp方法来验证操作及其签名,支付必要费用。任何验证失败都将导致至少跳过该操作,甚至完全回滚。
- 执行循环:根据UserOperation的calldata调用账户,如果calldata以IAccountExecute.executeUserOp方法签名开始,则需相应地构建并调用calldata。操作完成后,根据实际消耗的气体退还账户的押金,并对退还的气体费用施加一定百分比的惩罚(UNUSED_GAS_PENALTY_PERCENT),以防止过度预留气体空间而影响其他UserOperation的包含。
Bundler在将UserOperation加入内存池之前,应使用模拟验证(simulateValidation)功能来本地验证签名正确性和费用支付情况,确保操作的有效性。未能通过验证的UserOperation应被丢弃,不予加入内存池。

EntryPoint 合约接收 handleOps(userOps[]) 调用,开始处理一系列用户操作。这个设计允许批量处理多个用户操作,提高效率。
在验证阶段,EntryPoint 首先调用 Factory 的 create(initCode) 方法来创建新的 Account。这种方式允许按需创建账户,增加了灵活性。接着,EntryPoint 获取 account 信息,为后续操作做准备。
EntryPoint 然后调用 Account 的 validateUserOp 方法。这一步骤让账户有机会验证操作的有效性,例如检查签名。验证通过后,Account 向 EntryPoint 存入保证金。这个保证金机制确保了账户有足够的资金支付gas费用。
EntryPoint 随后扣除 Account 的保证金,这是为了预付可能的gas费用。整个过程对 Account2 重复,体现了系统处理多个账户操作的能力。
在执行阶段,EntryPoint 调用 Account 的 exec 方法来执行实际操作。这种设计将操作的执行委托给账户合约,增加了系统的灵活性。操作执行后,EntryPoint 退还 Account1 剩余的保证金,体现了精确的费用计算和退款机制。
同样的过程也应用于 Account2,展示了系统能够公平地处理多个账户的操作。
最后,EntryPoint 调用 compensate(beneficiary) 方法,可能是向打包者支付费用。这一步确保了为系统提供服务的参与者得到适当的补偿。
这整个流程体现了账户抽象(Account Abstraction)的核心理念:将复杂的验证和执行逻辑从以太坊核心层移至用户定义的智能合约中。它允许更灵活的账户行为,同时保持了安全性和效率。通过分离验证和执行阶段,系统可以在执行昂贵操作之前拒绝无效的操作,从而节省gas并防止潜在的攻击。保证金机制确保了费用的预付,避免了资源浪费,而最终的补偿步骤则激励了网络参与者的持续贡献。
paymasters 扩展
扩展了入口点逻辑,以支持可以为其他用户赞助交易的支付。此功能可用于允许应用程序开发人员为其用户补贴费用,允许用户使用[ERC-20]代币和许多其他用例支付费用。当UserOp中的paymasterAndData字段不为空时,入口点会为该UserOperation实现不同的业务流程

验证阶段流程:
- EntryPoint 接收 handleOps(userOps[])
- 调用 Account.validateUserOp()
- 调用 Paymaster.validatePaymasterUserOp()
- EntryPoint 扣除 Paymaster 存款
执行阶段流程
- 调用 Account.exec() 执行实际操作
- 调用 Paymaster.postOp() 进行后续处理
- EntryPoint 退还 Paymaster 剩余存款
通过 Paymaster 机制,它提供了灵活的费用支付方式,同时保证了操作的有效性、安全性和经济激励的平衡。
使用签名聚合器
签名聚合器提供了一套接口,允许更高效地处理和验证多笔交易的签名。以下是该接口的定义
【IAggregator 接口】
validateUserOpSignature: 验证单个UserOperation的签名,并返回一个用于打包的替代签名(通常是空的或特定格式的签名),以便于后续的交易捆绑。aggregateSignatures: 聚合一组UserOperation的签名到单一签名值,提高交易效率。validateSignatures: 验证聚合签名是否与数组中所有UserOperations匹配,确保签名的有效性。此方法在链上由handleOps()调用。
【账户与聚合器的互动】
当账户采用签名聚合时,它会在调用validateUserOp时返回聚合器的地址。在模拟验证(simulateValidation)阶段,此聚合器信息作为aggregatorInfo结构的一部分返回给Bundler(打包者),以便进行进一步的验证和处理。
【Bundler 的操作流程】
- 聚合器接受与验证:Bundler首先需要确认聚合器的有效性,包括检查其是否已抵押(staked)且未被限制或禁用。
- 签名验证:为了接受UserOperation,Bundler需调用聚合器的
validateUserOpSignature方法验证签名,并获得一个替代签名,该签名将在打包过程中使用。接着,Bundler需再次调用账户的validateUserOp方法,确保使用返回的替代签名也能得到相同的结果,以验证签名的正确性。 - 签名聚合:Bundler可利用
aggregateSignatures方法将多个UserOperation的签名聚合成单一签名,简化交易处理流程。 - 最终验证:
validateSignatures方法在链上确保所有聚合的签名与提交的UserOperations匹配,任何不匹配的情况都会导致回滚,确保交易安全性。
签名聚合器通过提供一套接口,实现了对多笔交易签名的高效聚合与验证,增强了交易处理的效率与安全性。Bundler在整合UserOperations时,需遵循特定的步骤来验证聚合器的有效性、验证单个交易签名、聚合签名,并最终确保所有签名的有效性,以顺利执行交易捆绑和上链。