Saltar al contenido principal

Dialecto del Contrato Rust

El desarrollo de contratos ocurre en el lenguaje de programación Rust, pero varias características del lenguaje Rust están ya sea no disponibles en el entorno de invitado de despliegue, o no se recomiendan porque su uso incurriría en costos inaceptables en tiempo de ejecución.

Por esta razón, tiene sentido considerar el código escrito para contratos como un dialecto o variante especial del lenguaje de programación Rust, con ciertas restricciones y prioridades inusuales, tales como el determinismo y el tamaño del código.

Estas restricciones y prioridades son similares a las que se encuentran al escribir código Rust para "sistemas embebidos", y las herramientas, bibliotecas y técnicas utilizadas en el "dialecto del contrato" se toman frecuentemente de la comunidad de sistemas embebidos de Rust, y por defecto se recomienda que los contratos se desarrollen con el modo #[no_std] que excluye completamente la biblioteca estándar de Rust, confiando en la más pequeña biblioteca subyacente core.

Nota: estas restricciones y prioridades no se aplican al construir en modo de prueba local, y de hecho, las pruebas de contratos locales frecuentemente usarán instalaciones -- para generar entradas de prueba, inspeccionar salidas de prueba y guiar las pruebas -- que no están admitidas en el entorno de invitado de despliegue. Los desarrolladores deben entender la diferencia entre el código que está compilado en módulos de Wasm para despliegue y el código que se compila condicionalmente para pruebas. Consulta depuración de contratos para más detalles.

El "dialecto del contrato" tiene las siguientes características:

No puntos flotantes

La aritmética de punto flotante en el invitado está completamente prohibida. Las operaciones de punto flotante en Wasm tienen algunos aspectos no deterministas o específicos de la plataforma: principalmente patrones de bits NaN, así como configuraciones del entorno de punto flotante como el modo de redondeo.

Si bien es teóricamente posible forzar todo el código de punto flotante a un comportamiento determinista a través de las implementaciones de Wasm, hacerlo en algunas implementaciones de Wasm puede ser difícil, costoso o propenso a errores. Para evitar la complejidad, todo el código de punto flotante es rechazado en el momento de la instanciación.

Esta restricción puede ser revisitada en una versión futura.

Asignación de memoria dinámica limitada (preferiblemente cero)

La asignación de memoria dinámica dentro del invitado es fuertemente desaconsejada, pero no completamente prohibida.

El repertorio de objetos host y funciones host ha sido diseñado para liberar al invitado de tener que realizar asignación dinámica dentro de su propia memoria lineal; en su lugar, se espera que el invitado asigne estructuras dinámicas dentro de objetos host e interactúe con ellas utilizando manejadores livianos.

Usar objetos host en lugar de estructuras de datos en la memoria del invitado tiene numerosos beneficios: rendimiento mucho más alto, tamaño de código mucho menor, interoperabilidad entre contratos, soporte compartido del host para serialización, depuración e introspección de estructuras de datos.

Sin embargo, el invitado tiene una pequeña memoria lineal disponible en casos donde la asignación dinámica de memoria es necesaria. Usar esta memoria conlleva costos: el invitado debe incluir en su código una copia completa de un asignador de memoria, y debe pagar el costo de tiempo de ejecución de ejecutar el código del asignador dentro de la VM.

Esta restricción se debe a la limitada capacidad de Wasm para soportar compartición de código: no hay una manera estándar para que el sandbox de Wasm proporcione código compartido de "biblioteca estándar" dentro de un invitado, como un asignador de memoria, ni el host tiene un conocimiento adecuado sobre el contenido de la memoria del invitado para proporcionar un asignador por sí mismo. Cada contrato que desee usar asignación dinámica debe, por lo tanto, llevar su propia copia de un asignador.

Muchas instancias en las que la asignación de memoria dinámica podría parecer ser necesaria también pueden ser atendidas igual de bien con una biblioteca como heapless. Esta biblioteca (y otras de su tipo) proporcionan estructuras de datos con API familiares que aparentan ser dinámicas, pero en realidad están implementadas en términos de una sola pila o asignación estática, con un tamaño máximo fijo establecido en la construcción: los intentos de ampliar el tamaño dinámico más allá del tamaño máximo simplemente fallan. En el contexto de un contrato, este comportamiento puede ser preferible y evita la cuestión de la asignación dinámica por completo.

I/O no estándar

Todas las instalaciones de I/O estándar y el acceso al sistema operativo que un programa Rust típico esperaría realizar utilizando la biblioteca estándar de Rust están prohibidos; los programas que intenten importar tales funciones del host a través de (por ejemplo) la interfaz WASI fallarán en instanciarse, ya que se refieren a funciones no proporcionadas por el host.

No hay sistema operativo, ni ninguna simulación del mismo, presente en el sandbox del contrato. Nuevamente, el repertorio de objetos host y funciones host está destinado a reemplazar y, en gran medida, desvirtuar la necesidad de tales instalaciones de la biblioteca estándar.

Esta restricción surge del hecho de que los contratos necesitan ejecutarse con garantías más fuertes que las ofrecidas por las API típicas de sistemas operativos. Específicamente, los contratos deben realizar I/O con semánticas transaccionales de todo o nada (relativas a su ejecución exitosa o falla) así como consistencia serializable. Esto elimina la mayoría de las API que estarían relacionadas con el I/O típico de archivos. Además, los contratos deben estar aislados de todas las fuentes de no determinismo, como redes o control de procesos, lo que elimina la mayoría de las API restantes. Una vez que se eliminan archivos, redes y control de procesos, simplemente no queda suficiente en las instalaciones estándar de I/O del sistema operativo para molestarse en intentar proporcionarlas.

No hay multihilo

El multihilo no está disponible. Al igual que con las funciones de I/O, intentar importar cualquier API del host relacionada con el multihilo fallará en el momento de la instanciación.

Esta restricción se basa de manera similar en la necesidad de que los contratos se ejecuten en un entorno con fuertes garantías de determinismo y consistencia serializable.

Pánico inmediato

La instalación panic!() de Rust para errores irrecuperables detendrá inmediatamente la máquina virtual Wasm, deteniendo la ejecución en la instrucción que captura en lugar de deshacer. Esto significa que el código Drop en tipos de Rust no se ejecutará durante un pánico. Este comportamiento es similar al perfil panic = "abort" con el que se puede (y a menudo se compila) el código Rust.

Esta no es una restricción dura impuesta por el host, sino una configuración suave realizada a través de una mezcla de funciones de SDK y banderas utilizadas al compilar, con el interés de minimizar el tamaño del código y limitar los costos de ejecución. Se puede eludir con cierto esfuerzo si se desea deshacer y que el código Drop se ejecute, a costa de un aumento significativo en el tamaño del código.

Colecciones puras-funcionales

Los objetos host tienen una semántica significativamente diferente a las estructuras de datos típicas de Rust, especialmente aquellas que implementan colecciones como mapas y vectores.

En particular: los objetos host son inmutables, y cualquier "modificación" a un objeto host devuelve una nueva copia completa del objeto, dejando el inicial sin cambios. En su mayor parte, esta distinción está oculta a través de wrappers en el SDK, de tal manera que objetos como Map o Vec parecen al programador de contratos ser valores mutables de propiedad única similares a los tipos de la biblioteca estándar de Rust, pero los objetos host subyacentes son inmutables, por lo que tienen características de rendimiento diferentes. Específicamente: clonar tal objeto es O(1), mientras que cualquier modificación es O(N). Dado que la mayoría de los objetos host son típicamente muy pequeños, el costo O(N) de la modificación suele ser más barato que cualquier implementación alternativa que involucre subestructuras compartidas.

Nota: estos tipos de contenedores Vec y Map no deberían ser utilizados para gestionar colecciones de datos grandes o no limitadas. Para tales casos, los contratos deben almacenar datos en múltiples entradas de ledger separadas, cada una con su propia clave única definida por el contrato. Hacerlo también limita el costo de IO de un contrato a solo las entradas que accede, y además permite la modificación concurrente de entradas con claves separadas de transacciones separadas.

Características limitadas de Webassembly

La especificación de Webassembly ha crecido significativamente desde su introducción inicial y ahora admite muchas características que pueden o no estar disponibles en una implementación dada de Webassembly.

Soroban limita intencionalmente las características de Webassembly que admite, para minimizar la superficie crítica de seguridad y mantener la flexibilidad en la elección de implementaciones de Webassembly.

El objetivo wasm32-unknown-unknown del compilador Rust utilizado para Webassembly usaba solo un subconjunto pequeño de características, hasta Rust 1.81. A partir de Rust 1.82, el objetivo wasm32-unknown-unknown añadió más características, lo que significa que Rust 1.82 generaba código que podría ser rechazado por Soroban debido al uso accidental de características más recientes de Webassembly.

A partir de Rust 1.84, se añadió un nuevo objetivo wasm32v1-none a Rust que se limita intencionalmente al subconjunto "Webassembly 1.0" de características, todas ellas admitidas por Soroban.

Como consecuencia, los nuevos contratos Soroban deben desarrollarse con Rust 1.84 o posterior y usar el objetivo wasm32v1-none, no wasm32-unknown-unknown. Si el contrato necesita versiones anteriores de Rust, o desea seguir usando wasm32-unknown-unknown, debe limitarse a Rust 1.81 o anterior.

El CLI de Stellar detecta automáticamente la versión de Rust y selecciona el objetivo apropiado al desarrollar contratos para Webassembly.

Nota: el objetivo wasm32v1-none no incluye, por defecto, ninguna versión de la biblioteca std. Esto se debe a que la versión de std que se incluye con wasm32-unknown-unknown es mayormente considerada un error por sus diseñadores: por ejemplo, la mayoría de las facilidades de E/S en esa versión de std son simulacros que no hacen nada o generan un pánico al ser llamados. Al añadir el objetivo wasm32v1-none, se decidió que esos simulacros no eran beneficiosos para los usuarios.

El objetivo wasm32v1-none incluye una copia del paquete alloc, que contiene la mayoría de los contenedores (como vectores y mapas) que son reexportados por std. Por ejemplo, en lugar de usar std::vec::Vec, puedes usar alloc::vec::Vec, que es el mismo código con un nombre diferente. Pero como se mencionó arriba en la sección sobre asignación dinámica de memoria, generalmente los contratos Soroban deberían evitar alloc también: un paquete como heapless suele funcionar mejor.