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には2つのEntryPointコントラクトがデプロイされており、一つはメインネット0xc3527348De07d591c9d567ce1998eFA2031B8675)で、もう一つはテストネット0xc3527348De07d591c9d567ce1998eFA2031B8675)です。EntryPointコントラクトは、特定の特殊コントラクト、すなわちAccountFactoryコントラクトを作成/デプロイする責任を負い、それらは特定の用途に利用できるアカウント(ウォレットコントラクト)を作成する役割を担います。

アカウントの抽象化を使用して新しいカスタムアカウントを作成するために、dApp開発者がアプリケーションのニーズに基づいて作成しなければならない特定のコンポーネントがあります:

  • Accountコントラクト:これはvalidateUserOpメソッドにおいて検証ロジックを実装し、ユーザー操作が要求する可能性のある実行ロジックを実装します。
  • AccountFactoryコントラクト:これは上記のように新しいカスタムアカウントコントラクトを作成/デプロイする責任を負います。
  • AccountFactoryに実装されている検証ルールと互換性のあるユーザー操作を構築するクライアントコード。
  • ペイマスターはAAアーキテクチャのオプションの一部です。IoTeXはテストネット専用でペイマスターサービスを提供していますhttps://paymaster.testnet.w3bstream.com。ペイマスターの役割は、ユーザー操作を実行するために必要なガスをスポンサーすることです。完全にスポンサーするか、ユーザーがさまざまなトークンで支払うことを許可します。

例:P256AccountFactory

最初の例として、開発者が「p256」暗号で署名されたユーザー操作を検証できるアカウントコントラクトを作成するための公式のP256AccountFactoryコントラクト(メインネット0xD98d2B6cBca981c777037c5784721d8179D7030bおよびテストネット0x508Db1A73FcBA98594679aD4f5d8D0B880BbdaFB)を提供しました。これは、開発者がユーザーが生体認証でトランザクションに署名できるアプリケーションを作成したり、シードフレーズから移行したり、専用のセキュリティチップ(例:AndroidのセキュアエレメントやAppleのセキュアエンクレーブなど)をサポートするデバイスであれば、より優れたセキュリティを持つことができるアプリケーションを作成することを容易にします。P256AccountFactoryのソースコードはhttps://github.com/iotexproject/account-abstraction-contracts/blob/main/contracts/accounts/secp256r1/P256AccountFactory.solにて確認でき、オープンソースのアカウント抽象化コントラクトは、Ethereumの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

    // UserOperationsを送信するための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)

    // アカウント作成のためのUserOperationを作成します。
    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曲線を使用してUserOperationに署名します。
    const chainId = (await ethers.provider.getNetwork()).chainId
    const signedOp = await signOp(
        fullCreateOp,
        entryPoint.address,
        chainId,
        new P2565Signer(keyPair)
    )

    // UserOperationをシミュレートします。
    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(`シミュレーションオペレーション成功`)

    // UserOperationを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で確認できます。