关于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基础架构的组成部分包括:

  • 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