XCM Parte II: Versiones y compatibilidad

En el primer artículo que escribí sobre XCM, presenté su arquitectura básica, objetivos y cómo podría usarse para algunos casos de uso simples. Aquí pasaremos a inspeccionar un aspecto interesante de XCM en profundidad: cómo XCM puede cambiar con el tiempo sin introducir roturas entre las mismas redes que está destinado a conectarse.

Tener un idioma común resuelve una gran cantidad de problemas con la interacción humana. Nos permite trabajar juntos, resolver conflictos y registrar información para su uso posterior. Pero el lenguaje es tan útil como los conceptos que es capaz de expresar, y en un mundo en constante cambio, un lenguaje debe cambiar y adaptar su repertorio conceptual o corre el riesgo de caer en desuso.

Desafortunadamente, cambiar un idioma de manera demasiado abrupta compromete su propósito principal: facilitar la comunicación entre las personas. Dado que los idiomas deben cambiar, debe haber formas de gestionar estas alteraciones sin hacer que las nuevas formas sean ininteligibles para los no iniciados. Un invento muy útil en este sentido fue el diccionario para ayudar a documentar y archivar la paleta conceptual de un idioma en un momento dado para que las generaciones futuras puedan comprender mejor los textos históricos. Una edición de un diccionario podría considerarse como una “versión” formalizada de un idioma.

Los tiempos pueden cambiar, pero los problemas siguen siendo inquietantemente familiares. Como expliqué en el artículo anterior, XCM no es más que un lenguaje, aunque muy especializado. Es un medio para que los sistemas de consenso se comuniquen entre sí, y a medida que las necesidades de este XCM evolucionan a la velocidad vertiginosa de la industria de la criptografía y el ecosistema de Polkadot en particular, debe haber algunos medios para garantizar que estos cambios no comprometan el objetivo original de XCM: la interoperabilidad. Ahora necesitamos resolver no solo la interoperabilidad en el espacio de consenso, sino también en el tiempo de consenso.

🔮 Control de Versiones

Dado que esperamos que el lenguaje de XCM cambie con el tiempo mientras está en uso, una precaución muy simple a tomar es asegurarse de identificar qué versión de XCM estamos comunicando antes del contenido real del mensaje. Hacemos esto usando varios tipos de envoltorios de versiones (version-wrapper), llamados así porque envuelven un mensaje XCM o un componente del mismo por una versión. En el código de Rust, esto parece muy simple:

Cuando se envía “por cable (over the wire)” (o, más bien, entre sistemas de consenso), XCM siempre se coloca en este contenedor versionado. Esto garantiza que los sistemas demasiado antiguos para poder interpretar el mensaje puedan recibirlos de forma segura y reconocer que el formato del mensaje no es compatible con ellos. También permite que los sistemas más nuevos reconozcan e interpreten en consecuencia los mensajes más antiguos.

No solo se versionan los mensajes XCM; en el código base XCM también versionamos MultiLocationMultiAsset, así como sus tipos asociados. Esto se debe a que es posible que sea necesario almacenarlos y luego interpretarlos cuando se actualice la lógica XCM de la cadena. Sin el control de versiones, podríamos intentar interpretar una MultiLocation antigua como una nueva y encontrar que es incomprensible (o peor aún, comprensible pero diferente al significado original).

💬 Compatibilidad y Traducción

El control de versiones es un primer paso y garantiza que podamos identificar la edición del idioma que se está utilizando. No asegura que podamos interpretarlo y ciertamente no asegura que sea la misma edición que usamos preferentemente. Aquí es donde entra en juego la compatibilidad. Por “compatibilidad” nos referimos a la capacidad de seguir interpretándonos y expresándonos en una versión de XCM que no es nuestra versión preferida.

Si esperamos poder actualizar nuestra red y su versión de XCM en el horario que elijamos, entonces esta compatibilidad se vuelve bastante importante ya que es posible que queramos comunicarnos con otras redes que aún no se han actualizado o, de hecho, que ya lo han hecho. Esto se puede dividir en compatibilidad con versiones anteriores y compatibilidad con versiones posteriores. Básicamente, la compatibilidad con versiones anteriores es la capacidad de un sistema actualizado de continuar funcionando en un mundo heredado, y la compatibilidad con versiones posteriores es la capacidad de un sistema heredado de continuar funcionando en un mundo actualizado.

En nuestro caso, nos gustaría tener ambos, sin embargo, existen limitaciones prácticas: donde una nueva versión de XCM proporciona capacidades que no existían en versiones anteriores, no es realista esperar que los sistemas más antiguos sean capaces de interpretar estos mensajes. Sería un poco como intentar traducir el término “redes sociales” al latín y luego esperar que Julio César lo entienda al pie de la letra. Algunos conceptos simplemente no se pueden expresar en un contexto heredado.

De manera similar, cambios significativos en XCM pueden resultar en la eliminación de capacidades de su modelo conceptual. Esto sucede con menos frecuencia, pero es similar al problema de traducir ciertos términos arcaicos en equivalentes de hoy en día. Curiosamente, el significado arcaico de “dot” podría ser un ejemplo aquí (solía significar una forma bastante particular de dotación financiera).

Por lo tanto, las nuevas versiones de XCM están diseñadas para ser en su mayoría compatibles con versiones anteriores y nuevas, pero generalmente habrá mensajes XCM que simplemente no tienen sentido en el contexto alternativo y no serán traducibles.

🗣 Comunicación Práctica

Como se mencionó anteriormente, nos aseguramos de que todos los mensajes que existen de forma independiente incluyan un identificador de versión. Esto significa que los mensajes enviados entre sistemas o los mensajes persisten en el almacenamiento. Sin embargo, no incluye todos los mensajes, ubicaciones y activos; los datos que existen como parte de otros datos no necesitan ser versionados ya que su versión puede inferirse de su contexto.

Si bien la identificación de la versión y la compatibilidad / traducción son útiles para recibir mensajes de una red más antigua o enviar mensajes a una red más nueva, pero, por sí solas, son menos útiles cuando se va en sentido contrario. Esto se debe a que una red heredada que recibe un mensaje de una red actualizada no tiene en sí misma la lógica para poder traducir el nuevo XCM en alguna forma que pueda interpretar; más bien, esa lógica existe solo en el lado de envío que tiene el código de traducción capaz para volver a expresar el nuevo mensaje en términos heredados.

Por tanto, debe ser responsabilidad de la red emisora ​​asegurarse de que el mensaje que envía pueda ser interpretado por la red receptora. En términos concretos, la versión de XCM utilizada para el mensaje no debe ser más reciente que la versión de XCM que admite la red receptora.

Por esta razón, las Relay chains Polkadot y Kusama, Statemint, Statemine, Shell y cualquier otra cadena basada en Substrate / Frame y su motor XCM mantienen un registro de las versiones XCM soportadas por las cadenas remotas. Siempre que estas cadenas envían un mensaje XCM, primero determina en qué versión enviar el mensaje consultando su registro. Traduce el mensaje a la versión más antigua de XCM del remitente y del receptor. En el caso de las cadenas que se mantienen actualizadas, la mayoría de las veces serán la misma versión, la más reciente, que pondrá a disposición el conjunto completo de funciones de XCM.

Este registro normalmente sería dictado y actualizado por procesos de gobernanza, lo cual es un poco engorroso y tedioso, especialmente a medida que aumenta el número de destinos potenciales. Por esta razón, se introdujo el seguimiento de versiones.

🤝 Negociación de Versiones

El seguimiento de versiones es la pieza final del rompecabezas de la historia de versiones de XCM. Su función es eliminar cualquier proceso fuera de la cadena o de gobierno necesario para rastrear la versión XCM de las posibles cadenas de destino. En cambio, el proceso ocurre de forma autónoma y en cadena.

Básicamente, funciona al permitir que una red use XCM para consultar a otra la última versión de XCM que admite y recibir una notificación cada vez que esto cambie. Las respuestas que provienen de esta consulta permiten a la red en cuestión completar y mantener su registro de versiones, asegurando que los mensajes se envíen con la última versión comprensible posible.

Específicamente, hay tres instrucciones valiosas en XCM: SubscribeVersion, que permite que uno le pida a otro que le notifique su versión XCM ahora y cuando cambie; UnsubscribeVersion para cancelar esa solicitud; y QueryResponse, un medio general de devolver cierta información de la red de respuesta a la red de inicio. Así es como se ven en Rust:

Por lo tanto, SubscribeVersion toma dos parámetros. El primero, query_id es de tipo QueryId, que es simplemente un número entero que se usa para permitirnos identificar y distinguir entre las respuestas que regresan. Todas las instrucciones XCM que dan como resultado el envío de una respuesta tienen un significado similar para garantizar que su respuesta pueda ser reconocida y tratada en consecuencia. El segundo parámetro se llama max_response_weight y es un valor Weight (también un número entero) que indica la cantidad máxima de tiempo de cálculo que la respuesta debe tomar cuando regrese. Al igual que query_id, esto se colocará en cualquier mensaje de respuesta que genere esta instrucción y es necesario para garantizar que los costos de peso impredecibles, costas de peso variable puedan al menos limitarse a un máximo antes de la ejecución. Sin esto, no podríamos obtener un límite superior en el tiempo que el mensaje de respuesta podría tardar en interpretarse y, por lo tanto, no podríamos programar su ejecución.

UnsubscribeVersion es bastante estéril como instrucción, principalmente porque solo se permite que una suscripción de versión para una ubicación determinada esté activa a la vez. Esto significa que la cancelación puede ocurrir sin nada más para identificarlo que el contenido del Registro de Origen (Origin Register).

Una ilustración del registro de versiones y su uso. Aquí, Chain A (XCM versión 2) negocia con Chain E (XCM versión 3) y finalmente envía un mensaje de la versión 2, que E traduciría automáticamente a la versión 3 antes de interpretarlo

👂 Responder

La tercera instrucción a tener en cuenta es QueryResponse, que es una instrucción de propósito muy general que permite que una cadena responda a otra y, al hacerlo, proporcione cierta información. Aquí está en Rust:

Ya conocemos dos de los tres parámetros, ya que se completan a partir de los valores proporcionados en SubscribeVersion. El tercero se llama responsey contiene la información real que nos importa. Se coloca en un nuevo tipo de Response, en sí mismo una unión de varios tipos de los cuales una red podría desear utilizar para informar a otra red. Se ve así en Rust:

Para nuestros propósitos actuales, solo se necesita el elemento Version, aunque, como veremos en los próximos artículos, otros elementos son útiles para otros contextos.

⏱ Tiempo de ejecución

En general, no requerimos que las instrucciones QueryResponse compren su propio tiempo de ejecución con BuyExecution ya que (asumiendo que son válidas), fue la red que ahora interpreta la que solicitó que se enviaran en primer lugar. De manera similar, consideramos que SubscribeVersion es algo en general de interés común tanto para el remitente como para el destinatario y, por lo tanto, no esperamos que sea necesario pagarlo. En cualquier caso, el pago sería bastante difícil de calcular debido a la naturaleza asincrónica e impredecible de las respuestas que generaría.

🤖 Automatización

Si bien estas instrucciones XCM permiten que una red utilice la lógica en cadena para determinar la última versión que admite su interlocutor, todavía existe la pregunta de cuándo iniciar este “reconocimiento” de descubrimiento de versiones. Por lo general, no se puede hacer cuando se crea un canal para enviar XCM, ya que la creación del canal de transporte es de un nivel conceptualmente más bajo que el de XCM, que es uno (quizás de muchos) formatos de datos que pueden enviarse a través de ese canal. Dos aguas turbias aquí podrían comprometer la independencia del diseño en capas. Además, algunos protocolos de transporte de consenso cruzado no se basan en ningún canal, lo que excluiría la posibilidad de negociación de versiones desde su inicio.

Dentro de las cadenas de Substrate como la Relay chain de Polkadot y Statemint, la solución es iniciar este proceso de descubrimiento de versión automáticamente cuando un mensaje necesita ser envuelto para su envío pero se desconoce la última versión del destino. Esto tiene el pequeño inconveniente de que los primeros mensajes se enviarían con una versión XCM subóptima, lo que sucedería hasta que se recibiera la respuesta de la versión. Si esto fuera un problema práctico, entonces la gobernanza podría intervenir para forzar que la versión inicial de XCM para ese destino sea algo diferente al predeterminado (generalmente establecido en la versión más antigua de XCM que aún se espera en producción).

⌨️ Compatibilidad de Código dentro de XCM

El último punto a abordar con respecto al control de versiones es la creación de código. A diferencia del formato over-the-wire de XCM, la compatibilidad de código se ocupa de lo que debe suceder con las bases de código de los proyectos (basados ​​en Substrate) que utilizan la implementación de Rust de la pila XCM a lo largo del tiempo a medida que evoluciona.

Claramente, las bases de código que tienen como objetivo utilizar un lenguaje en evolución para expresar ideas deben cambiar y adaptarse a los tiempos. Ya tenemos el sistema Semantic Versioning (SemVer) que ayuda a dictar qué cambios pueden ocurrir sobre cambios de versión particulares. Sin embargo, esto es realmente útil cuando se trata de APIs y ABIs y menos cuando se considera un formato o lenguaje de datos general. Afortunadamente, XCM está diseñado para tener poca necesidad de SemVer.

Sabemos que las versiones más recientes del software XCM son capaces de traducir entre mensajes XCM nuevos y antiguos, así como sus tipos de datos internos, como ubicaciones y activos. Puede hacer esto manteniendo varias versiones del lenguaje XCM en la base de código XCM a la vez. El sistema de módulos de Rust hace que esto sea trivial, con una nueva versión de XCM que simplemente corresponde a un nuevo módulo de Rust. Si revisamos la declaración de Rust del tipo de datos VersionedXcm (justo al comienzo de este artículo), es simplemente la unión etiquetada de cada una de las versiones específicas del tipo de datos Xcm subyacente, cada una de las cuales se encuentra en su propio módulo v0v1v2, y c.

Dado que las transacciones y las APIs que usan XCM y sus tipos de datos tienden a usar solo las variantes versionadas que son igualmente construibles con formatos antiguos y nuevos, el resultado final es que las bases de código se pueden actualizar para usar el software XCM más reciente (en Rust, esto es conocido como crate) con pocos o ningún cambio en su código. La actualización de la caja XCM permite que una red interopere mejor con otras redes mejoradas de manera similar, pero la actualización de cualquier fragmento del lenguaje XCM que utiliza la red no tiene por qué suceder hasta más adelante.

Esto actúa, espero, como un fuerte incentivo para que los equipos mantengan sus cajas XCM actualizadas y, por lo tanto, mantengan todo iterando y evolucionando rápidamente.

🏁 Conclusión

Espero que esto les haya iluminado con respecto al sistema de versiones de XCM y cómo se puede usar para mantener una red de cadenas soberanas comunicándose a medida que el lenguaje que usan para comunicarse evoluciona a diferentes velocidades y tiempos entre redes, y sin una sobrecarga operativa significativa en los equipos de desarrolladores. que mantienen su lógica.

En la próxima entrega, analizaremos mucho más profundamente una de las partes más interesantes de XCM: su modelo de ejecución y capacidades de administración de excepciones.