Le guide essentiel de l'Account Abstraction sur IoTeX : un guide pratique des signatures p256

Avec notre communauté ayant voté massivement en faveur de l’IoTeX Improvement Proposal 14, l’Account Abstraction est enfin arrivée sur le Mainnet et le Testnet d’IoTeX, et ses fonctionnalités sont désormais disponibles pour tous les développeurs de l’écosystème. Alors, qu’est-ce que l’AA, comment fonctionne-t-elle, et comment pouvez-vous l’utiliser dans votre prochaine application ?
Un petit rappel
L’Account Abstraction (AA) telle que définie par ERC-4337, “permet aux utilisateurs d’utiliser des portefeuilles à contrat intelligent contenant une logique de vérification arbitraire au lieu des EOA comme compte principal.” ERC-4337 introduit de nombreux avantages en matière d’expérience utilisateur, notamment en permettant aux gens d’utiliser des Smart Contracts comme comptes principaux.
ERC-4337 fonctionne au-dessus de la blockchain et ne nécessite aucune modification de la blockchain elle-même. Actuellement, le code d’Account Abstraction d’IoTeX est basé sur la version 0.6.0 d’ERC-4337.
Composants de l’infrastructure AA
Les composants de l’infrastructure AA sont :
- Services Bundler : un point de terminaison pour le Mainnet (https://bundler.w3bstream.com) et un pour le Testnet (https://bundler.testnet.w3bstream.com). Un bundler est un nœud hors chaîne qui regroupe plusieurs opérations utilisateur abstraites en une seule transaction que la blockchain sous-jacente peut traiter. Cette transaction est envoyée à l’autre composant fixe, appelé le contrat
EntryPoint. - Contrat
EntryPoint: Il existe deux contratsEntryPointdéployés sur IoTeX, un pour le Mainnet (0xc3527348De07d591c9d567ce1998eFA2031B8675) et un pour le Testnet (0xc3527348De07d591c9d567ce1998eFA2031B8675). Un contratEntryPointest chargé de créer/déployer certains contrats spéciaux, appelés contratsAccountFactory, qui sont eux-mêmes chargés de créer certains comptes (contrats de portefeuille) pouvant être utilisés à des fins spécifiques.
Afin d’utiliser l’account abstraction pour créer un nouveau compte personnalisé, certains composants devront être créés par le développeur de dApp en fonction des besoins de son application :
- Le contrat
Account, qui implémente la logique de validation dans la méthodevalidateUserOp, ainsi que toute logique d’exécution qu’une opération utilisateur peut nécessiter. - Le contrat
AccountFactory, qui est chargé, comme dit ci-dessus, de créer/déployer de nouveaux contrats de compte personnalisés. - Du code client qui construit les opérations utilisateur compatibles avec les règles de vérification implémentées dans l’
AccountFactory. - Un paymaster est une partie optionnelle de l’architecture AA. IoTeX propose un service de paymaster uniquement pour le Testnet à l’adresse https://paymaster.testnet.w3bstream.com. Le rôle du paymaster est de sponsoriser le gaz nécessaire à l’exécution des opérations utilisateur, soit en les sponsorisant entièrement, soit en permettant aux utilisateurs de les payer avec divers tokens.
Exemple : le P256AccountFactory
Comme premier exemple, nous avons fourni un contrat P256AccountFactory officiel (Mainnet 0xD98d2B6cBca981c777037c5784721d8179D7030b et Testnet 0x508Db1A73FcBA98594679aD4f5d8D0B880BbdaFB) qui permet aux développeurs de créer des contrats de compte capables de vérifier des opérations utilisateur signées avec la cryptographie “p256”, plutôt qu’avec la courbe elliptique native “secp256k1” d’Ethereum et d’IoTeX. Ceci est extrêmement utile, car cela permet aux développeurs de créer des applications où les utilisateurs peuvent, par exemple, signer des transactions avec leur biométrie, ou se passer des phrases de récupération (seed phrases), ou même bénéficier d’une sécurité supérieure lorsque leur appareil prend en charge une puce de sécurité dédiée (par exemple, le Secure Element d’Android et le Secure Enclave d’Apple, etc.). Le code source du P256AccountFactorycan se trouve à https://github.com/iotexproject/account-abstraction-contracts/blob/main/contracts/accounts/secp256r1/P256AccountFactory.sol tandis que les contrats Account Abstraction open source s’appuient sur l’implémentation de l’auteur original d’EIP-4337 pour Ethereum, disponible ici https://github.com/iotexproject/account-abstraction-contracts/tree/main.
Le P256AccountFactory prend également en charge la gestion d’un service de paymaster, composé de deux éléments, un contrat VerifyingPaymaster (https://github.com/iotexproject/account-abstraction-contracts/blob/main/contracts/paymaster/VerifyingPaymaster.sol) et un point de terminaison de service hors chaîne pour générer une preuve de paiement pour le contrat paymaster (https://paymaster.testnet.w3bstream.com, uniquement pour le Testnet).
Le code ci-dessous vous montre comment interagir avec l’implémentation de compte p256 depuis un client javascript afin de créer un compte :
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}`)
}
Tandis que le code suivant vous montrera comment transférer des IOTX en utilisant le service bundler et le paymaster :
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}`)
}
Le reste de l’exemple sur la manière d’interagir avec l’implémentation de compte p256 depuis un client javascript se trouve à https://github.com/iotexproject/account-abstraction-contracts/tree/main/scripts/secp256r1