En los dos primeros artículos que escribí sobre XCM, introduje los conceptos básicos de su diseño y estructura de versiones. En este artículo, analizaremos en profundidad su modelo de ejecución y diseño subyacente. Dado que XCM se basa en el conjunto de instrucciones de XCVM, una máquina virtual de muy alto nivel, esto equivale a familiarizarse con esta arquitectura de máquina.
XCVM es una máquina virtual completa no Turing, de muy alto nivel. Se basa en registros (en lugar de en pilas) y tiene varios registros de propósitos especiales, la mayoría de los cuales contienen datos muy estructurados. A diferencia de los procesadores de propósito general, los registros de XCVM no pueden establecerse libremente en valores arbitrarios, pero tienen una mecánica estricta que rige cómo pueden cambiar. Más allá de ciertos medios para interactuar con el estado de la cadena local (como las instrucciones WithdrawAsset
y DepositAsset
que ya hemos visto), no hay “memoria” adicional. No hay posibilidad de looping (bucle) ni instrucciones de bifurcación explícitas.
Ya hemos sido introducidos a dos de los registros: el Holding Register (Registro de Tenencia), que puede contener temporalmente uno o más activos y se puede completar retirando un activo de la cadena local, o bien recibiendo un activo de una fuente externa de confianza (por ejemplo, otra cadena); y el Origin Register (Registro de Origen), que al comienzo de la ejecución contiene la ubicación del sistema de consenso desde el cual se originó la ejecución actual de XCM, y solo se puede mutar a una ubicación interior o borrar por completo.
De los otros registros, tres están relacionados con la gestión de excepciones / errores y dos con el seguimiento del peso de ejecución. Conoceremos todos ellos en este artículo.
🎬 Modelo de Ejecución
Como ya se mencionó, no hay instrucciones explícitamente condicionales o primitivas de bucle (looping primitives) que permitan volver a ejecutar la misma instrucción más de una vez. Esto hace que sea bastante trivial predeterminar el flujo de control de un programa. Esta propiedad es útil dado que queremos determinar cuánto tiempo de ejecución (conocido como peso en todo Substrate/Polkadot) podría utilizar un mensaje XCM antes del punto de ejecución.
La mayoría de las plataformas de consenso que esperamos que ejecute XCM deberán poder determinar el peor de los casos de un tiempo de ejecución (worst case execution time) antes del comienzo de la ejecución. Esto se debe a que las blockchains generalmente necesitan asegurarse que los bloques individuales no tarden más en procesarse que un límite predeterminado para que no se detenga el sistema en su conjunto. Adicionalmente, si el sistema necesita el pago de la tarifa, entonces debe suceder necesariamente antes de la carga de trabajo por la que se realiza el pago y es importante que este pago cubra el tiempo de ejecución del peor de los casos.
Los sistemas que permiten lenguajes completos de Turing (por ejemplo, Ethereum) en realidad no pueden calcular el tiempo de ejecución del programa en el peor de los casos debido directamente a esta completitud de Turing. Lo resuelven exigiendo al usuario que predetermine los recursos de ejecución del programa y luego midiéndolo a medida que se ejecuta e interrumpiéndolo en caso de que exceda la cantidad pagada. A veces, las cosas cambian antes de que se ejecute la transacción y el peso se vuelva incorrecto. Afortunadamente, las máquinas virtuales como la XCVM que no son Turing-complete pueden evitar la necesidad de esta medición y prescripción de peso.
🏋️♀️ Peso
El peso se representa típicamente como el número entero de picosegundos que necesitaría una pieza de hardware representativo para ejecutar la operación dada. Como hemos visto con la instrucción BuyExecution
, el XCVM incluye este concepto de tiempo de ejecución/ peso cuando se trata de ciertas instrucciones.
No hay medición de peso, pero para permitir la posibilidad de que un programa XCVM finalmente tome menos de la predicción de peso del peor de los casos, tenemos un registro llamado Surplus Weight Register (Registro de peso excedente). La mayoría de las instrucciones no lo tocan, ya que podemos predecir con precisión cuánto peso usarán. Sin embargo, ocasionalmente existen circunstancias en las que la predicción de ponderación del peor de los casos es una sobreestimación y solo en el momento de la ejecución sabemos cuánto. Si bien se tiene en cuenta el tiempo de ejecución del bloque con una sobreestimación del peso del mensaje XCM, el seguimiento de la cantidad en la que el peso original es una sobreestimación y restarlo de las cuentas permite a la cadena optimizar su cuota de tiempo de ejecución del bloque.
Por lo tanto, Surplus Weight Register (Registro de peso excedente) es útil para nuestra contabilidad del tiempo de ejecución del bloque, pero no resuelve por sí solo el otro problema de garantizar que la cantidad pagada no sea una sobreestimación. Para ello, necesitamos una instrucción complementaria a BuyExecution
, que toma cualquier exceso de peso y lo reembolsa. Naturalmente, esta instrucción existe y se llama RefundSurplus
. Hay un segundo registro que utiliza llamado Refunded Weight Register (Registro de peso reembolsado), lo que garantiza que el mismo exceso de peso no se reembolse varias veces.
😱 Control de Flujo y Excepciones
Dos registros más han estado bastante implícitos en nuestro tratamiento del XCVM hasta ahora, pero, no obstante, es importante conocerlos. En primer lugar, está el Programme Register (Registro de Programa) que almacena el programa XCVM que se está ejecutando actualmente. En segundo lugar, está el Programme Counter (Contador de Programa), que almacena el índice de instrucción en ejecución. Esto se restablece a cero cuando se cambia el Programme Register (Registro de Programa) y se incrementa en uno al final de cada instrucción ejecutada con éxito.
La capacidad de manejar la posibilidad de una circunstancia “excepcional” es crucial para escribir un código robusto. Cuando sucede algo en un sistema remoto que no esperabas (o de hecho no podrías haber predicho), entonces necesitas alguna forma de administrarlo, incluso si es simplemente enviar un informe de regreso al origen indicando eso.
Si bien el conjunto de instrucciones XCVM no incluye ninguna instrucción general explicita con propósito de bifurcación, sí tiene un marco general de manejo de excepciones integrado en su modelo de ejecución. El XCVM incluye dos registros de código más, cada uno con un programa XCVM como el Programme Register (Registro de Programa). Estos dos registros se denominan Appendix Register (Registro de Apéndice) y Error Handler Register (Registro de Controlador de Errores). Si estás familiarizado con el sistema de excepción try / catch / finally en varios idiomas populares, lo que sigue puede parecerte bastante evocador.
Como se mencionó, la ejecución de un programa XCVM sigue cada instrucción, paso a paso. A medida que siga estas instrucciones hasta el final del programa, sucederá una de dos cosas: o llegará al final del programa correctamente o se producirá un error. En el primer caso de ejecución exitosa, el Error Register (Registro de Error) se borra y su peso se agrega al Surplus Weight Register (Registro de Peso Excedente). El Appendix Register (Registro de Apéndice) también se borra y su contenido se coloca en el Programme Register (Registro del Programa). Si el Programme Register se deja vacío, nos detenemos. De lo contrario, el Programme Counter (Contador del programa) vuelve a cero. En pocas palabras, descartamos el Current Programme y el Error Handler y comenzamos a ejecutar el Appendix Programme, si lo hay.
Esta funcionalidad no es tan útil por sí sola, pero puede ser útil cuando se combina con lo que sucede en caso de error. Aquí, el peso de cualquiera de las instrucciones que aún no se han ejecutado se agrega al Surplus Weight Register (Registro de Peso Excedente). El Error Handler Register (Registro del Controlador de Errores) se borra, su contenido se coloca en el Programme Register (Registro del Programa) y el Programme Counter (Contador del Programa) se restablece a cero. En pocas palabras, descartamos el Current Programme (Programa Actual) y comenzamos a ejecutar el Error Handler (controlador de errores). Debido a que no borramos el Appendix Register (Registro del Apéndice), a menos que el Error Handler (Controlador de Errores) lo restablezca, se ejecutará una vez que finalice correctamente.
Debido a su estructura de composición, permite el “anidamiento” arbitrario de los controladores de errores: los controladores de errores pueden, si se desea, también tener controladores de errores y los apéndices pueden tener sus propios apéndices.
Hay dos instrucciones que permiten manipular estos registros: SetAppendix
y SetErrorHandler
. Como podrías esperar, uno de ellos establece el Appendix Register (Registro de Apéndice) y el otro el Error Handler Register (Registro de Controlador de Errores). El peso previsto de cada uno de estos es una pequeña cantidad más que el peso de su parámetro. Sin embargo, cuando se ejecuta, el peso del mensaje XCM en el registro que será reemplazado se agrega al Surplus Weight Register (Registro de Peso Excedente), lo que permite recuperar el peso de cualquier apéndice no utilizado o controlador de errores.
☄️ Errores de Lanzamiento
A veces, puede ser útil asegurarse de que ocurra un error y personalizar algún aspecto de ese error. Esto se ha utilizado al escribir código de prueba, pero no es imposible que eventualmente encuentre uso dentro de una cadena activa. Esto se puede hacer en el XCVM a través de la instrucción Trap
que siempre da como resultado un error. El tipo de error que se lanza comparte el nombre Trap
. Tanto la instrucción como el error llevan un argumento entero que permite que se pase algún tipo de información entre el lanzador del error y un espectador externo.
Aquí tienes un ejemplo trivial:
El Trap
hace que se omita el DepositAsset
final y, en su lugar, se ejecute el DepositAsset
del controlador de errores, colocando 1 DOT (menos el costo de ejecución) bajo la propiedad de la parachain 2000. Siempre tendremos a usarRefundSurplus
al comienzo de un código de controlador de errores ya que si está funcionando sabemos que es probable que el peso predicho utilizado (y por tanto el peso comprado) sea una sobreestimación.
🗞 Informe de Errores
Ser capaz de introducir código para controlar errores es muy útil, pero una característica que se solicita con frecuencia es poder informar el resultado de un mensaje XCM al remitente original. Cumplimos con la instrucción QueryResponse
en el artículo anterior que permite que un sistema de consenso reporte alguna información a otro, todo lo que queda es poder insertar de alguna manera el resultado del XCM en esta QueryResponse
y enviarlo a quien esté esperando que le digan del resultado.
Resulta que hay precisamente una instrucción que hace eso llamado ReportError
. Funciona utilizando un registro que aún no hemos encontrado: el Error Register (Registro de errores). El Error Register es un tipo opcional (puede establecerse o borrarse). Si está configurado, entonces contiene dos piezas de información: un índice numérico y un tipo de error XCM.
Tiene una mecánica de funcionamiento extremadamente simple. En primer lugar, siempre se establece cuando una instrucción da como resultado un error; el tipo de error se establece en el tipo de ese error y el índice numérico se establece en el valor del Programme Counter Register (Registro de Contador de Programa). En segundo lugar, se borra solo cuando se ejecuta la instrucciónClearError
. Esta instrucción es una de las instrucciones infalibles; nunca se permite que resulte en un error. Eso es todo: se configura cuando ocurre un error y se borra cuando emite la instrucción adecuada.
Ahora debería quedar claro cómo funciona la instrucción ReportError
: simplemente compone una instrucción QueryResponse
utilizando el contenido del Error Register (Registro de Errores) y la envía a un destino en particular. Por supuesto, cualquier error que ocurra antes resultaría en la omisión de la instrucción ya que la ejecución salta primero al código del Error Handler Register (Registro del Controlador de Errores) y luego al código del Appendix Register (Registro del Apéndice). Sin embargo, la solución a esto es trivial: colocar ReportError
en el apéndice asegurará que se ejecute independientemente de si el código principal resultó en un error de ejecución.
Echemos un vistazo a un ejemplo sencillo. Teletransportaremos un activo (1 DOT) de la Relay Chain a Statemint (parachain 1000), compraremos algo de tiempo de ejecución allí y luego, utilizando Statemint como reserva, depositaremos el activo en la parachain 2000. El mensaje original (sin informes de errores ) se vería así:
Con el informe básico de errores, en su lugar, usaríamos esto:
WithdrawAsset((Here, 10_000_000_000).into()), InitiateTeleport { assets: All.into(), dest: Parachain(1000).into(), xcm: Xcm(vec![ BuyExecution { fees: (Parent, 10_000_000_000).into(), weight: Unlimited, }, SetAppendix(Xcm(vec![ ReportError { query_id: 42, dest: Parent.into(), max_response_weight: 10_000_000, }, ])), DepositReserveAsset { assets: All.into(), max_assets: 1, dest: ParentThen(Parachain(2000)).into(), xcm: Xcm(vec![ BuyExecution { fees: (Parent, 10_000_000_000).into(), weight: Unlimited, }, SetAppendix(Xcm(vec![ ReportError { query_id: 42, dest: Parent.into(), max_response_weight: 10_000_000, }, ])), DepositAsset { assets: All.into(), max_assets: 1, beneficiary: ParentThen(Parachain(2000)).into(), }, ]), }, ]), }
Como puedes ver, el único cambio es la introducción de dos instrucciones SetAppendix
que aseguran que el error o la falta del mismo dentro de Statemint y la parachain 2000 se informará a la Relay Chain. Esto supone que la Relay Chain se ha configurado para poder reconocer y manejar mensajes QueryResponse
que se originan en Statemint y la parachain 2000 con ID de consulta 42 y un límite de peso de diez millones. Afortunadamente, esto es algo que Substrate soporta bien, pero fuera de alcance en este momento.
La Trampa de los Activos
Cuando se producen errores durante los programas que se ocupan de los activos (como hace la mayoría, ya que a menudo tendrán que pagar por su ejecución con BuyExecution
), puede ser muy problemático. Puede haber casos en los que la instrucción BuyExecution
en sí misma dé como resultado un error, quizás porque el límite de peso era incorrecto o los activos utilizados para el pago eran insuficientes. O quizás un activo se envía a una cadena que no puede manejarlo de una manera útil. En estos casos, entre muchos otros, la ejecución de XCVM del mensaje finaliza con los activos que quedan en el Holding Register, que al igual que los otros registros son transitorios y esperaríamos que sean olvidados.
Los equipos y sus usuarios estarán felices de saber que el XCM de Substrate permite que las cadenas eviten esta pérdida por completo 🎉. El mecanismo funciona en dos pasos. Primero, cualquier activo en el Holding Register cuando se liquida no se olvida por completo. Si el Holding Register no está vacío cuando el XCVM se detiene, entonces se emite un evento que contiene tres piezas de información: el valor del Holding Register; el valor original del Origin Register; y el hash de estas dos piezas de información. A continuación, el sistema XCM de Substrate almacena este hash. Esta parte del mecanismo se llama Asset Trap (trampa de activos).
🎟 El Sistema de Reclamaciones
El segundo paso del mecanismo es poder reclamar algunos contenidos previos del Holding Register. En realidad, esto no sucede a través de algo especialmente diseñado para este propósito, sino a través de una instrucción de propósito general que aún no hemos cumplido llamada ClaimAsset
. Así es como se declara en Rust:
El nombre de esta instrucción puede parecer una reminiscencia de otras instrucciones de “financiación” que hemos reunido, como WithdrawAsset
y ReceiveTeleportedAsset
. Si es así, entonces es por una muy buena razón: lo es. Como los demás, intenta colocar los activos (dado por el ssets
argument aquí) en el Holding Register. A diferencia, por ejemplo, WithdrawAsset
, que reduce el balance de activos on chain (en cadena) de una cuenta, ClaimAsset
busca un reclamo válido para estos assets
disponibles para cualquiera que sea el valor del Origin Register. Para ayudar al sistema a encontrar el reclamo válido, se puede proporcionar información a través de la discusión del ticket.
Si se encuentra un reclamo válido, entonces se elimina de la cadena y los activos se agregan al Holding Register.
Ahora bien, exactamente lo que constituye un reclamo depende completamente de la propia cadena. Diferentes cadenas pueden admitir diferentes tipos de reclamos, y Substrate te permite componerlos fácilmente. Pero, como puedes adivinar, un tipo particular de reclamo que viene listo para funcionar, por supuesto, es el de los contenidos de Holding Register previamente eliminados.
Así que echemos un vistazo a cómo podría funcionar esto en la práctica. Supongamos que nuestro usuario de la parachain 2000 envía un mensaje a Statemint en el que retira 0.01 DOT de su cuenta soberana para pagar las tarifas y también le notifica de una transferencia de activos de reserva de 100 unidades de su propio token nativo para que se coloquen en su cuenta soberana en Statemint. Podría verse algo como esto:
Suponiendo que 0.01 DOT son tarifas suficientes para esto y que Statemint admite depósitos en cadena del activo nativo de la parachain 2000 (además de usar la parachain 2000 como reserva para eso), entonces esto debería funcionar bien. Sin embargo, quizás Statemint aún no se haya configurado para reconocer el activo nativo de la parachain 2000. En este caso, elDepositAsset
no sabrá qué hacer con el activo y, en consecuencia, arrojará un error. Después de ejecutar el apéndice que notificará a la parachain 2000 de esta falla, entonces nos quedaremos con las 100 unidades de los activos nativos de la parachain 2000, así como potencialmente algunos DOT en el Holding Register. Supongamos que las tarifas solo ascendieron a 0.005 DOT, dejando un remanente de 0.005 DOT.
Luego habría un evento registrado por la paleta XCM de Statemint sobre estos activos recientemente reclamables, algo como:
Se enviaría un mensaje a la parachain 2000 con el siguiente aspecto:
La Parachain 2000 podría en una etapa posterior (tal vez una vez que haya determinado que Statemint puede aceptar depósitos de su activo nativo), reclamar esas 100 unidades con un método bastante simple:
En este caso, no se proporciona información especial a través del ticket argument (discusión del ticket) para ayudar a localizar el reclamo. Por lo general, esto está bien para las Asset Trap Claims (reclamaciones de trampa de activo), aunque puede ser necesario usarlo para otros tipos de reclamaciones.
🏁 Conclusión
Así que eso es todo por ahora. Espero que esto haya sido fundamental para ayudarte a comprender más sobre la máquina virtual subyacente de XCM y cómo puede ayudarte a administrar y recuperarte de situaciones inesperadas. Los próximos artículos de esta serie cubrirán direcciones futuras en XCM y cómo se pueden sugerir mejoras al formato, así como profundizar en la implementación de XCM Rust de Substrate y cómo podemos usarlo para proporcionar una cadena con la capacidad de interpretar fácilmente XCM.