IoTeX 上账户抽象的实用指南:p256 签名的实用指南

The Essential Guide to Account Abstraction on IoTeX: A Practical Guide to p256 Signatures

随着我们社区投票全力支持 IoTeX 改进提案 14,账户抽象终于在 IoTeX 主网和测试网上启动,其功能现已向所有生态系统开发者开放。那么,AA 是什么?它是如何工作的?你如何在下一个应用中使用它?

快速回顾

账户抽象(AA)根据 ERC-4337 的定义,“允许用户使用包含任意验证逻辑的智能合约钱包,而不是使用外部拥有账户(EOA)作为其主要账户。”ERC-4337 引入了许多用户体验的好处,最显著的是使人们能够使用智能合约作为其主要帐户。

ERC-4337 建立在区块链之上,并不需要对区块链本身进行任何更改。目前,IoTeX 账户抽象代码基于 ERC-4337 0.6.0 版本。

AA 基础设施的组成部分

AA

AA 基础设施的组成部分包括:

  • 捆绑服务:一个用于 主网 的端点 (https://bundler.w3bstream.com) 和一个用于 测试网 的端点 (https://bundler.testnet.w3bstream.com)。捆绑器是一个离线节点,将多个抽象的用户操作聚合成一个可以由基础区块链处理的单一交易。该交易被发送到另一个固定组件,称为 EntryPoint 合约。
  • EntryPoint 合约:在 IoTeX 上部署了两个 EntryPoint 合约,一个用于 主网 (0xc3527348De07d591c9d567ce1998eFA2031B8675) 和一个用于 测试网 (0xc3527348De07d591c9d567ce1998eFA2031B8675)。EntryPoint 合约负责创建/部署某些特殊合约,称为 AccountFactory 合约,这些合约负责创建可以用于特定目的的某些帐户(钱包合约)。

为了使用账户抽象创建一个新的自定义账户,dApp 开发者必须根据其应用的需要创建某些组件:

  • Account 合约,实现 validateUserOp 方法中的验证逻辑,以及用户操作可能需要的任何执行逻辑。
  • AccountFactory 合约,如上所述,负责创建/部署新的自定义账户合约。
  • AccountFactory 中实现的验证规则兼容的用户操作。
  • 一个 支付方 是 AA 架构的可选部分。IoTeX 只为 测试网 提供支付方服务,网址为 https://paymaster.testnet.w3bstream.com。支付方的角色是资助执行用户操作所需的 gas,可以完全赞助他们或允许用户使用各种代币支付。

示例:P256AccountFactory

作为第一个示例,我们提供了一个官方的 P256AccountFactory 合约(主网 0xD98d2B6cBca981c777037c5784721d8179D7030b 和测试网 0x508Db1A73FcBA98594679aD4f5d8D0B880BbdaFB),允许开发者创建能够验证使用“p256”密码学签名的用户操作的账户合约,而不是使用以太坊和 IoTeX 原生的“secp256k1”椭圆曲线。这非常有用,因为它使开发者能够创建应用程序,用户可以例如使用生物识别技术签名交易,或摆脱种子短语,甚至在设备支持专用安全芯片(例如 Android 的安全元件和 Apple 的安全隔离等)时具有更高的安全性。P256AccountFactorycan 的源代码可以在 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() {
    // 加载已部署的合约
    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)

    // 为 gas 质押 IOTX
    const stake = await entryPoint.balanceOf(account)
    if (stake.isZero()) {
        console.log(`为账户 ${account} 存入 gas`)
        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}`)
}

以下代码将向您展示如何 使用捆绑器服务和支付方转移 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(`使用捆绑器转移成功 opHash: ${result}`)
}

有关如何从 javascript 客户端与 p256 账户实现交互的更多示例,请访问 https://github.com/iotexproject/account-abstraction-contracts/tree/main/scripts/secp256r1