# 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的值才能成功。
packages/bundler/src/modules/BundleManager.ts 86行,后面这个地方的gas可以再优化。
* 在测试optimistic主网的时候发现返回的错误信息不同,导致bundler服务启动不了。解决办法是在packages/bundler/src/BundlerServer.ts 77行,在if中添加一个“err?.errorName &&”。这个可能是因为Optimism网络的错误信息格式不太一样,在没有错误的时候,是没有errorName的。
* 还有一些和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