这篇文章深入探讨了 ERC-4337 提出的账户抽象技术,重点介绍了它的工作原理、主要组成部分以及如何解决用户操作的处理问题。文章详细阐述了 Bundler 组件及其与其他组件之间的交互,以及如何在以太坊生态系统中实现更好的用户体验。
账户抽象多年来已成为以太坊开发者社区中极为渴望的特性,许多人将其视为加速加密货币大规模采用的前提条件。一个新的框架将为高级钱包设计铺平道路,支持例如多重签名、后量子安全的签名方法和钱包可升级性等功能,最终将用户体验从web2应用带入web3,同时不削弱安全性。
为了解决这些挑战,ERC-4337:使用替代内存池的账户抽象由Vitalik Buterin及其他人提出。这个ERC引入了一种新的工作流程,在更高的系统层模拟交易内存池的功能,通过“UserOperation”对象封装意图和验证数据。矿工或“打包者”然后将这些操作整合成“打包交易”,以包括在以太坊块中,覆盖交易成本,并从各个UserOperation费用中获得偿还。
本文的目的是深入探讨ERC-4337:使用替代内存池的账户抽象的主要组成部分,解决常见问题、误解和安全考虑。该系列的第一部分将涵盖打包者及其在常用工作流中的交互。
强烈建议读者熟悉账户抽象的基本概念或ERC-4337草案。
ERC-4337的核心工作流最好通过下面的图片概述,该图片摘自提案草案。如我们所见,主要组件通过不同的功能和过程进行交互:打包者、入口点、钱包合约和可选的支付者。
打包者是负责监听UserOperations、打包并将其作为常规交易发送到以太坊节点的组件。这意味着想要接受并从UserOperations中获利的验证者也必须运行新的打包者软件或扩展版的以太坊客户端。为了简化这一过程,一些基础设施公司,例如Alchemy和Stackup,提供“打包者即服务”的解决方案,让dapp开发者不必担心这个问题。
如上所示,当用户想要使用这种“新方式”与区块链互动时,他们会将UserOperation对象发送到打包者,后者将其保存在与以太坊节点不同的内存池中(因此标题为“替代内存池”),然后作为“打包交易”包含在区块中(这仅仅是常规以太坊交易的另一种名称)。
在此流程中,打包者负责发送和支付以太坊交易,并通过入口点合约的流获得偿还。入口点负责保证钱包合约有足够的以太币以支付UserOperation,或者支付者有足够的存款。
现在让我们看看该ERC的参考实现,以理解这一切如何结合在一起。
bundler.config.json
是一个配置文件,操作员可以在其中设置与打包器执行相关的所有属性。除了某些特定于打包器的配置外,它还指定了entryPoint
和beneficiary
地址。第一个是必要的,因为入口点预期随着时间的推移会有新版本进行更新(正如它已经有过的,从第一个主网版本v0.4到当前的v0.6)。第二个地址是将因为提供打包UserOperations服务而收到费用的帐户。通常(但不一定)这是调用入口点以处理 UserOps数组的交易签名者。
在BundlerServer
模块内部,handleMethod
方法负责处理RCP调用,包括debug_
方法。例如,我们可以看到,eth_sendUserOperation
方法接收两个参数,一个UserOperation和一个入口点地址,这些参数在内部被处理。
packages/bundler/src/BundlerServer.ts – Medium
asynchandleMethod(method: string,params: any\[\]): Promise<any>{
letresult: any
switch(method){
case'eth\_supportedEntryPoints':
result=awaitthis.methodHandler.getSupportedEntryPoints()
break
case'eth\_sendUserOperation':
result=awaitthis.methodHandler.sendUserOperation(params\[0\],params\[1\])
break
case'eth\_estimateUserOperationGas':
result=awaitthis.methodHandler.estimateUserOperationGas(params\[0\],params\[1\])
break
case'eth\_getUserOperationReceipt':
result=awaitthis.methodHandler.getUserOperationReceipt(params\[0\])
break
case'eth\_getUserOperationByHash':
result=awaitthis.methodHandler.getUserOperationByHash(params\[0\])
break
// ...
default:
thrownewRpcError(\`Method ${method} is not supported\`,-32601)
}
returnresult
}
查看原文 BundlerServer.ts 托管 ❤ 由 GitHub
MempoolManager
模块的addUserOp
方法负责将用户操作添加到_替代内存池_中,这是待处理的UserOperations的数组。我们可以看到,可以用不同的gas替换先前发送的UserOp。例如,还实施了额外逻辑以阻止同一用户的高数量操作,以防止拒绝服务攻击。
packages/bundler/src/modules/MempoolManager.ts – Medium
// 将userOp添加到内存池中,经过初步验证。
// 如果存在(并且新的gas更高),则替换现有的
// 如果无法将UserOp添加到内存池则回滚(此发件人的UserOps过多)
addUserOp(userOp: UserOperation,userOpHash: string,prefund: BigNumberish,senderInfo: StakeInfo,referencedContracts: ReferencedCodeHashes,aggregator?: string): void{
constentry: MempoolEntry={
userOp,
userOpHash,
prefund,
referencedContracts,
aggregator
}
constindex=this._findBySenderNonce(userOp.sender,userOp.nonce)
if(index!==-1){
constoldEntry=this.mempool[index]
this.checkReplaceUserOp(oldEntry,entry)
debug('replace userOp',userOp.sender,userOp.nonce)
this.mempool[index]=entry
} else{
debug('add userOp',userOp.sender,userOp.nonce)
this.entryCount[userOp.sender]=(this.entryCount[userOp.sender]??0)+1
this.checkSenderCountInMempool(userOp,senderInfo)
this.mempool.push(entry)
}
this.updateSeenStatus(aggregator,userOp)
}
查看原文 MempoolManager.ts 托管 ❤ 由 GitHub
cUserOperation内存池的高层逻辑
BundleManager
模块的createBundle
方法负责创建将发送到入口点合约的UserOperations列表。
这是施加ERC所建立的存储限制的地方,通过ValidationManager
模块的validateUserOp
方法,并在“声誉系统”部分验证支付者的存款。
基于仅智能合约的账户抽象系统面临的一大挑战是确保对DoS攻击的保护。让区块构建者执行整个操作可能会使系统暴露于DoS攻击,因为恶意行为者可以发送看似支付费用的操作,但在执行后会回滚。为了防止这种攻击阻塞内存池,节点必须验证操作是否能支付其费用,然后才能转发。此外,UserOperations被模拟以确保它们有效并能够支付自己的执行费用,并且在链上执行时也会保持这种情况。验证不允许访问在模拟和执行期间可能会更改的任何信息,例如当前区块时间、块号、哈希等,并且只允许访问与此发送者地址有关的数据,以便多个访问同一存储的UserOperations不会因一次状态更改而全部无效。
packages/bundler/src/modules/BundleManager.ts – Medium
asynccreateBundle(): Promise<[UserOperation[],StorageMap]>{
constentries=this.mempoolManager.getSortedForInclusion()
constbundle: UserOperation[]=[]
// 支付者存款应足够支付打包中的所有UserOps。
constpaymasterDeposit: {[paymaster: string]: BigNumber}={}
// 限制的支付者和部署者每个打包只允许少量UserOps。
conststakedEntityCount: {[addr: string]: number}={}
// 每个发送者每个打包只能允许一次
constsenders=newSet<string>()
// 所有已知的有效发送者实体在内存池中
constknownSenders=entries.map(it=>{
returnit.userOp.sender.toLowerCase()
})
conststorageMap: StorageMap={}
lettotalGas=BigNumber.from(0)
debug('获得内存池的 ',entries.length)
// eslint-disable-next-line no-labels
mainLoop:
for(constentryofentries){
// 检查声誉系统
// 检查每个发送者的重复UserOps
// 检查抵押
// 检查存储访问
// 检查UserOp调用的gas限制
// 检查若存在的支付者存款
// 如果发送者的帐户已存在:用其存储根哈希替换
senders.add(entry.userOp.sender)
bundle.push(entry.userOp)
totalGas=newTotalGas
}
return[bundle,storageMap]
}
查看原文 BundleManager.ts 托管 ❤ 由 GitHub
打包验证和创建
在UserOperations包验证和创建完成后,BundleManager
的sendBundle
函数会签名以太坊交易并将其发送到以太坊提供者。如果交易回滚,而在前一步中成功验证,声誉处罚将被施加以防止滥用。ReputationManager
模块的hourlyCron
方法随后确保指数移动平均被应用于限制或禁用全局实体。
packages/bundler/src/modules/BundleManager.ts – Medium
/\*\*
\\* 提交打包。
\\* 提交打包后,从内存池中移除所有UserOps
\\* @return 发送打包返回成功交易的交易和UserOp哈希,或者在失败交易时返回null
\*/
asyncsendBundle(userOps: UserOperation\[\],beneficiary: string,storageMap: StorageMap): Promise<SendBundleReturn\undefined>{
try{
constfeeData=awaitthis.provider.getFeeData()
consttx=awaitthis.entryPoint.populateTransaction.handleOps(userOps,beneficiary,{
type: 2,
nonce: awaitthis.signer.getTransactionCount(),
gasLimit: 10e6,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas??0,
maxFeePerGas: feeData.maxFeePerGas??0
})
tx.chainId=this.provider.\_network.chainId
constsignedTx=awaitthis.signer.signTransaction(tx)
letret: string
// ...
ret=awaitthis.provider.send('eth\_sendRawTransaction',\[signedTx\])
// ...
consthashes=awaitthis.getUserOpHashes(userOps)
return{
transactionHash: ret,
userOpHashes: hashes
}
}catch(e: any){
// ...
constuserOp=userOps\[opIndex\]
constreasonStr: string=reason.toString()
if(reasonStr.startsWith('AA3')){
this.reputationManager.crashedHandleOps(getAddr(userOp.paymasterAndData))
}elseif(reasonStr.startsWith('AA2')){
this.reputationManager.crashedHandleOps(userOp.sender)
}elseif(reasonStr.startsWith('AA1')){
this.reputationManager.crashedHandleOps(getAddr(userOp.initCode))
}else{
this.mempoolManager.removeUserOp(userOp)
console.warn(\`处理操作失败 sender=${userOp.sender} reason=${reasonStr}\`)
}
}
}
查看原文 BundleManager.ts 托管 ❤ 由 GitHub
一个UserOperations的包被发送到入口点
当交易被签名并包含在区块内后,它被解析并返回到SDK,可以向用户的钱包显示可读的消息。
以太坊生态系统持续演进,ERC-4337:使用替代内存池(Alt mempool)的账户抽象引入了交易处理方式的范式转变。
随着我们深入探讨打包者及其与其他组件的交互,我们希望现在清楚了该系统在账户抽象领域中的角色。
请继续关注该系列的第二部分,我们将更深入地探讨ERC-4337的其他组件及其工作流。
- 原文链接: medium.com/oak-security/...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!