写给开发者的 Account Abstraction:集成 Bundler 的注意事项
imToken
2024-05-23 18:32
订阅此专栏
收藏此文章

作者:Jiahui @ imToken Labs

校对:Members at imToken Labs

封面来源:Image by freepik on freepik

目标读者:区块链开发者

背景知识:

  • 了解帐户抽象(Account Abstraction)的概念

  • 了解 ERC-4337 的基本运作机理

  • 熟悉智能合约开发

  • 熟悉 EOA 交易与以太坊节点的交互及其生命周期

⚠️ 本文基于 ERC-4337 的 EntryPoint v0.6.0 撰写。








ERC-4337 中的 Bundler


▶ Bundler 在 ERC-4337 扮演的角色


在 Account Abstraction 中,Account 以智能合约的形式存在,不能主动触发交易。因此,Account 需要 EOA 地址把其 userOp 上链。在 ERC-4337 中,协助 Account 的 userOp 上链的服务被称为 Bundler,在区块链上表现为一个调用 EntryPoint 合约的 EOA 地址


图源:https://docs.stackup.sh/


具体而言,Bundler 通过调用 EntryPoint 合约的批量上链方法 handleOps 来触发 Account 进行 userOp 的验证和执行。



  • ops 为上链 bundle,含多个 userOp。

  • beneficiary 为手续费收集地址,一般为 Bundler 所拥有。


▶ Bundler 模拟执行


虽然 userOp 的有效性会在链上由 EntryPoint 合约校验,但是 Bundler 在上链前仍需对 userOp 模拟执行以保证上链 userOp 能够通过 EntryPoint 的校验。否则,Bundler 的上链交易可能因 bundle 内的 userOp 验证失败而 revert。


此时,Bundler 只能独自承担 revert 交易的手续费(被白嫖),无法如合法 bundle 般通过 EntryPoint 从 Account 上收取手续费。



因此,Bundler 的模拟执行具有以下特点:


  1. 只需关心 userOp 的验证阶段,以保证 Bundler 能够收取相应手续费。

  2. 无需关心 userOp 的执行阶段。


不同于 EOA 帐户,Account Abstraction 帐户的验证逻辑是自定义的。自定义的灵活性带来了链下验证和链上验证不一致的问题,根源在于链下链上验证依赖的状态不一致。


下面以验证逻辑使用 block.baseFee 的 Account 为例:



  1. Bundler 在当前区块 N 的状态下进行模拟验证 userOp:

    1. 区块 N 对应的 baseFee 是 29 gwei。

    2. 通过 Account 自定义验证逻辑 validateUserOp 中对 block.basefee < 30 gwei 的约束。

    3. Bundler 模拟验证 userOp 成功。


  2. Bundler 模拟验证 userOp 成功后,在区块 N+1 完成 userOp 上链:

    1. 区块 N 对应的 baseFee 是 31 gwei

    2. 无法通过 Account 自定义验证逻辑 validateUserOp 中对 block.basefee < 30 gwei 的约束

    3. Bundler 提交的 bundle revert,Bundler 无法通过 EntryPoint 收取 Account 的手续费,只能独自承担 revert 交易的手续费


因此,ERC-4337 在协议设计时,会对 Account 的自定义验证逻辑进行约束,保证 userOp 的验证尽可能如 EOA 般的无状态。由于 ERC-4337 的验证规则太多,下面只对 Account 开发者容易违背的部分验证规则进行说明。


▶ 禁用字节码


下面是 userOp 在验证阶段禁用的字节码,原因类似于上面例子中提及的 BASEFEE。



  Storage Access 规则


ASSOCIATED STORAGE


在深入 Storage Access 规则前,先引入 Associated Storage 的概念。


地址 A 的 Associated Storage 指的是:


  • 非 A 合约中的 slot A

  • 非 A 合约中的 slot keccak256(A || X) + n,即访问 mapping(address => value/struct)

    • 主要是覆盖访问地址 A 在 ERC-20 合约中余额的场景

Associated Storage 是比地址 A 中 slot 的一种更宽松的扩展,是为了保证 ERC-4337 可用性的一种权衡。


质押(STAKE)


在深入 Storage Access 规则前,先引入 ERC-4337 中质押的概念。ERC-4337 的质押不同于以太坊共识的质押。以太坊共识的质押资金会有被 slash 的可能。然而,ERC-4337 的质押只会锁定资金,并在 unstakeDelaySec 后可以提取。其目的主要为了防止 Bundler 受到恶意 account / factory / paymaster 的 sybil-attack,提高攻击者的成本。


Bundler 会标记为质押金额大于等于 MIN_STAKE_VALUE = $1000 以及提款延迟大于等于 MIN_UNSTAKE_DELAY = 1 day 的 account / factory / paymaster 为质押实体。


Bundler 会对质押实体有着更宽松的 Storage Access 规则。


ACCOUNT 在 USEROP 验证阶段 STORAGE ACCESS 规则


Account 已部署的场景:



Account 使用 initCode 通过 Factory 部署的场景:



PAYMASTER 在 USEROP 验证阶段 STORAGE ACCESS 规则


Account 已部署的场景:



Account 使用 initCode 通过 Factory 部署的场景:



FACTORY 在 USEROP 验证阶段 STORAGE ACCESS 规则


Account 使用 initCode 通过 Factory 部署的场景:



为什么 FACTORY / PAYMASTER 需要 STAKE 才能访问自身及其相关 STORAGE?


原因是 factory / paymaster 可能会被多个 userOp 使用。因此,factory / paymaster 做恶,可以同时使得多个 userOp 无效。


为了防止这种情况,Bundler 要求 factory / paymaster 质押。一旦做恶,Bundler 可以禁用这些 factory / paymaster,而 factory / paymaster 无法立即换马甲(需要质押)。


⚠️ 注意:上面提及的只是 userOp 的验证规则,而非 userOp 的执行规则。


最后,我们可以发现 ERC-4337 的验证规则到达字节码级别的粒度,因此 Bundler 的模拟验证无法通过调用节点提供的 eth_call 等类似接口实现,而是需要调用的 debug_traceCall 接口追踪验证流程。然而,一般的节点服务商是不提供 / 提供不完整 debug_traceCall 接口,无法满足 Bundler 的需求。


因此,搭建 Bundler 服务门槛不低,通常需要依赖于提供 debug_traceCall 接口的自建以太坊节点。


Bundler 下的 userOp 生命周期


客户端一般通过 SDK 接入 Bundler 处理 userOp 相关工作流。Bundler 一般作为 Provider 接入 SDK。



Bundler 是一个独特的 Provider


  • 一般需要提供 ETH 节点需要提供的常规接口,如 eth_call

  • 需要提供 ERC-4337 规范的接口


下面会详细描述如何通过与 Bundler 提供的接口走完 userOp 的生命周期,并与普通的 EOA 交易进行对比。



注 1:getSenderAddress():

https://github.com/eth-infinitism/account-abstraction/blob/abff2aca61a8f0934e533d0d352978055fddbd96/contracts/interfaces/IEntryPoint.sol#L187

注 2:getNonce():

https://github.com/eth-infinitism/account-abstraction/blob/abff2aca61a8f0934e533d0d352978055fddbd96/contracts/interfaces/INonceManagersol#L15

注 3:UserOperationEvent():

https://github.com/eth-infinitism/account-abstraction/blob/abff2aca61a8f0934e533d0d352978055fddbd96/contracts/interfaces/IEntryPoint.sol#L29


下面会描述一些协助接口:



开发者注意事项


▶ 如何在区块链浏览器查询 userOp 的状态?


当前区块链浏览器是基于 Tx 的,而非基于 userOp 的。因此,需要通过 Bundle Tx 查询 userOp 的状态。


一笔典型的 bundle 交易如下:



更多 bundle 交易的详情可查阅:https://polygonscan.com/tx/0xba1c0dbf1c3a27f6a1e3f72e9ec6c646566d7e821787c5504821f23534a009f6


  • bundle 交易是由 Bundler 调用 EntryPoint 的 handleOps 方法的 EOA 交易。

  • 在 Transaction Action 处会标识出 bundle 内 userOp 的信息。


其中有一个很容易触犯的错误,就是基于 Status 判断 userOp 是否执行成功。可以观察下面一笔交易:



  • 若基于 Status 判断 userOp 成功,则会出错。Status 为 Success 只表明 bundle 交易成功,并非表明内部的 userOp 执行成功。实际上,此 bundle 内的 userOp 是执行失败的。


  • 可以注意里面的黄字 Although one or more Error Occurred [execution reverted] Contract Execution Completed,代表是有一个下级合约调用 revert 掉了,但上层合约没有跟着 revert。实际指的就是 userOp 执行 revert 了,但 EntryPoint 的执行没有随之 revert。


若要准确地了解 userOp 的状态需要通过 EntryPoint 特定的 UserOperationEvent 事件进行查询。



  • 可以看到 bundle 交易中 userOpHash 的执行实际上是失败的。(success: Fasle)。


上面的查询方法有点复杂而且不直觉,当前市面上也有以 userOp 为基础的浏览器,如 Jiffyscan。上面一笔执行失败的 userOp 在 Jiffyscan 就很直观:



  如何从 Account / Paymaster 获取错误信息?


对于合约开发者而言,会在 Account / Paymaster 在验证逻辑进行一些校验,验证失败会抛出一些错误。合约开发者可以通过这些错误进行问题定位。


抛出错误一般有 Custom Error 和 String Error 的形式,在 Solidity 的范畴下,Custom Error 是更优于 String Error 的。


然而,在 ERC-4337 的范畴下,String Error 似乎是更好的选择。


EntryPoint 的 Account 验证逻辑如下:



EntryPoint 的 Paymaster 验证逻辑如下:



可以发现,若验证逻辑抛出的 error 非 String Error,EntryPoint 无法识别只会抛出信息量不足的 reverted (or OOG)。


因此,若合约开发者希望能够获得 Account / Paymaster 的验证错误信息,那么合约开发者需要使用 String Error,而非 Custom Error。


 不直觉违反 ERC-4337 验证规则


对于 ERC-4337 开发者而言,时常会发现 userOp 在 foundry 上运作正常,但是一旦提交至 Bundler,Bundler 会报错。


通常这种情况是因为 userOp 相应的验证逻辑违反了 ERC-4337 验证规则。


这些问题有时会十分隐晦,不直觉,难以定位。下面介绍三个这样的案例。


案例一:OpenZeppelin SafeTransferFrom


考虑 ERC-20 Paymaster 的场景,一般 Paymaster 在验证阶段需要从 Account 划扣 ERC-20 预付款。对于智能合约开发者,自然选择 OpenZeppelin 的 SafeTransaferFrom。


然而,这个常规的选择违反了 ERC-4337 验证规则。因为,SafeTransaferFrom 使用了禁用字节码 SELFBALANCE。这里有另外一层反直觉:SafeTransaferFrom 的确用了 SELFBALANCE,使得问题难以定位。


SafeTransaferFrom 实际用到了 functionCallWithValue,从而使用了 SELFBALANCE。


图源:https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol#L83-L89


此处的解决方法有两种:


  1. 等待 OZ 的 PR 被合入。

  2. 使用 Solady 的 SafeTransferFrom。


这里有一个重要的经验:小心第三方库。


案例二:USDC Paymaster


在 ERC-20 Paymaster 开发的过程中,我们可能会发现一些很奇怪的现象:Paymaster 接入 USDT 一切正常,而接入 USDC 就会报错。


这是因为 USDC 是 Proxy 合约。具体使用时需要读取相应的 implementation slot,而该操作违反了 ERC-4337 验证规则(只能访问 Account / Paymaster 相关的 slot)。


这里有一个重要的经验:小心 Proxy 合约。


案例三:Token Bound Account


Token Bound Account (TBA)让 NFT 拥有相应的 Account,是 Account Abstraction 的一个探索方向。然而,ERC-4337 框架下实际上很难实现 TBA。


因为 TBA 需要基于 tokenId 获取相应 owner 进行鉴权。然而,该操作违反了 ERC-4337 验证规则(只能访问 Account 相关的 slot)。


▶ 容易忽略的 ERC-4337 规则


ERC-4337 有些规则散落在规范 / 实现的各处。这些规则很容易被忽略,导致不合预期的结果。


  • MAX_MEMPOOL_USEROPS_PER_SENDER 参数,在 Bundler 中被设置为 4。

    • 同一个非质押 sender 不能把超过 MAX_MEMPOOL_USEROPS_PER_SENDER 个 userOp 发送到 Bundler 的 userOp。该规则实际上也是为了防止 sender 很容易地 revert 多个 userOp。

    • 质押 sender 不受 MAX_MEMPOOL_USEROPS_PER_SENDER 约束。

  • 如果 validatePaymasterUserOp 返回的 context 非空,则 Paymaster 也应该质押。

    • context 会作为参数被 postOp 使用。非空 context 意味着 postOp 的执行。同时,若 postOp revert,会 revert 整个 bundle。因此,context 非空需要 Paymaster 质押。


图源:https://github.com/eth-infinitism/account-abstraction/blob/abff2aca61a8f0934e533d0d352978055fddbd96/contracts/interfaces/IPaymaster.sol#L36-L37


  • Bundler 自身会维护一个本地的信誉系统

    • Bundler 会追踪所有质押 Entity (Paymaster, Factory, ..)的信誉。

    • opsSeen 为使用 Entity 的 userOp 进入 mempool 的数量

    • opsInclued 为使用 Entity 的 userOp 成功上链的数量

    • opsSeen 与 opsInclued 相差越多,代表该 Entity 的信誉越差

    • 另外,opsSeen 和 opsInclued 每小时会进行更新,赋予最新的 userOp 更高的权重。

      • opsSeen[entity] -= opsSeen[entity] // 24

      • opsIncluded[entity] -= opsIncluded[entity] // 24

  • 信誉系统分为三个档次

    • OK:无限制

    • THROTTLED:使用该 Entity 的 userOp 只能在 bundle 中出现一次

    • BANNED:Bundler 会拒绝所有使用该 Entity 的 userOp


图源:https://github.com/ethereum/ercs/blob/master/ERCS/erc-4337.md#specification-2


▶ GasLimit 估算


一般而言,Bundler 利用 EntryPoint 的 simulateHandleOp 为 userOp 估算 verificationGasLimit 和 callGasLimit。


图源:https://github.com/eth-infinitism/account-abstraction/blob/abff2aca61a8f0934e533d0d352978055fddbd96/contracts/core/EntryPoint.sol#L188


  • simulateHandleOp 用于模拟 userOp 整个处理流程(验证 & 执行)

  • 最后通过 error ExecutionResult 把模拟过程中收集的信息返回。例如,

    • preOpGas 指的是验证阶段消耗的 gas,含 preVerificationGas 以及 verificationGasLimit

    • paid 指的是执行阶段消耗的手续费,可以推导出 callGasLimit


Dummy Signature


SDK 或钱包需要在调用 Bundler 的 eth_estimateUserOperationGas 时提供 Account 的 dummy signature。


dummy signature 的存在是因为用户无法在得到估算 gasLimit 前对 userOp 进行签名。透过 dummy signature,userOp 可以执行一次验证阶段的操作,得到验证阶段消耗的 Gas 作为估算的 verificationGasLimit。


dummy signature 需要满足以下要求:


  • 拥有 dummy signature 的 userOp 不能在验证阶段发生 revert。否则,无法返回 ExecutionResult,并提取其中的 GasLimit。

  • 拥有 dummy signature 的 userOp 只能在验证阶段返回验签失败的 validationData,毕竟 dummy signature 不是真正的签名。值得注意的是,simulateHandleOp 不会对 validationData 的值进行校验。然而,在实际验证阶段,会对 validationData 进行校验。


⚠️ 若 Paymaster 在验证阶段需要验证签名,Paymaster dummy signature 也需要被提供。


Gas 设置


Bundler 在利用 EntryPoint 的 simulateHandleOp 为 userOp 估算时,会把传入的 userOp 的 verificationGasLimit 和 callGasLimit 设置得足够大(一般是 1e6),保证能够模拟整个 userOp 的验证和执行,最终通过 ExecutionResult 得到估算的 verificationGasLimit (通过 preOpGas-preVerificationGas 得到)和 callGasLimit(通过 paid / gasprice 得到)。


  • 由于 verificationGasLimit 和 callGasLimit 设置得足够大,所以 maxFeePerGas 要足够小,保证在 simulateHandleOp 的过程中,Account 和 Paymaster 的余额足够。

  • 然而,如果把 maxFeePerGas 设为 0,会导致 paid 为 0,无法得出 callGasLimit。


综上,Bundler 在模拟前会把 verificationGasLimit 和 callGasLimit 设置为 1e6,会把 maxFeePerGas 设置为 1。


理论上,开发者集成 Bundler 时无需考虑上述细节。但是,由于 Bundler 的实现目前仍不成熟,其不一定把请求 userOp 的 gas 相关字段设置成正确的值。因此,开发者以防万一可以把请求 userOp 的 gas 相关字段设置好再请求 Bundler 的 eth_estimateUserOperationGas 接口。


Bundler 间兼容性


当前 ERC-4337 的规范不包含 Gas Limit 估算的方式,因此各种 Bundler 实现有着不同的 Gas Estimation 实现。


因此,下面的场景可能会出现:


  1. userOp 使用 Bundler A 估算的 GasLimit。

  2. userOp 的 GasLimit 被 Bundler B 所拒绝。


  Gas Price 设置


Priority Fee


前文提到,在一些链上,我们不能直接使用 eth_maxPriorityFeePerGas 接口的返回值设置 userOp 的 maxPriorityFee。


在 Arbitrum 的场景中,eth_maxPriorityFeePerGas 只会返回 0。


  • 为了保障 Bundler 的利益,一般会把 userOp 的 maxPriorityFee 设置成其 baseFee 的固定比例(如 5%)。


在 Optimism 的场景中, eth_maxPriorityFeePerGas 只应用于 L2 成本。


  • userOp 的 maxPriorityFee 会应用于 L1 & L2 成本,直接使用 Optimism 返回的 PriorityFee 会导致用户超额支付。

  • 为了保障 Bundler 的利益,一般会把 userOp 的 maxPriorityFee 设置成其 baseFee 的固定比例(如 5%)。


Alchemy 为用户抽象了这些细节,直接给用户提供另外的接口 rundler_maxPriorityFeePerGas。


Buffer


直接用接口返回的 baseFee 和 priorityFee 一般很难保证 userOp 后续会被打包。一般而言,需要另外增加 buffer。


buffer 的取值在不同的链以及不同的 bundler 下会不同。下面为 Alchemy Bundler 的建议:


图源:https://docs.alchemy.com/reference/bundler-api-fee-logic


实际上,这块的设计仍未被好好审视,一些 Bundler 甚至会亏损,即 Revenue 是负值。


图源:https://dune.com/niftytable/account-abstraction


Bundler 的未来


  P2P Bundler Mempool


当前 Bundler 的 mempool 是私有的,具有中心化以及审查的风险。一旦接受 userOp 的 Bundler 宕机下线,userOp 会丢失。P2P Bundler Mempool 是当前 Bundler 演进的方向,即 Bundler 间组建 P2P 网络,userOp 会被广播到 P2P 网络中。



目前,P2P Bundler Mempool 已经是 ready 的状态。


Gas Price Estimation


一旦 P2P mempool 运作后,会出现一个 Bundler 费用市场。基于 Bundler 费用市场用户可以通过历史费用推测其 userOp 应该设置的 priorityFee,而非现在静态计算 priorityFee 的方式。


历史费用的 priorityFee 估算方式需要 Bundler 提供类似于 eth_feeHistory 的接口,供用户进行查询。


Gas Estimation


当前 ERC-4337 的规范不包含 Gas Limit 估算的方式,因此各种 Bundler 实现有着不同的 Gas Estimation 实现。


图源:https://x.com/ch4r10t33r/status/1724116675983765915?s=20


Gas Estimation 的不一致会影响 P2P Bundler Mempool 的运作。例如,Alchemy 估算的 PVG 会被其他 Bundler 所拒绝。该问题已被意识并处于讨论阶段。


 Alternative Mempool


实践上,有很多应用无法遵循 ERC-4337 规则,如 USCD Paymaster。考虑到这种需求,Bundler 应该需要支持 Alternative Mempool。Alternative Mempool 指的是为 userOp 应用不同于 Main mempool 的规则,而 Main mempool 遵循的是 ERC-4337 规则。例如,Bundler 可以支持白名单 USDC 合约的 Alternative mempool 以满足 USDC Paymaster 的需求。



Alternative mempool 相对于 Main mempool 更容易被攻击(如 DDOS),同时遵循一定的信任假设。另外,Alternative mempool 会导致中心化的问题。以 USDC Paymaster 为例,Alternative mempool 可能只支持 Bundler 合作方的 USDC Paymaster,而不支持其他 USDC Paymaster,即使实现是一致的。


▶ Aggregator


虽然 EntryPoint 合约预留了 Aggregator 的空间,但是 Bundler 仍未支持 Aggregator。Aggregator 可以聚合 bundle 中多个 userOp 的签名,带来一下好处:


  1. 节省 bundle 的 calldata。

  2. 只需要检验聚合签名,无需依次检验 bundle 中所有 userOp 的签名。


一个重要的应用场景是把 BLS 签名应用在 L2 的 ERC-4337 交易中。值得注意的是,把 BLS 签名应用在 L1 的 ERC-4337 交易中并不是一个特别好的主意。


L1 中验证 ECDSA 签名的成本远低于验证 BLS 签名的成本。一般而言,验证一次 BLS 签名相对验证多次 ECDSA 签名仍是不划算的。然而,L2 的计算成本很低,而 calldata 相对成本较高。因此,L2 是应用 BLS Aggregator 的好场景。



END


想了解更多区块链技术、工具和数字资产信息

请关注我们

点击在看分享我们

【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。

imToken
数据请求中
查看更多

推荐专栏

数据请求中
在 App 打开