XCM Partie II : Versioning et Compatibilité

xcRom1.dot ⭕️
Polkadot Francophonie
11 min readDec 22, 2021

Source officiel : XCM Part II: Versioning and Compatibility

Dans le premier article que j’ai écrit sur XCM, j’ai présenté son architecture de base, ses objectifs et la manière dont il pouvait être utilisé pour certains cas d’utilisation simples. Nous allons maintenant examiner en profondeur un aspect intéressant de XCM : comment XCM peut évoluer dans le temps sans introduire de rupture entre les réseaux qu’il est censé connecter.

Le fait de disposer d’une langue commune résout un grand nombre de problèmes liés à l’interaction humaine. Elle nous permet de travailler ensemble, de résoudre des conflits et d’enregistrer des informations pour une utilisation ultérieure. Mais l’utilité d’une langue dépend des concepts qu’elle est capable d’exprimer et, dans un monde en constante évolution, une langue doit changer et adapter son répertoire conceptuel sous peine de tomber en désuétude.

Malheureusement, le changement trop brutal d’une langue compromet son objectif premier, à savoir faciliter la communication entre les personnes. Puisque les langues doivent changer, il doit y avoir des moyens de gérer ces modifications sans rendre les nouvelles formes inintelligibles pour les non-initiés. Une invention très utile à cet égard a été le dictionnaire, qui permet de documenter et d’archiver la palette conceptuelle d’une langue à une époque donnée, afin que les générations futures soient mieux à même de comprendre les textes historiques. L’édition d’un dictionnaire peut être considérée comme une “version” formalisée d’une langue.

Les temps peuvent changer, mais les problèmes restent étrangement familiers. Comme je l’ai expliqué dans l’article précédent, XCM n’est rien d’autre qu’un langage, bien que très spécialisé. C’est un moyen pour les systèmes de consensus de parler entre eux, et comme les besoins de ce XCM évoluent à la vitesse folle de l’industrie cryptographique et de l’écosystème Polkadot en particulier, il doit y avoir un moyen de s’assurer que ces changements ne compromettent pas l’objectif initial du XCM : l’interopérabilité. Nous devons maintenant résoudre non seulement l’interopérabilité dans l’espace du consensus, mais aussi dans le temps du consensus.

🔮 Versioning

Comme nous nous attendons à ce que le langage de XCM évolue au fil du temps tout en étant très utilisé, une précaution très simple à prendre est de s’assurer que nous identifions la version de XCM que nous communiquons avant le contenu du message proprement dit. Pour ce faire, nous utilisons un certain nombre de types d’enveloppeurs de version, ainsi nommés parce qu’ils enveloppent un message XCM ou un de ses composants par une version. En code Rust, cela semble très simple :

pub enum VersionedXcm {
V0(v0::Xcm),
V1(v1::Xcm),
V2(v2::Xcm),
}

Lorsqu’il est envoyé “over the wire” (ou, plutôt, entre systèmes de consensus), XCM est toujours placé dans ce conteneur versionné. Cela garantit que les systèmes trop anciens pour pouvoir interpréter le message peuvent le recevoir en toute sécurité et reconnaître que le format du message n’est pas pris en charge par eux. Cela permet également aux systèmes plus récents de reconnaître et d’interpréter en conséquence les messages plus anciens.

Les messages XCM ne sont pas les seuls à être versionnés ; dans la base de code XCM, nous versionnons également MultiLocation, MultiAsset,ainsi que les types associés. En effet, il peut être nécessaire de les stocker et de les interpréter ultérieurement lorsque la logique XCM de la chaîne a été mise à jour. Sans versioning, nous pourrions tenter d’interpréter une ancienne MultiLocation comme une nouvelle et découvrir qu’elle est incompréhensible (ou pire, compréhensible mais différente de la signification originale).

💬 Compatibilité et traduction

La gestion des versions est une première étape qui nous permet d’identifier l’édition de la langue utilisée. Il ne garantit pas que nous puissions l’interpréter et certainement pas qu’il s’agisse de l’édition que nous utilisons de préférence. C’est là que la compatibilité entre en jeu. Par “compatibilité”, nous entendons la possibilité de continuer à interpréter et à s’exprimer dans une version de XCM qui n’est pas notre version préférée.

Si nous prévoyons de pouvoir mettre à niveau notre réseau et sa version de XCM à un moment de notre choix, alors cette compatibilité devient assez importante, puisque nous pouvons vouloir communiquer avec d’autres réseaux qui n’ont pas encore fait de mise à niveau ou qui l’ont déjà fait. Cette compatibilité peut être décomposée en compatibilité ascendante et compatibilité descendante. En gros, la compatibilité ascendante est la capacité d’un système mis à niveau à continuer à fonctionner dans un monde ancien, et la compatibilité descendante est la capacité d’un système ancien à continuer à fonctionner dans un monde mis à niveau.

Dans notre cas, nous aimerions avoir les deux, mais il y a des limites pratiques : lorsqu’une nouvelle version de XCM offre des capacités qui n’existaient pas dans les versions précédentes, il n’est pas réaliste d’attendre des systèmes plus anciens qu’ils soient capables d’interpréter ces messages. Ce serait un peu comme essayer de traduire le terme “médias sociaux” en latin et s’attendre ensuite à ce qu’il soit compris tel quel par Jules César. Certains concepts ne peuvent tout simplement pas être exprimés dans un contexte patrimonial.

De même, des changements importants apportés à XCM peuvent entraîner la suppression de capacités de son modèle conceptuel. Cela se produit moins souvent, mais est similaire au problème de la traduction de certains termes archaïques en équivalents modernes. Il est intéressant de noter que le sens archaïque du mot “dot” pourrait être un exemple ici (il désignait une forme assez particulière de dotation financière).

C’est pourquoi les nouvelles versions de XCM sont conçues pour être en grande partie compatibles avec les anciennes et les nouvelles versions, mais il y aura généralement des messages XCM qui n’auront tout simplement pas de sens dans le contexte alternatif et qui ne pourront pas être traduits.

🗣 Communication pratique

Comme mentionné précédemment, nous veillons à ce que tous les messages qui existent indépendamment comprennent un identifiant de version. Il s’agit des messages envoyés entre les systèmes ou des messages persistants dans le stockage. Cela n’inclut cependant pas tous les messages, emplacements et actifs — les données qui existent en tant que partie d’autres données ne doivent pas être versionnées puisque leur version peut être déduite de leur contexte.

L’identification de la version et la compatibilité/traduction sont utiles pour recevoir des messages d’un réseau plus ancien ou envoyer des messages à un réseau plus récent, mais — prises isolément — elles sont moins utiles dans l’autre sens. En effet, un réseau ancien qui reçoit un message d’un réseau plus récent ne dispose pas de la logique nécessaire pour traduire le nouveau XCM sous une forme qu’il peut interpréter. Cette logique n’existe que du côté de l’émetteur, qui dispose du code de traduction capable de réexprimer le nouveau message en termes anciens.

Il doit donc incomber au réseau émetteur de s’assurer que le message qu’il envoie est capable d’être interprété par le réseau récepteur. Concrètement, la version de XCM utilisée pour le message ne doit pas être plus récente que la version de XCM que le réseau récepteur supporte.

Pour cette raison, les Relay Chains Polkadot et Kusama, Statemint, Statemine, Shell et toute autre chaîne basée sur Substrate/Frame et son moteur XCM, conservent toutes un registre des versions de XCM supportées par les chaînes distantes. Lorsqu’un message XCM est envoyé par ces chaînes, elles déterminent d’abord dans quelle version envoyer le message en consultant leur registre. Elle traduit le message dans la plus ancienne des versions XCM supportées par l’expéditeur et le destinataire. Pour les chaînes qui se tiennent à jour, la plupart du temps, il s’agira de la même version, la plus récente, ce qui permet de bénéficier de l’ensemble des fonctionnalités de XCM.

Ce registre serait normalement dicté et mis à jour par des processus de gouvernance, ce qui est un peu lourd et fastidieux, surtout lorsque le nombre de destinations potentielles augmente. C’est pour cette raison que le suivi des versions a été introduit.

🤝 Négociation de la version

Le suivi des versions est la dernière pièce du puzzle de l’histoire du versioning de XCM. Sa fonction est de supprimer tout processus hors chaîne ou de gouvernance nécessaire pour suivre la version XCM des chaînes de destination potentielles. Au lieu de cela, le processus se déroule de manière autonome et sur la chaîne.

Essentiellement, il fonctionne en permettant à un réseau d’utiliser XCM pour demander à un autre la dernière version de XCM qu’il prend en charge et d’être informé de tout changement. Les réponses à cette requête permettent au réseau en question de remplir et de maintenir son registre de versions, garantissant que les messages sont envoyés avec la dernière version compréhensible possible.

Plus précisément, il existe trois instructions précieuses dans XCM : SubscribeVersion, qui permet à une personne de demander à une autre de lui communiquer sa version de XCM maintenant et lorsqu’elle change ; UnsubscribeVersion pour annuler cette demande ; et QueryResponse, un moyen général de renvoyer certaines informations du réseau répondeur au réseau initiateur. Voici à quoi ils ressemblent en Rust :

enum Instruction {
SubscribeVersion {
query_id: QueryId,
max_response_weight: u64,
},
UnsubscribeVersion,
/* snip */
}

SubscribeVersion prend donc deux paramètres. Le premier, query_id est de type QueryId, qui est simplement un nombre entier utilisé pour nous permettre d’identifier et de distinguer les réponses qui reviennent. Toutes les instructions XCM qui entraînent l’envoi d’une réponse disposent d’un moyen similaire pour s’assurer que leur réponse peut être reconnue et traitée en conséquence. Le deuxième paramètre s’appelle max_response_weightet est une valeur Weight(également un nombre entier) indiquant le temps de calcul maximum que la réponse doit prendre par nous quand elle revient. Comme le query_id, ce paramètre sera placé dans tous les messages de réponse que cette instruction génère et est nécessaire pour garantir que tout poids imprévisible et variable puisse au moins être limité à un maximum avant l’exécution. Sans cela, nous serions incapables d’obtenir une limite supérieure sur le temps que le message de réponse pourrait prendre à interpréter et donc être incapable de le planifier pour l’exécution.

L’instruction UnsubscribeVersion est plutôt stérile, principalement parce qu’un seul abonnement à une version peut être actif à la fois pour un emplacement donné. Cela signifie que l’annulation peut se produire sans rien d’autre pour l’identifier que le contenu du registre d’origine.

Une illustration du registre de version et de son utilisation. Ici, la chaîne A (XCM version 2) négocie avec la chaîne E (XCM version 3) et envoie finalement un message version 2, que E traduirait automatiquement en version 3 avant de l’interpréter.

👂 Réponse

La troisième instruction à connaître est QueryResponse, qui est une instruction très générale permettant à une chaîne de répondre à une autre, et ce faisant, de rapporter certaines informations. La voici en Rust :

enum Instruction {
QueryResponse {
query_id: QueryId,
response: Response,
max_weight: u64,
},
/* snip */
}

Nous connaissons déjà deux des trois paramètres, puisqu’ils sont remplis à partir des valeurs fournies dans SubscribeVersion. Le troisième est appelé réponse et contient les informations qui nous intéressent. Il est placé dans un nouveau type Response, lui-même une union de plusieurs types qu’un réseau peut souhaiter utiliser pour informer un autre réseau. Cela ressemble à ceci en Rust :

pub enum Response {
Null,
Assets(MultiAssets),
ExecutionResult(Result<(), (u32, XcmError)>),
Version(XcmVersion),
}

Pour nos besoins actuels, seul l’élément Version est nécessaire, bien que, comme nous le verrons dans les prochains articles, d’autres éléments soient utiles dans d’autres contextes.

⏱ Temps d’exécution

En général, nous n’exigeons pas que les instructions QueryResponse achètent leur propre temps d’exécution avec BuyExecution puisque (en supposant qu’elles soient valides), c’est le réseau qui interprète maintenant qui a demandé qu’elles soient envoyées en premier lieu. De même, nous considérons que SubscribeVersion est quelque chose qui est largement dans l’intérêt commun de l’expéditeur et du destinataire et nous ne nous attendons donc pas à ce qu’il soit nécessaire de payer pour cela. De toute façon, le paiement serait plutôt difficile à calculer en raison de la nature asynchrone et imprévisible des réponses qu’il générerait.

🤖 Automatisation

Bien que ces instructions XCM permettent à un réseau d’utiliser une logique entièrement sur la chaîne pour déterminer la dernière version prise en charge par son interlocuteur, il reste à savoir quand initier cette “poignée de main” de découverte de la version. Elle ne peut généralement pas être effectuée lors de la création d’un canal pour l’envoi de XCM, car la création d’un canal de transport est d’un niveau conceptuellement inférieur à celui de XCM, qui est l’un (peut-être parmi de nombreux) formats de données qui peuvent être envoyés sur ce canal. Embrouiller les choses ici pourrait compromettre l’indépendance de la conception en couches. En outre, certains protocoles de transport de consensus croisé ne sont pas du tout basés sur les canaux, ce qui exclurait la possibilité de négocier la version lors de leur création.

Dans les chaînes de substrat telles que la Relay Chain Polkadot et Statemint, la solution consiste à lancer automatiquement ce processus de découverte de version lorsqu’un message doit être emballé pour être envoyé mais que la dernière version de la destination est inconnue. Cela présente le léger inconvénient que les premiers messages seraient envoyés sous une version XCM sous-optimale, ce qui se produirait jusqu’à ce que la réponse sur la version soit reçue. Si cela constituait un problème pratique, la gouvernance pourrait intervenir pour forcer la version initiale de XCM pour cette destination à être différente de la version par défaut (généralement fixée à la première version de XCM encore attendue en production).

⌨️ Compatibilité des codes au sein de XCM

Le dernier point à aborder en ce qui concerne le versioning est la création de code. Assez différente du format over-the-wire de XCM, la compatibilité du code traite de ce qui doit arriver aux bases de code des projets (basés sur Substrate) qui utilisent l’implémentation Rust de la pile XCM au fil du temps et de son évolution.

Il est clair que les bases de code qui visent à utiliser un langage évolutif pour exprimer des idées doivent changer et s’adapter à l’époque. Nous disposons déjà du système de Versioning sémantique (SemVer) qui permet de dicter les modifications qui peuvent être apportées à des changements de version particuliers. Cependant, ce système est vraiment utile lorsqu’il s’agit d’API et d’ABI, et moins lorsqu’il s’agit de considérer un format ou un langage de données global. Heureusement, XCM est conçu pour n’avoir que peu besoin de SemVer.

Nous savons que les versions les plus récentes du logiciel XCM sont capables de traduire entre les nouveaux et les anciens messages XCM ainsi que leurs types de données internes comme les emplacements et les actifs. Il est capable de le faire en conservant plusieurs versions du langage XCM dans la base de code XCM en même temps. Le système de modules de Rust rend cela banal, une nouvelle version de XCM correspondant simplement à un nouveau module Rust. Si nous examinons la déclaration Rust du type de données VersionedXcm (au début de cet article), il s’agit simplement de l’union étiquetée de chacune des versions spécifiques du type de données Xcm sous-jacent, chacune se trouvant dans son propre module v0, v1, v2, &c.

Puisque les transactions et les API qui utilisent XCM et ses types de données ont tendance à n’utiliser que les variantes versionnées qui peuvent être construites de la même manière avec les anciens et les nouveaux formats, le résultat final est que les bases de code peuvent être mises à jour pour utiliser le logiciel XCM le plus récent (dans Rust, cela s’appelle un crate) avec peu ou pas de changements dans leur code. La mise à jour du crate XCM permet à un réseau de mieux interagir avec d’autres réseaux mis à jour de manière similaire, mais la mise à jour de tout fragment du langage XCM utilisé par le réseau ne doit pas avoir lieu avant une date ultérieure.

Cela constitue, je l’espère, une forte incitation pour les équipes à maintenir leurs crates XCM à jour et donc à maintenir l’itération et l’évolution rapide de l’ensemble.

🏁 Conclusion

J’espère que cela vous a éclairé sur le système de gestion des versions de XCM et sur la manière dont il peut être utilisé pour maintenir la communication d’un réseau de chaînes souveraines alors que le langage qu’elles utilisent pour communiquer évolue à des rythmes et des moments différents d’un réseau à l’autre, et ce sans surcharge opérationnelle significative pour les équipes de développeurs qui maintiennent leur logique.

Dans le prochain épisode, nous examinerons plus en profondeur l’une des parties les plus intéressantes de XCM : son modèle d’exécution et ses capacités de gestion des exceptions.

--

--