# A MultiLayer2 Bundler 最近在和PlanckerDao的小伙伴一起研究ERC4337相关技术,我们打算利用Infinitism的源码搭建一套Layer2上的多链bundler服务。 首先,Entrypoint和账户相关的合约必须部署在链上。目前Entrypoint在很多链上已经部署过,所以现在只需要部署账户相关的合约到链上。我们选择部署SoulWallet的账户合约来进行测试,具体的部署步骤是: * 一个单例工厂SingletonFactory:https://mumbai.polygonscan.com/address/0xce0042b868300000d44a59004da54a005ffdcf9f * 一个账户合约SoulWallet:https://mumbai.polygonscan.com/address/0x0F8065973c4F7AB41E739302152c5cB6aC7590BA * 一个账户工厂合约SoulWalletFactory:https://mumbai.polygonscan.com/address/0xC544A5107d887c9df046Cd8C5fB9D61e7559c229 有了这三个部署好的合约,就可以开始激活一个账户了,部分代码如下: ``` async function activateWallet() { const { chainId } = await loadFixture(deployFixture); const upgradeDelay = 30; const guardianDelay = 30; const walletAddress = await soulWalletLib.calculateWalletAddress( walletLogicAddress, entryPointAddress, walletOwner.address, upgradeDelay, guardianDelay, SoulWalletLib.Defines.AddressZero, slat ); log('walletAddress: ' + walletAddress); log('walletBalance: ' + await provider.getBalance(walletAddress), 'wei'); //#region const feeData = await provider.getFeeData() const activateOp = soulWalletLib.activateWalletOp( walletLogicAddress, entryPointAddress, walletOwner.address, upgradeDelay, guardianDelay, SoulWalletLib.Defines.AddressZero, '0x', feeData.maxFeePerGas?.toNumber() as NumberLike, feeData.maxPriorityFeePerGas?.toNumber() as NumberLike, slat ); await estimateUserOperationGas(bundler, activateOp); const requiredPrefund = await (await activateOp.requiredPrefund()).requiredPrefund; log('requiredPrefund: ', ethers.utils.formatEther(requiredPrefund)); const balance = await provider.getBalance(walletAddress); log('walletBalance: ' + balance, 'wei'); const userOpHash = activateOp.getUserOpHashWithTimeRange(entryPointAddress, chainId, walletOwner.address); activateOp.signWithSignature( walletOwner.address, Utils.signMessage(userOpHash, walletOwner.privateKey) ); const validation = await bundler.simulateValidation(activateOp); if (validation.status !== 0) { throw new Error(`error code:${validation.status}`); } log(`simulateValidation result:`, validation); const simulate = await bundler.simulateHandleOp(activateOp); if (simulate.status !== 0) { throw new Error(`error code:${simulate.status}`); } log(`simulateHandleOp result:`, simulate); let activated = false; const bundlerEvent = bundler.sendUserOperation(activateOp, 1000 * 60 * 5); bundlerEvent.on('error', (err: any) => { console.log(err); }); bundlerEvent.on('send', (userOpHash: string) => { console.log('send: ' + userOpHash); }); bundlerEvent.on('receipt', (receipt: IUserOpReceipt) => { console.log('receipt: ' + receipt); activated = true; }); bundlerEvent.on('timeout', () => { console.log('timeout'); }); while (!activated) { console.log("send userOperration, waiting..."); await new Promise(r => setTimeout(r, 3000)); } const walletAddressCode = await provider.getCode(walletAddress); log('walletAddressCode: ' + walletAddressCode); return { walletAddress } } ``` 需要注意的是生成的账户地址一开始里面是没有余额的,需要在生成这个账户地址后往里面转一些币。然后执行激活程序,没有意外的话就得到了一个智能合约账户,然后通过这个账户可以进行转账等操作,下面是转账部分代码: ``` async function transferEth() { const { chainId } = await loadFixture(deployFixture); const walletAddress = accounts[0]; let nonce = await soulWalletLib.Utils.getNonce(walletAddress, provider); const feeData = await provider.getFeeData() const sendETHOP = await soulWalletLib.Tokens.ETH.transfer( walletAddress, nonce, '0x', feeData.maxFeePerGas?.toNumber() as NumberLike, feeData.maxPriorityFeePerGas?.toNumber() as NumberLike, accounts[1], ethers.utils.parseEther('0.00001').toHexString() ); if (!sendETHOP) { throw new Error('setGuardianOP is null'); } await estimateUserOperationGas(bundler, sendETHOP); const requiredPrefund = await (await sendETHOP.requiredPrefund()).requiredPrefund; log('requiredPrefund: ', ethers.utils.formatEther(requiredPrefund)); const sendETHOPuserOpHash = sendETHOP.getUserOpHashWithTimeRange(entryPointAddress, chainId, walletOwner.address); const sendETHOPSignature = Utils.signMessage(sendETHOPuserOpHash, walletOwner.privateKey) sendETHOP.signWithSignature(walletOwner.address, sendETHOPSignature); let validation = await bundler.simulateValidation(sendETHOP); if (validation.status !== 0) { throw new Error(`error code:${validation.status}`); } let simulate = await bundler.simulateHandleOp(sendETHOP); if (simulate.status !== 0) { throw new Error(`error code:${simulate.status}`); } // get balance of accounts[1].address let finish = false const balanceBefore = await provider.getBalance(accounts[1]); console.log('balanceBefore: ' + balanceBefore); const bundlerEvent = bundler.sendUserOperation(sendETHOP, 1000 * 60 * 5); bundlerEvent.on('error', (err: any) => { console.log(err); }); bundlerEvent.on('send', (userOpHash: string) => { console.log('send: ' + userOpHash); }); bundlerEvent.on('receipt', (receipt: IUserOpReceipt) => { console.log('receipt: ' + receipt); finish = true }); bundlerEvent.on('timeout', () => { console.log('timeout'); }); while (!finish) { console.log("send userOperration, waiting..."); await new Promise(r => setTimeout(r, 3000)); } // get balance of accounts[1].address const balanceAfter = await provider.getBalance(accounts[1]); console.log('balanceAfter: ' + balanceAfter); console.log('diffAmount:' + balanceAfter.sub(balanceBefore).toString()); } ``` 这样的话,我们就在polygon mumbai上成功的通过soulwallet和bundler激活了智能合约账户。然后我们又在polygon主网、arbitrum主网、optimistic主网上重复同样的操作。然后我们又把这些流畅能跑通的不同网络的bundler服务分别部署到了服务器上,这样有需要的人也可以使用这些bundler服务了。 # 后期计划 当然目前的服务只能用于简单测试激活账户、转账操作,真正稳定运行还需要很多优化。后面我们将 * 真正在Layer2部署bundler服务,并在前端启用RPC服务 * 另外我们计划对替代内存池的验证规则进行研究 * 以及Ban policy定制化机制和开发。 # 遇到的问题 下面是我们在测试过程中碰到的一些问题: * 在polygon主网激活时,一直提示gas相关信息(更改了不同gas返回的信息): replacement transaction underpriced Replacement UserOperation must have higher maxFeePerGas transaction underpriced 然后使用Infinitism账户合约进行测试,情况同上。后来通过排查代码发现,当bundler向节点发送请求时,也设置的有gas相关信息。之前只是调整了userOp的gas数据,所以怎么调整都没用。Infinitsm的bundler代码使用provider.getFeeData()来设置gas,但是这个 feeData.maxPriorityFeePerGas 总是返回 1500000000,所以必须增加 maxPriorityFeePerGas的值才能成功。![](https://i.imgur.com/Ea84mbr.png) packages/bundler/src/modules/BundleManager.ts 86行,后面这个地方的gas可以再优化。 * 在测试optimistic主网的时候发现返回的错误信息不同,导致bundler服务启动不了。解决办法是在packages/bundler/src/BundlerServer.ts 77行,在if中添加一个“err?.errorName &&”。这个可能是因为Optimism网络的错误信息格式不太一样,在没有错误的时候,是没有errorName的。![](https://i.imgur.com/A1Ib5cP.png) * 还有一些和bundler不太相关的问题,看到的人可能有帮助。比如: 在服务器上运行测试脚本总是报告“未定义 AbortController”。这是因为npm版本太低,没有这个AbortController。然后更新更高版本的npm,又提示“GLIBC_2.28' not found”。这个是因为Ubuntu版本太低,需要升级系统。原文章链接:https://carmencincotti.com/2023-02-13/resolve-glibc-228-not-found/ # 以下是目前已经部署在链上的合约: Entrypoint: [https://blockscan.com/address/0x0576a174d229e3cfa37253523e645a78a0c91b57](https://blockscan.com/address/0x0576a174d229e3cfa37253523e645a78a0c91b57) SimpleAccountFactory: [https://blockscan.com/address/0x09c58cf6be8e25560d479bd52b4417d15bca2845](https://blockscan.com/address/0x09c58cf6be8e25560d479bd52b4417d15bca2845) polygon-mumbai: SoulWalletFactory:0xC544A5107d887c9df046Cd8C5fB9D61e7559c229 walletImpl:0x0F8065973c4F7AB41E739302152c5cB6aC7590BA polygon-mainnet: SoulWalletFactory:0x28D188e045528c6C29BD51AA6CF0D1a885720EA1 walletImpl:0xc535620Bc1E7e0A5682A1A0b3D4878e8a32c0e81 arbitrum-mainnet: SoulWalletFactory:0x56F379758b6B52830D7dde019A43b2cBdec37c5E walletImpl:0x9f8241410c00a24bf2efe5bf42226645fb1f3839 polygon-mainnet: SimpleAccountFactory:0x09c58cf6be8e25560d479bd52b4417d15bca2845 polygon-mumbai: SimpleAccountFactory:0x09c58cf6be8e25560d479bd52b4417d15bca2845 arbitrum-mainnet: SimpleAccountFactory:0x09c58cf6be8e25560d479bd52b4417d15bca2845