ERC 4337:无需改动以太坊协议的账户抽象

ERC 4337:无需改动以太坊协议的账户抽象
账户抽象(Account abstraction)长期以来都是以太坊开发者社区的梦想。它的理念是:EVM 代码不只用来实现应用逻辑,也用来实现单个用户钱包的验证逻辑(nonce、签名……)。这会为钱包设计打开创造力的大门,带来一些重要特性:
-
多签与社交恢复
-
更高效、更简单的签名算法(如 Schnorr、BLS)
-
抗量子的签名算法(如 Lamport、Winternitz)
-
可升级性
这些事今天用智能合约钱包其实就能做到,但以太坊协议本身要求一切都必须打包进一笔由 ECDSA 保护的外部账户(EOA)发起的交易,这让事情变得非常困难。每一个用户操作都得被一笔来自 EOA 的交易包裹,凭空多出 21000 gas 的开销。用户要么得在一个单独的 EOA 里备着 ETH 来付 gas、同时管理两个账户的余额,要么依赖中继系统——而后者通常是中心化的。
EIP 2938 是修复此问题的一条路径:它引入若干以太坊协议改动,允许顶层交易从合约而非 EOA 发起,合约本身带有矿工会检查的验证与付费逻辑。但这需要在协议开发者正全力投入合并(the merge)与扩容之际,对协议做出重大改动。在我们的新提案(ERC 4337)里,我们给出了一种无需改动共识层、就能获得同样收益的方法。
这个提案如何运作?
我们不去修改共识层本身的逻辑,而是在一个更高层的系统里复刻交易内存池(mempool)的功能。用户发送 UserOperation 对象,把用户意图连同签名和其他验证数据一起打包。矿工或使用 Flashbots 等服务的打包者(bundler),可以把一组 UserOperation 打包成单独的一笔"打包交易(bundle transaction)",再被纳入以太坊区块。

打包者用 ETH 支付这笔打包交易的费用,并通过各个 UserOperation 执行时支付的费用获得补偿。打包者会依据与现有交易内存池中矿工类似的费用优先级逻辑,来选择纳入哪些 UserOperation。UserOperation 看起来像一笔交易,是一个经 ABI 编码的结构体,包含如下字段:
-
sender:发起该操作的钱包
-
nonce 和 signature:传入钱包验证函数的参数,让钱包能验证一个操作
-
initCode:若钱包尚不存在,用来创建钱包的初始化代码
-
callData:实际执行步骤中要用什么数据来调用钱包
其余字段与 gas 和费用管理有关;完整字段列表见 ERC 4337 规范。
钱包是一个智能合约,必须具备两个函数:
-
validateUserOp:以一个 UserOperation 为输入。该函数负责验证 UserOperation 上的签名和 nonce;验证成功则支付费用并递增 nonce,验证失败则抛出异常。
-
一个操作执行函数:把 calldata 解释为钱包要执行的指令。它如何解释 calldata、据此做什么,完全是开放式的;但我们预期最常见的行为是把 calldata 解析为让钱包发起一次或多次调用的指令。
为了简化钱包逻辑,确保安全所需的大量复杂智能合约技巧并不放在钱包本身,而是放在一个名为**入口点(entry point)**的全局合约里。validateUserOp 和执行函数都应被 require(msg.sender == ENTRY_POINT) 守卫,使得只有受信任的入口点才能让钱包执行任何动作或支付费用。入口点只有在某个携带相应 calldata 的 UserOperation 的 validateUserOp 已经成功之后,才会对钱包发起任意调用,因此这足以保护钱包免受攻击。若钱包尚不存在,入口点还负责用提供的 initCode 创建它。

运行 handleOps 时入口点的控制流
内存池节点和打包者需要对 validateUserOp 能做什么施加一些限制:尤其是,validateUserOp 的执行不能读写其他合约的存储,不能使用 TIMESTAMP 等环境操作码,也不能调用其他合约——除非那些合约可被证明不可能自毁(self-destruct)。这样才能确保打包者和 UserOperation 内存池节点为验证某个 UserOperation 是否可纳入/可转发而做的模拟执行,在它真正被纳入未来区块时会产生相同的效果。
如果一个 UserOperation 的验证已被成功模拟,那么在 sender 账户发生其他内部状态变更之前,该 UserOperation 都能保证可被纳入(状态变更可能来自同一 sender 的另一个 UserOperation,或另一个合约调入该 sender;无论哪种,针对单个账户触发这一条件都需在链上花费 7500+ gas)。此外,UserOperation 会为 validateUserOp 步骤指定一个 gas 上限,内存池节点和打包者会拒绝那些上限不够小(如超过 200000)的操作。这些限制复刻了现有以太坊交易那些让内存池免受 DoS 攻击的关键属性。打包者和内存池节点可以用类似当今以太坊交易处理的逻辑,来决定是否纳入或转发一个 UserOperation。
与常规以太坊交易内存池相比,这套设计新增、保留与牺牲了哪些属性?
保留的属性:
-
没有中心化角色;一切都通过点对点内存池完成
-
DoS 安全(通过模拟检查的 UserOperation 保证可被纳入,直到 sender 发生另一次状态变更——这要求攻击者为每个 sender 花费 7500+ gas)
-
用户侧无需钱包初始化的复杂性:用户无需关心其钱包合约是否"已发布";钱包存在于确定性的 CREATE2 地址上,若钱包尚不存在,第一个 UserOperation 会自动创建它
-
完整支持 EIP 1559,包括费用设置的简便(用户可设一个固定的费用溢价和一个较高的最大总费用,并期望被快速纳入、被公平收费)
-
支持按费用替换(replace-by-fee):发送一个溢价显著高于旧操作的新 UserOperation,以替换该操作或让它更快被纳入
新增的好处:
-
验证逻辑的灵活性:validateUserOp 函数可加入任意的签名与 nonce 验证逻辑(新签名方案、多签……)
-
足以让执行层抗量子:若此提案被普遍采用,执行层无需为抗量子再做额外工作。用户可各自把钱包升级为抗量子版本。连包裹交易也是安全的,因为矿工可为每笔打包交易使用一个全新创建、因而受哈希保护的 EOA,并在交易被纳入区块前不公开它。
-
钱包可升级:钱包验证逻辑可以是有状态的,因此钱包可更换其公钥,或(若以 DELEGATECALL 发布)完全升级其代码。
-
执行逻辑的灵活性:钱包可为执行步骤加入自定义逻辑,例如实现原子化的多操作(这是 EIP 3074 的关键目标)
弱点:
-
DoS 脆弱性略有增加:尽管协议已尽力,但仅因为验证逻辑被允许比"单次 ECDSA 验证"这一现状稍复杂一些。
-
gas 开销:比常规交易多一些 gas 开销(不过在某些用例中可由多操作支持来弥补)。
-
一次只能一笔交易:账户无法把多笔交易排队送进内存池。但原子化多操作的能力,让这一特性的必要性大大降低。
用 paymaster 实现代付
代付交易有若干关键用例,最常被提到的两个是:
-
让应用开发者替其用户支付费用
-
让用户用 ERC20 代币支付费用,由一个合约充当中介收取 ERC20 并以 ETH 付费
本提案可通过内置的 paymaster 机制支持这一功能。一个 UserOperation 可把另一个地址设为它的 paymaster。若设置了 paymaster(即非零),在验证步骤中,入口点也会调用 paymaster,以确认它愿意为该 UserOperation 付费。若愿意,则费用从 paymaster 质押在入口点内的 ETH 中扣除(出于安全设有提款延迟),而非从钱包扣除。在执行步骤中,钱包照常以 UserOperation 里的 calldata 被调用,但之后会以 postOp 调用 paymaster。
上述两个用例的示例流程:
-
paymaster 验证 paymasterData 中是否包含来自赞助方的签名,以确认赞助方愿意为该 UserOperation 付费。若签名有效,paymaster 接受,该 UserOperation 的费用从赞助方质押中支付。
-
paymaster 验证 sender 钱包是否有足够的 ERC20 余额来支付该 UserOperation。若有,paymaster 接受并支付 ETH 费用,然后在 postOp 中收取 ERC20 代币作为补偿(若 postOp 因 UserOperation 耗尽 ERC20 余额而失败,执行将回滚、postOp 会被再次调用,因此 paymaster 总能拿到补偿)。注意目前这仅在该 ERC20 是由 paymaster 自己管理的包装代币时才可行。
特别注意第二个用例中,paymaster 可以是纯被动的,或许只偶尔做做再平衡和参数重设。相比以往那些要求 paymaster 始终在线、主动包裹每一笔交易的代付方案,这是一个巨大的改进。
这个提案进展到哪了?
ERC 4337 见此处,一个实现正在此处推进。预计很快会有一个早期开发者 alpha 版本,之后的下一步将是敲定最终细节、并进行审计以确认方案的安全性。
开发者们应该很快就能开始试验账户抽象钱包了!

