IoTeX 账户抽象基础指南:p256 签名实用指南
随着我们的社区以全票通过了 IoTeX 改进提案 14,账户抽象(Account Abstraction)终于登陆 IoTeX 主网和测试网,其功能现已面向所有生态系统开发者开放。那么,什么是 AA,它是如何运作的,以及你如何在下一个应用中使用它?
快速回顾
根据 ERC-4337 的定义,账户抽象(AA)“允许用户使用包含任意验证逻辑的智能合约钱包,而不是使用 EOA 作为其主账户。” ERC-4337 带来了许多用户体验上的提升,其中最值得关注的是,它使人们能够将智能合约用作自己的主账户。
ERC-4337 运行在区块链之上,不需要对区块链本身进行任何修改。目前,IoTeX 的账户抽象代码基于 ERC-4337 0.6.0 版本。
AA 基础设施的组成部分
AA 基础设施的组成部分包括:
- 打包器服务(Bundler Services):主网有一个端点(https://bundler.w3bstream.com),测试网有一个端点(https://bundler.testnet.w3bstream.com)。打包器是一个链下节点,它将多个抽象化的用户操作聚合为一笔交易,供底层区块链处理。该交易随后被发送到另一个固定组件,即
EntryPoint合约。 EntryPoint合约:IoTeX 上部署了两个EntryPoint合约,一个用于主网(0xc3527348De07d591c9d567ce1998eFA2031B8675),一个用于测试网(0xc3527348De07d591c9d567ce1998eFA2031B8675)。EntryPoint合约负责创建/部署某些特殊合约,称为AccountFactory合约,而这些合约又负责创建用于特定用途的账户(钱包合约)。
为了使用账户抽象创建一个新的自定义账户,dApp 开发者需要根据应用的需求创建以下几个组件:
Account合约,用于在validateUserOp方法中实现验证逻辑,以及用户操作可能需要的任何执行逻辑。AccountFactory合约,如上所述,负责创建/部署新的自定义账户合约。- 一些客户端代码,用于构建与
AccountFactory中实现的验证规则相兼容的用户操作。 - 付款主(paymaster)是 AA 架构中的一个可选部分。IoTeX 仅为测试网提供了付款主服务,地址为 https://paymaster.testnet.w3bstream.com。付款主的作用是为执行用户操作所需的 Gas 提供赞助,可以完全赞助,也可以允许用户使用多种代币来支付。
示例:P256AccountFactory
作为第一个示例,我们提供了一个官方的 P256AccountFactory 合约(主网地址 0xD98d2B6cBca981c777037c5784721d8179D7030b,测试网地址 0x508Db1A73FcBA98594679aD4f5d8D0B880BbdaFB),它允许开发者创建能够验证使用“p256”密码学签名的用户操作的账户合约,而不是使用以太坊和 IoTeX 原生的“secp256k1”椭圆曲线。这非常有用,因为它使开发者能够创建这样的应用:用户可以使用生物识别信息签署交易,摆脱助记词的束缚,甚至在设备支持专用安全芯片(例如 Android 的 Secure Element 和 Apple 的 Secure Enclave 等)时获得更高的安全性。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 还支持付款主服务的管理,该服务由两个部分组成:一个 VerifyingPaymaster 合约(https://github.com/iotexproject/account-abstraction-contracts/blob/main/contracts/paymaster/VerifyingPaymaster.sol),以及一个用于为付款主合约生成支付证明的链下服务端点(https://paymaster.testnet.w3bstream.com,仅限测试网)。
以下代码展示了如何从 JavaScript 客户端与 p256 账户实现进行交互,以创建账户:
async function main() {
// load deployed contracts
const factory = (await ethers.getContract("P256AccountFactory")) as P256AccountFactory
const entryPoint = (await ethers.getContract("EntryPoint")) as EntryPoint
// an EOA account for send UserOperations
const bundler = new ethers.Wallet(process.env.BUNDLER!, ethers.provider)
// load secp256r1 keypair
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)
// create create account UserOperation
const initCode = hexConcat([ factory.address, factory.interface.encodeFunctionData("createAccount", [publicKey, index]),
])
const createOp = {
sender: account,
initCode: initCode,
}
const fullCreateOp = await fillUserOp(createOp, entryPoint)
// stake IOTX for gas
const stake = await entryPoint.balanceOf(account)
if (stake.isZero()) {
console.log(`deposit gas for account ${account}`)
const tx = await entryPoint
.connect(bundler)
.depositTo(account, { value: ethers.utils.parseEther("10") })
await tx.wait()
}
// sign UserOperation using secp256r1 curve
const chainId = (await ethers.provider.getNetwork()).chainId
const signedOp = await signOp(
fullCreateOp,
entryPoint.address,
chainId,
new P2565Signer(keyPair)
)
// simulate UserOperation
const err = await entryPoint.callStatic.simulateValidation(signedOp).catch((e) => e)
if (err.errorName === "FailedOp") {
console.error(`simulate op error ${err.errorArgs.at(-1)}`)
return
} else if (err.errorName !== "ValidationResult") {
console.error(`unknow error ${err}`)
return
}
console.log(`simulate op success`)
// send UserOpersion to EntryPoint
const tx = await entryPoint.connect(bundler).handleOps([signedOp], bundler.address)
console.log(`create account tx: ${tx.hash}, account: ${account}`)
}
而以下代码将展示如何使用打包器服务和付款主来转账 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 // one day
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(`simulate op error ${err.errorArgs.at(-1)}`)
return
} else if (err.errorName !== "ValidationResult") {
console.error(`unknow error ${err}`)
return
}
console.log(`simulate op success`)
const hexifiedUserOp = deepHexlify(await resolveProperties(signedOp))
const result = await bundler.send("eth_sendUserOperation", [hexifiedUserOp, entryPoint.address])
console.log(`transfer use bundler success opHash: ${result}`)
}
关于如何从 JavaScript 客户端与p256账户实现进行交互的其余示例,可以在 https://github.com/iotexproject/account-abstraction-contracts/tree/main/scripts/secp256r1 找到。