Gavin Wood:深入研究XCM底层设计和执行模型

Gavin Wood: Un análisis profundo del diseño subyacente y el modelo de ejecución de XCM

BroadChainBroadChain30/09/2021, 18:35
Este contenido ha sido traducido por IA
Resumen

En este artículo, Gavin Wood realizará un análisis profundo del diseño subyacente y el modelo de ejecución de XCM.

Como lenguaje para intercambiar ideas entre los sistemas de consenso del ecosistema Polkadot, la importancia de XCM es innegable. En su artículo «Gavin Wood: Explicación detallada de los principios de diseño y mecanismos operativos del formato de mensajes entre consensos (XCM)», Gavin Wood ofrece una explicación exhaustiva sobre los principios de diseño y el funcionamiento de XCM. Asimismo, en «Gavin Wood: Exploración del control de versiones y la compatibilidad de XCM», profundiza en su control de versiones y compatibilidad.

En este artículo, Gavin Wood analiza en profundidad el diseño subyacente y el modelo de ejecución de XCM, con el objetivo de facilitar la comprensión de su máquina virtual.

Autor: Gavin Wood

Fuente: Polkadot

Traducción: Chen Yiwanfeng

Dado que XCM es un conjunto de instrucciones basado en XCVM, y esta última es una máquina virtual muy avanzada, comenzaremos presentando brevemente XCVM para familiarizarnos con su arquitectura.

XCVM es una máquina virtual muy avanzada y no Turing-completa. Está basada en registros, no en pilas, y cuenta con varios registros especializados, la mayoría de los cuales almacenan datos altamente estructurados. A diferencia de los procesadores generales, los registros de XCVM no pueden asignarse a valores arbitrarios, sino que están sujetos a mecanismos estrictos que controlan sus cambios. Además de ciertas formas de interactuar con el estado local de la cadena (como las instrucciones WithdrawAsset y DepositAsset mencionadas anteriormente), no existe memoria adicional. No hay posibilidad de bucles ni instrucciones de bifurcación explícitas.

En artículos anteriores ya presentamos dos tipos de registros: el Holding Register y el Origin Register. El Holding Register puede retener temporalmente uno o varios activos, ya sea extrayéndolos de la cadena local o recibiéndolos de una fuente externa de confianza (como otra cadena). El Origin Register contiene, al inicio de la ejecución, la ubicación del sistema de consenso de origen de la ejecución actual de XCM, y solo puede modificarse a una ubicación interna o borrarse por completo.

Entre los demás registros, tres están relacionados con la gestión de excepciones o errores, y dos con el seguimiento del peso de la ejecución. En este artículo nos centraremos especialmente en el modelo de ejecución de estos registros.

Modelo de ejecución

Como se mencionó, no existen instrucciones condicionales explícitas ni primitivas de bucle que permitan ejecutar repetidamente la misma instrucción. Esto facilita determinar de antemano el flujo de control del programa. Esta propiedad es útil porque queremos saber cuánto tiempo de ejecución (denominado «peso» en el entorno Substrate/Polkadot) puede consumir un mensaje XCM antes de ejecutarlo.

Esperamos que la mayoría de las plataformas de consenso que ejecuten XCM necesiten determinar el tiempo de ejecución en el peor de los casos antes de iniciarla. Esto se debe a que las blockchains suelen requerir garantizar que el tiempo de procesamiento de un bloque no supere un límite preestablecido, para evitar que todo el sistema se bloquee. Además, si el sistema requiere el pago de tarifas, este debe realizarse antes de ejecutar la carga de trabajo correspondiente, y dicha tarifa debe cubrir el tiempo de ejecución en el peor escenario.

Debido a su completitud de Turing, los sistemas que permiten lenguajes Turing-completos (como Ethereum) no pueden calcular directamente el tiempo de ejecución en el peor de los casos a partir del programa. Resuelven este problema exigiendo a los usuarios que especifiquen previamente los recursos de ejecución del programa y luego midiendo su consumo durante la ejecución, interrumpiéndola si se supera la cantidad pagada. En ocasiones, las transacciones pueden cambiar incluso antes de su ejecución, haciendo que el peso calculado sea incorrecto. Afortunadamente, máquinas virtuales no Turing-completas como XCVM evitan la necesidad de esta medición y especificación de pesos.

Peso

El peso suele expresarse como un número entero que representa los picosegundos que tarda un hardware de referencia en ejecutar una operación. Como vimos en la instrucción BuyExecution, el XCVM incorpora el concepto de tiempo de ejecución o peso al procesar ciertas instrucciones.

No existe una medición exacta del peso, pero para que los programas XCVM puedan consumir menos peso del previsto en el peor caso, contamos con un registro llamado «registro de peso restante». La mayoría de las instrucciones no lo modifican, ya que podemos predecir con precisión cuánto peso utilizarán. Sin embargo, a veces la estimación del peor caso resulta demasiado alta y solo al ejecutarse sabemos cuánto sobra. Al calcular el tiempo de ejecución del bloque para mensajes XCM cuyo peso se ha sobrestimado, se rastrea el exceso de peso original y se resta de la cuenta, lo que permite a la cadena optimizar su asignación de tiempo de ejecución por bloque.

Así, el registro de peso restante es útil para gestionar el tiempo de ejecución de los bloques, pero no resuelve por sí solo otro problema: garantizar que no se pague de más. Para ello necesitamos una instrucción relacionada con BuyExecution que cobre el peso sobrante y lo reembolse. Efectivamente, esta instrucción existe y se llama «Reembolsar resto». Utiliza un segundo registro llamado «registro de peso para reembolso», lo que evita reembolsar dos veces el mismo peso sobrante.

Control de flujo y excepciones

Hasta ahora, hemos mencionado dos registros de forma implícita, pero son fundamentales. El primero es el registro de programa, que almacena el programa XCVM en ejecución. El segundo es el contador de programa, que guarda el índice de la instrucción actual. Cuando cambia el registro de programa, este contador se reinicia a cero y se incrementa en uno tras ejecutar cada instrucción correctamente.

Gestionar situaciones excepcionales es clave para escribir código robusto. Cuando ocurre algo inesperado (o realmente impredecible) en un sistema remoto, necesitamos un mecanismo para manejarlo, aunque solo sea para informar al estado original.

Aunque el conjunto de instrucciones del XCVM no incluye bifurcaciones genéricas explícitas, su modelo de ejecución sí incorpora un marco general para manejar excepciones. El XCVM cuenta con otros dos registros de código, cada uno de los cuales almacena un programa XCVM, igual que el registro de programa. Se llaman registro de apéndice y registro de manejador de errores. Si conoces los sistemas de excepciones try/catch/finally de lenguajes populares, te resultará familiar.

Como mencionamos, la ejecución de un programa XCVM avanza paso a paso, instrucción por instrucción. Al llegar al final del programa, pueden ocurrir dos cosas: que termine con éxito o que se produzca un error. En el primer caso, tras una ejecución exitosa, se borra el registro de errores y su peso se añade al registro de peso restante. Además, se borra el registro de apéndice y su contenido se coloca en el registro de programa. Si el registro de programa queda vacío, la ejecución se detiene; si no, el contador de programa se reinicia a cero. En resumen, descartamos el programa actual y el manejador de errores, y, si existe, comenzamos a ejecutar el programa de apéndice.

Esta funcionalidad por sí sola no es muy útil, pero cobra valor cuando se combina con lo que ocurre al producirse un error. En ese caso, el peso de las instrucciones pendientes se añade al registro de peso restante. El registro de manejador de errores se borra y su contenido se coloca en el registro de programa, mientras que el contador de programa se reinicia a cero. Básicamente, descartamos el programa actual y empezamos a ejecutar el manejador de errores. Como no se borra el registro de apéndice, este se ejecutará tras una finalización exitosa, a menos que el propio manejador de errores lo restablezca.

Gracias a su estructura compuesta, permite un «anidamiento» arbitrario de manejadores de errores: si es necesario, un manejador de errores puede tener su propio manejador, y un apéndice puede tener su propio apéndice.

Existen dos instrucciones para manipular estos registros: SetAppendix y SetErrorHandler. La primera establece el registro de apéndice y la segunda el de manejador de errores. Cada una tiene un peso predictivo ligeramente superior al de sus parámetros. Sin embargo, durante la ejecución, el peso del mensaje XCM que se reemplaza en el registro se añade al registro de peso restante, lo que permite recuperar el peso asociado a cualquier apéndice o manejador de errores no utilizado.

Lanzar errores

A veces puede ser útil forzar un error y personalizar algunos de sus aspectos. Esto ya se usa al escribir código de pruebas, pero eventualmente podría tener aplicaciones en cadenas en producción. En el XCVM, esto se logra con la instrucción Trap, que siempre provoca un error. El tipo de error lanzado se llama Trap. Tanto la instrucción como el error llevan un parámetro entero, lo que permite transmitir información entre quien lanza el error y los observadores externos.

Este es un ejemplo sencillo:

Este «trap» hace que se omita la instrucción final DepositAsset, mientras que se ejecuta la instrucción DepositAsset del manejador de errores, depositando 1 DOT (menos los costes de ejecución) en la parachain 2000. Por ello, es preferible utilizar RefundSurplus al inicio del código del manejador de errores, ya que si este se activa, es muy probable que el peso previsto (y, por tanto, el adquirido) haya sido sobreestimado.

Informes de error

La capacidad de incluir código para manejar errores es muy útil, pero una funcionalidad muy solicitada es la de poder informar al remitente original sobre el resultado de un mensaje XCM. La instrucción QueryResponse permite que un sistema de consenso informe a otro; solo falta poder insertar el resultado del mensaje XCM en dicha instrucción y enviarla a quien desee ser notificado.

Existe una instrucción específica para esta tarea: ReportError. Funciona utilizando un registro que aún no hemos mencionado: el «registro de errores». Este registro es opcional (puede estar establecido o vacío). Cuando está establecido, contiene dos datos: un índice numérico y un tipo de error XCM.

Su funcionamiento es muy sencillo. Primero, se establece cada vez que una instrucción provoca un error; el tipo de error se fija como el tipo de dicho error, y el índice numérico se establece con el valor del registro del contador de programa. Segundo, solo se vacía cuando se ejecuta la instrucción ClearError. Esta instrucción es absolutamente fiable, ya que nunca provoca un error por sí misma. Se establece cuando ocurre un error y se vacía al ejecutar la instrucción adecuada.

Así queda claro cómo funciona ReportError: simplemente construye una instrucción QueryResponse con el contenido del registro de errores y la envía a un destino específico. Cualquier error anterior hará que esta instrucción se omita, ya que la ejecución salta primero al código del manejador de errores y luego al apéndice. La solución es sencilla: colocar ReportError en el apéndice garantiza su ejecución, independientemente de si el código principal provoca un error o no.

Veamos un ejemplo sencillo. Transferiremos un activo (1 DOT) desde la cadena de relé a Statemint (parachain 1000), donde compraremos tiempo de ejecución, y luego, usando Statemint como reserva, depositaremos el activo en la parachain 2000. El mensaje original (sin informe de errores) sería así:

Con un informe básico de errores, usaríamos lo siguiente:

Como se puede observar, el único cambio es la introducción de dos instrucciones SetAppendix, para asegurar que cualquier error o ausencia en Statemint y en la parachain 2000 se reporte a la cadena de relé. Esto supone que la cadena de relé ya está configurada para reconocer y procesar mensajes QueryResponse procedentes de Statemint y de la parachain 2000, con un ID de consulta igual a 42 y un límite de peso de 10 millones. Afortunadamente, Substrate ofrece un buen soporte para esto, aunque actualmente queda fuera del alcance de este documento.

Trampa de activos

Cuando ocurre un error en un programa que maneja activos (algo común, ya que suelen necesitar pagar los costes de ejecución de BuyExecution), el problema se agrava. Podría darse el caso de que la propia instrucción BuyExecution falle, quizás por un límite de peso incorrecto o por falta de activos para el pago. O bien, el activo podría enviarse a una cadena incapaz de procesarlo correctamente. En estos casos, al finalizar la ejecución del XCVM, el activo permanece en el «Registro de Tenencia» y, al igual que otros registros, estos activos son transitorios y se espera que se olviden.

Los equipos y sus usuarios se alegrarán al saber que XCM de Substrate permite a las cadenas evitar por completo esta pérdida. Este mecanismo opera en dos pasos. Primero, cuando se elimina cualquier activo del «Registro de Tenencia», no se olvida completamente. Si, al detenerse el XCVM, el «Registro de Tenencia» no está vacío, se emite un evento que contiene tres datos: el valor del «Registro de Tenencia»; el valor original del «Registro de Origen»; y el hash de ambos. Luego, el sistema XCM de Substrate almacena dicho hash. Esta parte del mecanismo se denomina «trampa de activos».

Sistema de reclamaciones

El segundo paso de este mecanismo permite solicitar contenidos previos del «Holding Register». En realidad, esto no se logra con una instrucción específica, sino mediante la instrucción genérica ClaimAsset, que aún no hemos analizado. A continuación, su declaración en Rust:

El nombre de esta instrucción puede recordar a otras de «financiación» que ya conocemos, como WithdrawAsset y ReceiveTeleportedAsset. Al igual que ellas, intenta colocar los activos (definidos por el parámetro assets) en el «Holding Register». Sin embargo, a diferencia de WithdrawAsset —que reduce el saldo en cadena de la cuenta—, ClaimAsset busca reclamaciones válidas para esos activos, independientemente del valor del «Origin Register». Para ayudar al sistema a encontrar dichas reclamaciones, se puede aportar información adicional mediante el parámetro ticket. Si se localiza una reclamación válida, esta se elimina de la cadena y los activos correspondientes se añaden al «Holding Register».

Lo que constituye una reclamación válida depende por completo de la cadena en cuestión. Diferentes cadenas pueden soportar distintos tipos de reclamaciones, y Substrate permite combinarlas con facilidad. Como ya habrás intuido, existe una reclamación específica ya preparada: precisamente, los contenidos previos descartados del «Holding Register».

Veamos cómo funciona en la práctica. Imaginemos que nuestra parachain 2000 envía un mensaje a Statemint: extrae 0,01 DOT de su cuenta soberana para cubrir las tarifas y notifica al mismo tiempo que ha transferido 100 unidades de su token nativo a la cuenta soberana de Statemint. Esto se ilustra en la siguiente figura:

Suponiendo que 0,01 DOT basten para las tarifas y que Statemint admita depósitos en cadena de los activos nativos de la parachain 2000 (usándola como reserva), la operación debería salir bien. Pero puede que Statemint aún no esté configurada para reconocer esos activos nativos. En ese caso, DepositAsset no sabría cómo procesarlos y arrojaría un error. Tras ejecutar el apéndice correspondiente —que notificará el fallo a la parachain 2000—, quedarán 100 unidades de los activos nativos de la parachain 2000, y posiblemente algunos DOT, en el «Holding Register». Si la tarifa fue de solo 0,005 DOT, quedaría un remanente de 0,005 DOT.

A continuación, el panel XCM de Statemint registrará un evento que indica la existencia de estos nuevos activos reclamables, por ejemplo:

Luego se enviará un mensaje de vuelta a la parachain 2000, como se muestra a continuación:

Posteriormente, la parachain 2000 podrá recuperar estas 100 unidades mediante un método bastante sencillo —quizás una vez que determine que Statemint ya puede aceptar depósitos de sus activos nativos—:

En este caso, el parámetro ticket no aporta información especial para localizar la reclamación. Esto es habitual en las reclamaciones de tipo «trampa de activos», aunque en otros tipos su uso podría ser necesario.

Conclusión

Esperamos que este contenido te ayude a comprender mejor la máquina virtual subyacente de XCM y cómo puede asistirte en la gestión y recuperación ante situaciones inesperadas. El próximo artículo de esta serie abordará las direcciones futuras de XCM, cómo proponer mejoras al formato, y profundizará en la implementación de XCM en Rust para Substrate, así como en su uso para que una cadena interprete fácilmente los mensajes XCM.