Passer au contenu principal

Comprendre les spécifications de l'EVM du Livre jaune

evm
Intermédiaire
qbzzt
15 mai 2022
20 minutes de lecture

Le Livre jauneopens in a new tab est la spécification formelle pour Ethereum. Sauf là où il a été modifié par le processus EIP, il contient la description exacte du fonctionnement de tout. Il est rédigé sous forme de document mathématique, qui inclut une terminologie que les programmeurs pourraient ne pas connaître. Dans ce document, vous apprendrez comment le lire et, par extension, d'autres documents mathématiques connexes.

Quel Livre jaune ?

Comme presque tout le reste dans Ethereum, le Livre jaune évolue avec le temps. Afin de pouvoir se référer à une version spécifique, j'ai téléversé la version actuelle au moment de la rédaction. Les numéros de section, de page et d'équation que j'utilise se référeront à cette version. Il est recommandé de l'avoir ouvert dans une autre fenêtre pendant la lecture de ce document.

Pourquoi l'EVM ?

Le Livre jaune original a été rédigé au tout début du développement d'Ethereum. Il décrit le mécanisme de consensus original basé sur la preuve de travail qui était initialement utilisé pour sécuriser le réseau. Cependant, Ethereum a abandonné la preuve de travail et a commencé à utiliser un consensus basé sur la preuve d'enjeu en septembre 2022. Ce tutoriel se concentrera sur les parties du Livre jaune qui définissent la machine virtuelle Ethereum. L'EVM est resté inchangé suite à la transition vers la preuve d'enjeu (à l'exception de la valeur de retour de l'opcode DIFFICULTY).

9 Modèle d'exécution

Cette section (p. 12-14) inclut la majeure partie de la définition de l'EVM.

Le terme état du système inclut tout ce que vous devez savoir sur le système pour le faire fonctionner. Dans un ordinateur typique, cela signifie la mémoire, le contenu des registres, etc.

Une machine de Turingopens in a new tab est un modèle de calcul. Il s'agit essentiellement d'une version simplifiée d'un ordinateur, dont il est prouvé qu'elle a la même capacité à effectuer des calculs qu'un ordinateur normal (tout ce qu'un ordinateur peut calculer, une machine de Turing peut le calculer et vice-versa). Ce modèle facilite la preuve de divers théorèmes sur ce qui est et ce qui n'est pas calculable.

Le terme Turing-completopens in a new tab désigne un ordinateur qui peut effectuer les mêmes calculs qu'une machine de Turing. Les machines de Turing peuvent entrer dans des boucles infinies, ce que l'EVM ne peut pas faire, car il serait à court de gaz. Il est donc seulement quasi-Turing-complet.

9.1 Notions de base

Cette section présente les bases de l'EVM et sa comparaison avec d'autres modèles de calcul.

Une machine à pileopens in a new tab est un ordinateur qui stocke des données intermédiaires non pas dans des registres, mais dans une pileopens in a new tab. C'est l'architecture privilégiée pour les machines virtuelles, car elle est facile à mettre en œuvre, ce qui signifie que les bogues et les vulnérabilités de sécurité sont beaucoup moins probables. La mémoire de la pile est divisée en mots de 256 bits. Ce choix a été fait, car il est pratique pour les opérations cryptographiques de base d'Ethereum telles que le hachage Keccak-256 et les calculs sur les courbes elliptiques. La taille maximale de la pile est de 1024 éléments (1024 x 256 bits). Lorsque des opcodes sont exécutés, ils obtiennent généralement leurs paramètres depuis la pile. Il existe des opcodes spécifiques pour réorganiser les éléments dans la pile tels que POP (retire un élément du sommet de la pile), DUP_N (duplique le Nième élément de la pile), etc.

L'EVM dispose également d'un espace volatile appelé mémoire qui est utilisé pour stocker des données pendant l'exécution. Cette mémoire est organisée en mots de 32 octets. Tous les emplacements mémoire sont initialisés à zéro. Si vous exécutez ce code Yulopens in a new tab pour ajouter un mot à la mémoire, il remplira 32 octets de mémoire en complétant l'espace vide du mot avec des zéros, c'est-à-dire qu'il crée un mot avec des zéros aux emplacements 0-29, 0x60 à l'emplacement 30 et 0xA7 à l'emplacement 31.

1mstore(0, 0x60A7)

mstore est l'un des trois opcodes que l'EVM fournit pour interagir avec la mémoire. Il charge un mot en mémoire. Les deux autres sont mstore8 qui charge un seul octet en mémoire, et mload qui déplace un mot de la mémoire vers la pile.

L'EVM dispose également d'un modèle de stockage non volatile distinct qui est maintenu dans le cadre de l'état du système. Cette mémoire est organisée en tableaux de mots (par opposition aux tableaux d'octets adressables par mot dans la pile). Ce stockage est l'endroit où les contrats conservent les données persistantes. Un contrat ne peut interagir qu'avec son propre stockage. Le stockage est organisé en correspondances clé-valeur.

Bien que cela ne soit pas mentionné dans cette section du Livre jaune, il est également utile de savoir qu'il existe un quatrième type de mémoire. Calldata est une mémoire en lecture seule adressable par octets, utilisée pour stocker la valeur transmise avec le paramètre data d'une transaction. L'EVM a des opcodes spécifiques pour la gestion des calldata. calldatasize renvoie la taille des données. calldataload charge les données dans la pile. calldatacopy copie les données en mémoire.

L'architecture Von Neumannopens in a new tab standard stocke le code et les données dans la même mémoire. L'EVM ne suit pas cette norme pour des raisons de sécurité. Le partage de la mémoire volatile permet de modifier le code du programme. Au lieu de cela, le code est enregistré dans le stockage.

Il n'y a que deux cas dans lesquels le code est exécuté à partir de la mémoire :

  • Lorsqu'un contrat crée un autre contrat (à l'aide de CREATEopens in a new tab ou CREATE2opens in a new tab), le code du constructeur de contrat provient de la mémoire.
  • Lors de la création de n'importe quel contrat, le code du constructeur s'exécute, puis renvoie le code du contrat réel, également depuis la mémoire.

L'expression « exécution exceptionnelle » désigne une exception qui provoque l'arrêt de l'exécution du contrat en cours.

9.2 Aperçu des frais

Cette section explique comment les frais de gaz sont calculés. Il existe trois coûts :

Coût de l'opcode

Le coût inhérent de l'opcode spécifique. Pour obtenir cette valeur, trouvez le groupe de coût de l'opcode dans l'annexe H (p. 28, sous l'équation (327)), puis trouvez le groupe de coût dans l'équation (324). Cela vous donne une fonction de coût qui, dans la plupart des cas, utilise les paramètres de l'annexe G (p. 27).

Par exemple, l'opcode CALLDATACOPYopens in a new tab est un membre du groupe Wcopy. Le coût de l'opcode pour ce groupe est Gverylow+Gcopy×⌈μs[2]÷32⌉. En regardant l'annexe G, nous voyons que les deux constantes sont 3, ce qui nous donne 3+3×⌈μs[2]÷32⌉.

Nous devons encore déchiffrer l'expression ⌈μs[2]÷32⌉. La partie la plus externe, ⌈ <valeur> ⌉, est la fonction plafond, une fonction qui, pour une valeur donnée, renvoie le plus petit entier qui n'est pas inférieur à la valeur. Par exemple, ⌈2.5⌉ = ⌈3⌉ = 3. La partie intérieure est μs[2]÷32. En regardant la section 3 (Conventions) à la p. 3, μ est l'état de la machine. L'état de la machine est défini dans la section 9.4.1 à la p. 13. Selon cette section, l'un des paramètres d'état de la machine est s pour la pile. En rassemblant tous ces éléments, il semble que μs[2] soit l'emplacement n°2 dans la pile. En regardant l'opcodeopens in a new tab, l'emplacement n°2 de la pile correspond à la taille des données en octets. Si l'on regarde les autres opcodes du groupe Wcopy, CODECOPYopens in a new tab et RETURNDATACOPYopens in a new tab, ils ont aussi une taille de données au même emplacement. Donc ⌈μs[2]÷32⌉ est le nombre de mots de 32 octets nécessaires pour stocker les données en cours de copie. En résumé, le coût inhérent de CALLDATACOPYopens in a new tab est de 3 gaz plus 3 par mot de données copié.

Coût d'exécution

Le coût d'exécution du code que nous appelons.

Coût d'expansion de la mémoire

Le coût de l'expansion de la mémoire (si nécessaire).

Dans l'équation 324, cette valeur est écrite comme suit : Cmemi')-Cmemi). En examinant à nouveau la section 9.4.1, nous voyons que μi est le nombre de mots en mémoire. Ainsi, μi est le nombre de mots en mémoire avant l'opcode et μi' est le nombre de mots en mémoire après l'opcode.

La fonction Cmem est définie dans l'équation 326 : Cmem(a) = Gmemory × a + ⌊a2 ÷ 512⌋. ⌊x⌋ est la fonction plancher, une fonction qui, pour une valeur donnée, renvoie le plus grand entier qui n'est pas supérieur à la valeur. Par exemple, ⌊2.5⌋ = ⌊2⌋ = 2. Lorsque a < √512, a2 < 512, et le résultat de la fonction plancher est zéro. Ainsi, pour les 22 premiers mots (704 octets), le coût augmente de façon linéaire avec le nombre de mots de mémoire requis. Au-delà de ce point, ⌊a2 ÷ 512⌋ est positif. Lorsque la mémoire requise est suffisamment élevée, le coût en gaz est proportionnel au carré de la quantité de mémoire.

Remarque : ces facteurs n'influencent que le coût en gaz inhérent. Ils ne tiennent pas compte du marché des frais ni des pourboires versés aux validateurs qui déterminent le montant qu'un utilisateur final doit payer. Il s'agit simplement du coût brut de l'exécution d'une opération particulière sur l'EVM.

En savoir plus sur le gaz.

9.3 Environnement d'exécution

L'environnement d'exécution est un uplet, I, qui inclut des informations ne faisant pas partie de l'état de la blockchain ou de l'EVM.

ParamètreOpcode pour accéder aux donnéesCode Solidity pour accéder aux données
IaADDRESSopens in a new tabaddress(this)
IoORIGINopens in a new tabtx.origin
IpGASPRICEopens in a new tabtx.gasprice
IdCALLDATALOADopens in a new tab, etc.msg.data
IsCALLERopens in a new tabmsg.sender
IvCALLVALUEopens in a new tabmsg.value
IbCODECOPYopens in a new tabaddress(this).code
IHChamps d'en-tête de bloc, tels que NUMBERopens in a new tab et DIFFICULTYopens in a new tabblock.number, block.difficulty, etc.
IeProfondeur de la pile d'appels pour les appels entre contrats (y compris la création de contrat)
IwL'EVM est-il autorisé à modifier l'état, ou s'exécute-t-il de manière statique

Quelques autres paramètres sont nécessaires pour comprendre le reste de la section 9 :

ParamètreDéfini dans la sectionSignification
σ2 (p. 2, équation 1)L'état de la blockchain
g9.3 (p. 13)Gaz restant
A6.1 (p. 8)Sous-état accumulé (changements prévus pour la fin de la transaction)
o9.3 (p. 13)Sortie - le résultat retourné dans le cas d'une transaction interne (lorsqu'un contrat en appelle un autre) et des appels aux fonctions de vue (lorsque vous demandez simplement des informations, il n'est donc pas nécessaire d'attendre une transaction)

9.4 Aperçu de l'exécution

Maintenant que nous avons tous les préliminaires, nous pouvons enfin commencer à étudier le fonctionnement de l'EVM.

Les équations 137-142 nous donnent les conditions initiales pour l'exécution de l'EVM :

SymboleValeur initialeSignification
μggGaz restant
μpc0Compteur de programme, l'adresse de la prochaine instruction à exécuter
μm(0, 0, ...)Mémoire, initialisée à zéro
μi0Emplacement mémoire le plus élevé utilisé
μs()La pile, initialement vide
μoLa sortie, un ensemble vide jusqu'à ce que nous nous arrêtions avec des données de retour (RETURNopens in a new tab ou REVERTopens in a new tab) ou sans (STOPopens in a new tab ou SELFDESTRUCTopens in a new tab).

L'équation 143 nous indique qu'il existe quatre conditions possibles à chaque instant lors de l'exécution, et ce qu'il faut faire avec elles :

  1. Z(σ,μ,A,I). Z représente une fonction qui teste si une opération crée une transition d'état non valide (voir arrêt exceptionnel). Si elle est évaluée à Vrai, le nouvel état est identique à l'ancien (à l'exception du gaz brûlé), car les modifications n'ont pas été appliquées.
  2. Si l'opcode exécuté est REVERTopens in a new tab, le nouvel état est le même que l'ancien, et une partie du gaz est perdue.
  3. Si la séquence d'opérations est terminée, comme l'indique un RETURNopens in a new tab), l'état est mis à jour vers le nouvel état.
  4. Si nous ne sommes pas dans l'une des conditions de fin 1-3, l'exécution continue.

9.4.1 État de la machine

Cette section explique plus en détail l'état de la machine. Elle spécifie que w est l'opcode actuel. Si μpc est inférieur à ||Ib||, la longueur du code, alors cet octet (Ibpc]) est l'opcode. Sinon, l'opcode est défini comme STOPopens in a new tab.

Comme il s'agit d'une machine à pileopens in a new tab, nous devons suivre le nombre d'éléments retirés (δ) et insérés (α) par chaque opcode.

9.4.2 Arrêt exceptionnel

Cette section définit la fonction Z, qui spécifie quand nous avons une fin anormale. Il s'agit d'une fonction booléenneopens in a new tab, elle utilise donc pour un OU logiqueopens in a new tab et pour un ET logiqueopens in a new tab.

Nous avons un arrêt exceptionnel si l'une de ces conditions est vraie :

  • μg < C(σ,μ,A,I) Comme nous l'avons vu dans la section 9.2, C est la fonction qui spécifie le coût du gaz. Il ne reste pas assez de gaz pour couvrir le prochain opcode.

  • δw=∅ Si le nombre d'éléments dépilés pour un opcode est indéfini, l'opcode lui-même est indéfini.

  • || μs || < δw Sous-dépassement de la pile, pas assez d'éléments dans la pile pour l'opcode actuel.

  • w = JUMP ∧ μs[0]∉D(Ib) L'opcode est JUMPopens in a new tab et l'adresse n'est pas une JUMPDESTopens in a new tab. Les sauts ne sont valides que si la destination est une JUMPDESTopens in a new tab.

  • w = JUMPI ∧ μs[1]≠0 ∧ μs[0] ∉ D(Ib) L'opcode est JUMPIopens in a new tab, la condition est vraie (non nulle), donc le saut doit avoir lieu, et l'adresse n'est pas une JUMPDESTopens in a new tab. Les sauts ne sont valides que si la destination est une JUMPDESTopens in a new tab.

  • w = RETURNDATACOPY ∧ μs[1]+μs[2]>|| μo || L'opcode est RETURNDATACOPYopens in a new tab. Dans cet opcode, l'élément de pile μs[1] est le décalage à partir duquel lire dans le tampon des données de retour, et l'élément de pile μs[2] est la longueur des données. Cette condition se produit lorsque vous essayez de lire au-delà de la fin du tampon de données retournées. Notez qu'il n'y a pas de condition similaire pour les données d'appel ou pour le code lui-même. Lorsque vous essayez de lire au-delà de la fin de ces tampons, vous obtenez simplement des zéros.

  • || μs || - δw + αw > 1024

    Dépassement de la pile. Si l'exécution de l'opcode entraîne une pile de plus de 1024 éléments, abandonnez.

  • ¬Iw ∧ W(w,μ) Sommes-nous en train d'exécuter statiquement (¬ est la négationopens in a new tab et Iw est vrai lorsque nous sommes autorisés à changer l'état de la blockchain) ? Si c'est le cas et que nous essayons une opération qui change l'état, cela ne peut pas se produire.

    La fonction W(w,μ) est définie plus loin dans l'équation 150. W(w,μ) est vraie si l'une de ces conditions est vraie :

    • w ∈ {CREATE, CREATE2, SSTORE, SELFDESTRUCT} Ces opcodes modifient l'état, soit en créant un nouveau contrat, en stockant une valeur, soit en détruisant le contrat actuel.

    • LOG0≤w ∧ w≤LOG4 Si nous sommes appelés de manière statique, nous ne pouvons pas émettre d'entrées de journal. Les opcodes de journal se situent tous dans la plage entre LOG0 (A0)opens in a new tab et LOG4 (A4)opens in a new tab. Le nombre après l'opcode de journal spécifie combien de sujets l'entrée de journal contient.

    • w=CALL ∧ μs[2]≠0 Vous pouvez appeler un autre contrat lorsque vous êtes statique, mais si vous le faites, vous ne pouvez pas lui transférer d'ETH.

  • w = SSTORE ∧ μg ≤ Gcallstipend Vous ne pouvez pas exécuter SSTOREopens in a new tab à moins d'avoir plus de Gcallstipend (défini à 2300 dans l'Annexe G) de gaz.

9.4.3 Validité de la destination de saut

Ici, nous définissons formellement ce que sont les opcodes JUMPDESTopens in a new tab. Nous ne pouvons pas simplement chercher la valeur de l'octet 0x5B, car elle pourrait se trouver à l'intérieur d'un PUSH (et donc être une donnée et non un opcode).

Dans l'équation (153), nous définissons une fonction, N(i,w). Le premier paramètre, i, est l'emplacement de l'opcode. Le second, w, est l'opcode lui-même. Si w∈[PUSH1, PUSH32], cela signifie que l'opcode est un PUSH (les crochets définissent une plage qui inclut les points d'extrémité). Dans ce cas, l'opcode suivant se trouve à i+2+(w−PUSH1). Pour PUSH1opens in a new tab, nous devons avancer de deux octets (le PUSH lui-même et la valeur d'un octet), pour PUSH2opens in a new tab, nous devons avancer de trois octets parce que c'est une valeur de deux octets, etc. Tous les autres opcodes EVM ont une longueur d'un seul octet, donc dans tous les autres cas N(i,w)=i+1.

Cette fonction est utilisée dans l'équation (152) pour définir DJ(c,i), qui est l'ensembleopens in a new tab de toutes les destinations de saut valides dans le code c, à partir de l'emplacement de l'opcode i. Cette fonction est définie de manière récursive. Si i≥||c||, cela signifie que nous sommes à la fin ou après la fin du code. Nous ne trouverons plus d'autres destinations de saut, donc retournez simplement l'ensemble vide.

Dans tous les autres cas, nous examinons le reste du code en passant à l'opcode suivant et en obtenant l'ensemble à partir de celui-ci. c[i] est l'opcode actuel, donc N(i,c[i]) est la position du prochain opcode. DJ(c,N(i,c[i])) est donc l'ensemble des destinations de saut valides qui commence au prochain opcode. Si l'opcode actuel n'est pas un JUMPDEST, retournez simplement cet ensemble. Si c'est un JUMPDEST, incluez-le dans l'ensemble résultant et retournez-le.

9.4.4 Arrêt normal

La fonction d'arrêt H, peut retourner trois types de valeurs.

  • Si nous ne sommes pas dans un opcode d'arrêt, retournez ∅, l'ensemble vide. Par convention, cette valeur est interprétée comme un booléen faux.
  • Si nous avons un opcode d'arrêt qui ne produit pas de sortie (soit STOPopens in a new tab ou SELFDESTRUCTopens in a new tab), retournez une séquence d'octets de taille zéro comme valeur de retour. Notez que ceci est très différent de l'ensemble vide. Cette valeur signifie que l'EVM s'est vraiment arrêté, il n'y a juste aucune donnée de retour à lire.
  • Si nous avons un opcode d'arrêt qui produit une sortie (soit RETURNopens in a new tab ou REVERTopens in a new tab), retournez la séquence d'octets spécifiée par cet opcode. Cette séquence est prise de la mémoire, la valeur en haut de la pile (μs[0]) est le premier octet, et la valeur après elle (μs[1]) est la longueur.

H.2 Jeu d'instructions

Avant de passer à la sous-section finale de l'EVM, 9.5, examinons les instructions elles-mêmes. Elles sont définies dans l'Annexe H.2 qui commence à la page 29. Tout ce qui n'est pas spécifié comme changeant avec cet opcode spécifique est censé rester identique. Les variables qui changent sont spécifiées comme <quelque chose>′.

Par exemple, examinons l'opcode ADDopens in a new tab.

ValeurMnémoniqueδαDescription
0x01ADD21Opération d'addition.
μ′s[0] ≡ μs[0] + μs[1]

δ est le nombre de valeurs que nous retirons de la pile. Dans ce cas deux, car nous additionnons les deux premières valeurs.

α est le nombre de valeurs que nous remettons. Dans ce cas une, la somme.

Ainsi, le nouveau sommet de la pile (μ′s[0]) est la somme de l'ancien sommet de la pile (μs[0]) et de l'ancienne valeur en dessous (μs[1]).

Au lieu de passer en revue tous les opcodes avec une « liste ennuyeuse », cet article n'explique que les opcodes qui introduisent quelque chose de nouveau.

ValeurMnémoniqueδαDescription
0x20KECCAK25621Calculez le hachage Keccak-256.
μ′s[0] ≡ KEC(μms[0] . . . (μs[0] + μs[1] − 1)])
μ′i ≡ M(μis[0],μs[1])

Il s'agit du premier opcode qui accède à la mémoire (dans ce cas, en lecture seule). Cependant, il pourrait dépasser les limites actuelles de la mémoire, nous devons donc mettre à jour μi. Nous faisons cela en utilisant la fonction M définie dans l'équation 328 à la page 29.

ValeurMnémoniqueδαDescription
0x31BALANCE11Obtenez le solde du compte donné.
...

L'adresse dont nous devons trouver le solde est μs[0] mod 2160. Le sommet de la pile est l'adresse, mais comme les adresses ne font que 160 bits, nous calculons la valeur moduloopens in a new tab 2160.

Si σ[μs[0] mod 2160] ≠ ∅, cela signifie qu'il y a des informations sur cette adresse. Dans ce cas, σ[μs[0] mod 2160]b est le solde de cette adresse. Si σ[μs[0] mod 2160] = ∅, cela signifie que cette adresse n'est pas initialisée et le solde est zéro. Vous pouvez voir la liste des champs d'information du compte dans la section 4.1 à la page 4.

La deuxième équation, A'a ≡ Aa ∪ {μs[0] mod 2160}, est liée à la différence de coût entre l'accès au stockage chaud (stockage qui a récemment été accédé et est susceptible d'être mis en cache) et le stockage froid (stockage qui n'a pas été accédé et est susceptible de se trouver dans un stockage plus lent qui est plus coûteux à récupérer). Aa est la liste des adresses précédemment accédées par la transaction, qui devraient donc être moins chères à accéder, comme défini dans la section 6.1 à la page 8. Vous pouvez en lire plus sur ce sujet dans l'EIP-2929opens in a new tab.

ValeurMnémoniqueδαDescription
0x8FDUP161617Duplique le 16e élément de la pile.
μ′s[0] ≡ μs[15]

Notez que pour utiliser un élément de la pile, nous devons le retirer, ce qui signifie que nous devons également retirer tous les éléments de la pile au-dessus de lui. Dans le cas de DUP<n>opens in a new tab et SWAP<n>opens in a new tab, cela signifie avoir à dépiler puis à empiler jusqu'à seize valeurs.

9.5 Le cycle d'exécution

Maintenant que nous avons toutes les parties, nous pouvons enfin comprendre comment le cycle d'exécution de l'EVM est documenté.

L'équation (155) dit qu'étant donné l'état :

  • σ (état global de la blockchain)
  • μ (état de l'EVM)
  • A (sous-état, changements à effectuer lorsque la transaction se termine)
  • I (environnement d'exécution)

Le nouvel état est (σ', μ', A', I').

Les équations (156)-(158) définissent la pile et le changement qui s'y produit à cause d'un opcode (μs). L'équation (159) est le changement de gaz (μg). L'équation (160) est le changement dans le compteur de programme (μpc). Enfin, les équations (161)-(164) spécifient que les autres paramètres restent les mêmes, sauf s'ils sont explicitement modifiés par l'opcode.

Avec cela, l'EVM est entièrement défini.

Conclusion

La notation mathématique est précise et a permis au Livre jaune de spécifier chaque détail d'Ethereum. Cependant, elle présente quelques inconvénients :

  • Elle ne peut être comprise que par les humains, ce qui signifie que les tests de conformitéopens in a new tab doivent être écrits manuellement.
  • Les programmeurs comprennent le code informatique. Ils peuvent ou non comprendre la notation mathématique.

Peut-être pour ces raisons, les spécifications plus récentes de la couche de consensusopens in a new tab sont écrites en Python. Il existe des spécifications de la couche d'exécution en Pythonopens in a new tab, mais elles ne sont pas complètes. Jusqu'à ce que le Livre jaune soit également traduit en Python ou dans un langage similaire, le Livre jaune continuera d'être utilisé, et il est utile de pouvoir le lire.

Dernière mise à jour de la page : 1 février 2026

Ce tutoriel vous a été utile ?