IoTeXにおけるアカウントアブストラクションの基本ガイド:p256署名の実践ガイド

コミュニティによるIoTeX改善提案14への全面的な賛成投票を受けて、アカウントアブストラクション(AA)はついにIoTeXのメインネットとテストネットに導入され、その機能はすべてのエコシステム開発者が利用できるようになりました。では、AAとは何か、どのように機能するのか、そして次のアプリケーションでどのように活用できるのでしょうか?
おさらい
ERC-4337によって定義されているアカウントアブストラクション(AA)とは、「ユーザーが主要なアカウントとしてEOAの代わりに、任意の検証ロジックを含むスマートコントラクトウォレットを使用できるようにするもの」です。ERC-4337は多くのユーザーエクスペリエンス上の利点を導入し、特にスマートコントラクトを主要なアカウントとして使用できるようにすることが最も注目される点です。
ERC-4337はブロックチェーンの上で動作し、ブロックチェーン自体への変更を必要としません。現在、IoTeXのアカウントアブストラクションのコードは、ERC-4337の0.6.0リリースバージョンに基づいています。
AAインフラの構成要素
AAインフラの構成要素は以下の通りです:
- バンドラーサービス:メインネット用に1つのエンドポイント(https://bundler.w3bstream.com)、テストネット用に1つのエンドポイント(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)を用意しました。これはEthereumやIoTeXネイティブの「secp256k1」楕円曲線ではなく、「p256」を使用するものです。これは非常に有用です。というのも、これにより開発者はユーザーが例えば生体認証でトランザクションに署名できたり、シードフレーズから移行できたり、あるいはデバイスが専用のセキュリティチップ(例:AndroidのSecure ElementやAppleのSecure Enclaveなど)をサポートしている場合には、より高いセキュリティを実現できるアプリケーションを作成できるようになるからです。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はペイマスターサービスの管理もサポートしており、これは2つのコンポーネントで構成されています。1つはVerifyingPaymasterコントラクト(https://github.com/iotexproject/account-abstraction-contracts/blob/main/contracts/paymaster/VerifyingPaymaster.sol)であり、もう1つはペイマスターコントラクト用の支払い証明を生成するオフチェーンサービスエンドポイント(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で確認できます。