关于IoTeX上账户抽象的基础指南:p256签名的实用指南
随着我们社区投票支持IoTeX改进提案14,账户抽象终于在IoTeX主网和测网上线,其特性现在可供所有生态系统开发者使用。那么,AA是什么,它是如何工作的,以及你如何在下一个应用中使用它?
快速回顾
账户抽象(AA)根据ERC-4337的定义,"允许用户使用包含任意验证逻辑的智能合约钱包,而不是EOA,作为其主要账户"。ERC-4337引入了许多用户体验优势,最显著的是使人们可以将智能合约用作他们的主要账户。
ERC-4337运行在区块链之上,不需要对区块链本身进行任何更改。目前,IoTeX的账户抽象代码基于ERC-4337 0.6.0版本。
AA基础架构的组成部分
AA基础架构的组成部分包括:
- Bundler服务:一个用于主网的端点(https://bundler.w3bstream.com)和一个用于测网的端点(https://bundler.testnet.w3bstream.com)。Bundler是一个离线节点,负责将多个抽象用户操作聚合成一个基础区块链可以处理的交易。该交易被发送到另一个固定组件,称为
EntryPoint合约。 EntryPoint合约:在IoTeX上部署了两个EntryPoint合约,一个用于主网(0xc3527348De07d591c9d567ce1998eFA2031B8675)和一个用于测网(0xc3527348De07d591c9d567ce1998eFA2031B8675)。EntryPoint合约负责创建/部署某些特殊合约,即AccountFactory合约,而这些合约又负责创建可以用于特定目的的某些账户(钱包合约)。
为了使用账户抽象创建一个新的自定义账户,dApp开发者需要基于应用的需求创建某些组件:
Account合约,该合约在validateUserOp方法中实现验证逻辑,以及用户操作可能需要的任何执行逻辑。AccountFactory合约,负责如上所述创建/部署新的自定义账户合约。- 一些客户端代码,用于构建与
AccountFactory中实现的验证规则兼容的用户操作。 - paymaster是AA架构的可选部分。IoTeX仅在https://paymaster.testnet.w3bstream.com提供测网的paymaster服务。paymaster的角色是赞助执行用户操作所需的燃气费用,可以完全赞助或者允许用户以各种代币支付。
示例:P256AccountFactory
作为第一个示例,我们提供了一个官方的P256AccountFactory合约(主网0xD98d2B6cBca981c777037c5784721d8179D7030b和测网0x508Db1A73FcBA98594679aD4f5d8D0B880BbdaFB),允许开发者创建可以验证使用"p256"加密签名的用户操作的账户合约,而不是使用以太坊和IoTeX原生的"secp256k1"椭圆曲线。这非常有用,因为它使开发者能够创建应用程序,用户可以例如使用生物识别进行交易签名,或者不再使用种子短语,甚至在其设备支持专用安全芯片(例如Android的安全元件和Apple的安全加密区等)时拥有更好的安全性。P256AccountFactory的源代码可在https://github.com/iotexproject/account-abstraction-contracts/blob/main/contracts/accounts/secp256r1/P256AccountFactory.sol找到,而开源的账户抽象合约依赖于原作者为以太坊实施的EIP-4337的实现,链接在此https://github.com/iotexproject/account-abstraction-contracts/tree/main。
P256AccountFactory还支持管理一个paymaster服务,该服务由两个组件构成,一个是VerifyingPaymaster合约(https://github.com/iotexproject/account-abstraction-contracts/blob/main/contracts/paymaster/VerifyingPaymaster.sol)和一个离线服务端点,用于生成paymaster合约的支付证明(https://paymaster.testnet.w3bstream.com,仅针对测网)。
下面的代码展示了如何通过JavaScript客户端与p256账户实现进行交互,以创建一个账户:
async function main() {
// 加载已部署的合约
const factory = (await ethers.getContract("P256AccountFactory")) as P256AccountFactory
const entryPoint = (await ethers.getContract("EntryPoint")) as EntryPoint
// 发送用户操作的EOA账户
const bundler = new ethers.Wallet(process.env.BUNDLER!, ethers.provider)
// 加载secp256r1密钥对
const keyContent = fs.readFileSync(path.join(__dirname, "key.pem"))
const keyPair = ecPem.loadPrivateKey(keyContent)
const publicKey = "0x" + keyPair.getPublicKey("hex").substring(2)
const index = 0
const account = await factory.getAddress(publicKey, index)
// 创建账户的用户操作
const initCode = hexConcat([ factory.address, factory.interface.encodeFunctionData("createAccount", [publicKey, index]),
])
const createOp = {
sender: account,
initCode: initCode,
}
const fullCreateOp = await fillUserOp(createOp, entryPoint)
// 为燃气押金IOTX
const stake = await entryPoint.balanceOf(account)
if (stake.isZero()) {
console.log(`为账户 ${account} 存入燃气费用`)
const tx = await entryPoint
.connect(bundler)
.depositTo(account, { value: ethers.utils.parseEther("10") })
await tx.wait()
}
// 使用secp256r1曲线签名用户操作
const chainId = (await ethers.provider.getNetwork()).chainId
const signedOp = await signOp(
fullCreateOp,
entryPoint.address,
chainId,
new P2565Signer(keyPair)
)
// 模拟用户操作
const err = await entryPoint.callStatic.simulateValidation(signedOp).catch((e) => e)
if (err.errorName === "FailedOp") {
console.error(`模拟操作错误 ${err.errorArgs.at(-1)}`)
return
} else if (err.errorName !== "ValidationResult") {
console.error(`未知错误 ${err}`)
return
}
console.log(`模拟操作成功`)
// 将用户操作发送到EntryPoint
const tx = await entryPoint.connect(bundler).handleOps([signedOp], bundler.address)
console.log(`创建账户交易: ${tx.hash}, 账户: ${account}`)
}
以下代码将展示如何使用bundler服务和paymaster转移IOTX:
async function main() {
const factory = (await ethers.getContract("P256AccountFactory")) as P256AccountFactory
const accountTpl = await ethers.getContractFactory("P256Account")
const entryPoint = (await ethers.getContract("EntryPoint")) as EntryPoint
const paymaster = await ethers.getContract("VerifyingPaymaster")
const bundler = new JsonRpcProvider("http://localhost:4337")
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!)
const keyContent = fs.readFileSync(path.join(__dirname, "key.pem"))
const keyPair = ecPem.loadPrivateKey(keyContent)
const publicKey = "0x" + keyPair.getPublicKey("hex").substring(2)
const index = 0
const account = await factory.getAddress(publicKey, index)
const callData = accountTpl.interface.encodeFunctionData("execute", [
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
ethers.utils.parseEther("0.1"),
"0x",
])
const transferOp = {
sender: account,
callData,
preVerificationGas: 50000,
}
const fullCreateOp = await fillUserOp(transferOp, entryPoint)
fullCreateOp.paymasterAndData = hexConcat([
paymaster.address,
defaultAbiCoder.encode(["uint48", "uint48"], [0, 0]),
"0x" + "00".repeat(65),
])
const validAfter = Math.floor(new Date().getTime() / 1000)
const validUntil = validAfter + 86400 // 一天
const pendingOpHash = await paymaster.getHash(fullCreateOp, validUntil, validAfter)
const paymasterSignature = await signer.signMessage(arrayify(pendingOpHash))
fullCreateOp.paymasterAndData = hexConcat([
paymaster.address,
defaultAbiCoder.encode(["uint48", "uint48"], [validUntil, validAfter]),
paymasterSignature,
])
const chainId = (await ethers.provider.getNetwork()).chainId
const signedOp = await signOp(
fullCreateOp,
entryPoint.address,
chainId,
new P2565Signer(keyPair)
)
const err = await entryPoint.callStatic.simulateValidation(signedOp).catch((e) => e)
if (err.errorName === "FailedOp") {
console.error(`模拟操作错误 ${err.errorArgs.at(-1)}`)
return
} else if (err.errorName !== "ValidationResult") {
console.error(`未知错误 ${err}`)
return
}
console.log(`模拟操作成功`)
const hexifiedUserOp = deepHexlify(await resolveProperties(signedOp))
const result = await bundler.send("eth_sendUserOperation", [hexifiedUserOp, entryPoint.address])
console.log(`使用bundler成功转移操作哈希: ${result}`)
}
有关如何通过JavaScript客户端与p256账户实现的更多示例,请访问https://github.com/iotexproject/account-abstraction-contracts/tree/main/scripts/secp256r1