Le Guide Essentiel de l'Abstraction de Compte sur IoTeX : Un Guide Pratique sur les Signatures p256

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

Avec notre communauté votant en faveur de la Proposition d'Amélioration IoTeX 14, l'Abstraction de Compte a enfin atteint le Mainnet et le Testnet d'IoTeX, et ses fonctionnalités sont désormais disponibles pour tous les développeurs de l'écosystème. Donc, qu'est-ce que l'AA, comment cela fonctionne-t-il, et comment pouvez-vous l'utiliser dans votre prochaine application ?

Un Petit Rappel

L'Abstraction de Compte (AA) telle que définie par l'ERC-4337, "permet aux utilisateurs d'utiliser des portefeuilles de contrat intelligent contenant une logique de vérification arbitraire au lieu d'EOA comme leur compte principal." L'ERC-4337 introduit de nombreux avantages en matière d'expérience utilisateur, notamment la possibilité pour les utilisateurs d'utiliser des contrats intelligents comme leurs comptes principaux.

L'ERC-4337 fonctionne au-dessus de la blockchain et ne nécessite aucun changement dans la blockchain elle-même. Actuellement, le code de l'Abstraction de Compte d'IoTeX est basé sur la version de sortie ERC-4337 0.6.0.

Composants de l'Infrastructure AA

AA

Les composants de l'infrastructure AA sont :

  • Services de Regroupement : un point de terminaison pour le Mainnet (https://bundler.w3bstream.com) et un pour le Testnet (https://bundler.testnet.w3bstream.com). Un regroupement est un nœud hors chaîne qui agrège plusieurs opérations d'utilisateurs 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 y a deux contrats EntryPoint déployés sur IoTeX, un pour le Mainnet (0xc3527348De07d591c9d567ce1998eFA2031B8675) et un pour le Testnet (0xc3527348De07d591c9d567ce1998eFA2031B8675). Un contrat EntryPoint est responsable de la création/de la mise en œuvre de certains contrats spéciaux, appelés contrats AccountFactory, qui à leur tour sont chargés de créer certains comptes (contrats de portefeuille) qui peuvent être utilisés à des fins spécifiques.

Pour utiliser l'abstraction de compte pour créer un nouveau compte personnalisé, il y a certains composants que le développeur de l'application décentralisée devra créer en fonction des besoins de son application :

  • Contrat Account, qui implémente la logique de validation dans la méthode validateUserOp, et toute logique d'exécution que nécessite une opération utilisateur.
  • Contrat AccountFactory, qui est responsable, comme dit ci-dessus, de la création/de la mise en œuvre de nouveaux contrats de compte personnalisés.
  • Un certain code client qui construit les opérations utilisateur compatibles avec les règles de vérification implémentées dans le AccountFactory.
  • Un paymaster est une partie optionnelle de l'architecture AA. IoTeX fournit un service de paymaster uniquement pour le Testnet au https://paymaster.testnet.w3bstream.com. Le rôle du paymaster est de sponsoriser le gaz nécessaire pour exécuter les opérations des utilisateurs, soit en les sponsorisant complètement, soit en permettant aux utilisateurs de les payer en divers tokens.

Exemple : Le P256AccountFactory

Comme premier exemple, nous avons fourni un contrat officiel P256AccountFactory (Mainnet 0xD98d2B6cBca981c777037c5784721d8179D7030b et Testnet 0x508Db1A73FcBA98594679aD4f5d8D0B880BbdaFB) qui permet aux développeurs de créer des contrats de compte capables de vérifier les opérations utilisateur signées avec la cryptographie "p256", plutôt qu'avec la courbe elliptique native "secp256k1" d'Ethereum et d'IoTeX. C'est incroyablement utile, car cela permet aux développeurs de créer des applications où les utilisateurs peuvent, par exemple, signer des transactions avec leurs biométries, ou s'éloigner des phrases de récupération, 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, l'élément sécurisé d'Android et l'enclave sécurisée d'Apple, etc.). Le code source de la P256AccountFactorycan se trouve à https://github.com/iotexproject/account-abstraction-contracts/blob/main/contracts/accounts/secp256r1/P256AccountFactory.sol tandis que les contrats d'Abstraction de Compte open source s'appuient sur l'implémentation de l'auteur original de l'EIP-4337 pour Ethereum ici https://github.com/iotexproject/account-abstraction-contracts/tree/main.

Le P256AccountFactory prend également en charge la gestion d'un service de paymaster, qui est constitué de deux composants, un contrat VerifyingPaymaster (https://github.com/iotexproject/account-abstraction-contracts/blob/main/contracts/paymaster/VerifyingPaymaster.sol) et un point de service hors chaîne pour générer une preuve de paiement pour le contrat de paymaster (https://paymaster.testnet.w3bstream.com, pour le Testnet uniquement).

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() {
    // charger les contrats déployés
    const factory = (await ethers.getContract("P256AccountFactory")) as P256AccountFactory
    const entryPoint = (await ethers.getContract("EntryPoint")) as EntryPoint

    // un compte EOA pour envoyer des UserOperations
    const bundler = new ethers.Wallet(process.env.BUNDLER!, ethers.provider)

    // charger la paire de clés 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)

    // créer l'opération UserOperation pour créer le compte
    const initCode = hexConcat([        factory.address,        factory.interface.encodeFunctionData("createAccount", [publicKey, index]),
    ])
    const createOp = {
        sender: account,
        initCode: initCode,
    }

    const fullCreateOp = await fillUserOp(createOp, entryPoint)

    // miser IOTX pour le gaz
    const stake = await entryPoint.balanceOf(account)
    if (stake.isZero()) {
        console.log(`dépôt de gaz pour le compte ${account}`)
        const tx = await entryPoint
            .connect(bundler)
            .depositTo(account, { value: ethers.utils.parseEther("10") })
        await tx.wait()
    }

    // signer l'opération UserOperation en utilisant la courbe secp256r1
    const chainId = (await ethers.provider.getNetwork()).chainId
    const signedOp = await signOp(
        fullCreateOp,
        entryPoint.address,
        chainId,
        new P2565Signer(keyPair)
    )

    // simuler l'opération UserOperation
    const err = await entryPoint.callStatic.simulateValidation(signedOp).catch((e) => e)
    if (err.errorName === "FailedOp") {
        console.error(`erreur de simulation d'op ${err.errorArgs.at(-1)}`)
        return
    } else if (err.errorName !== "ValidationResult") {
        console.error(`erreur inconnue ${err}`)
        return
    }
    console.log(`simulation d'op réussie`)

    // envoyer UserOperation au EntryPoint
    const tx = await entryPoint.connect(bundler).handleOps([signedOp], bundler.address)
    console.log(`tx de création de compte : ${tx.hash}, compte : ${account}`)
}

Tandis que le code suivant vous montrera comment transférer des IOTX en utilisant le service de regroupement 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 // un jour
    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 comment interagir avec l'implémentation de compte p256 depuis un client javascript, peut être trouvé à https://github.com/iotexproject/account-abstraction-contracts/tree/main/scripts/secp256r1