IoTeX 账户抽象基础指南:p256 签名实用指南

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

随着我们的社区以全票通过了 IoTeX 改进提案 14,账户抽象(Account Abstraction)终于登陆 IoTeX 主网和测试网,其功能现已面向所有生态系统开发者开放。那么,什么是 AA,它是如何运作的,以及你如何在下一个应用中使用它?

快速回顾

根据 ERC-4337 的定义,账户抽象(AA)“允许用户使用包含任意验证逻辑的智能合约钱包,而不是使用 EOA 作为其主账户。” ERC-4337 带来了许多用户体验上的提升,其中最值得关注的是,它使人们能够将智能合约用作自己的主账户。

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

AA 基础设施的组成部分

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 找到。