Desarrollo de Contratos Inteligentes en Near Usando Rust – Parte 1

(-2 nL)
11 min read
To Share and +4 nLEARNs

El 16 de febrero de 2023 di un taller en la Universidad de Waterloo sobre el desarrollo de contratos inteligentes en Near usando Rust. Disfruté armarlo y pensé que sería divertido presentar también el contenido aquí como una serie de publicaciones de blog. En esta primera publicación, daré una analogía para explicar cómo se parecen el desarrollo de blockchain con un patrón utilizado en aplicaciones web normales, presentaré el ejemplo de contrato inteligente que usaremos a lo largo de esta serie y discutiré algunos principios generales del desarrollo de contratos inteligentes que son exclusivos de blockchain en relación con otros dominios de la programación.

Un Modelo Mental Para Crear Una Aplicación Distribuida (dapp)

El propósito de esta sección es hacer una analogía entre el desarrollo sobre una cadena de bloques (las aplicaciones respaldadas por tecnología de cadena de bloques a menudo se denominan “dapps” en el ámbito) y una tecnología más común para aplicaciones web que puedes haber encontrado antes. Esta analogía puede ser útil al pensar en cómo los usuarios interactúan con los contratos inteligentes.

La idea es que las dapps son muy similares a las aplicaciones web basadas en una “arquitectura sin servidor”. El término “sin servidor” es un poco engañoso porque, por supuesto, los servidores todavía están involucrados, pero la razón del nombre es que el hardware subyacente (es decir, el servidor) que ejecuta el código se abstrae del desarrollador. Esto tiene beneficios sobre otras infraestructuras de computación en la nube en términos de costo y escalabilidad porque solo paga exactamente por los recursos que usa en lugar de pagar para ejecutar una VM que puede permanecer inactiva si el tráfico es bajo o puede dejar de responder si hay demasiado tráfico. Cada vez que un usuario interactúa con la aplicación web, se invoca una nueva instancia de la “función sin servidor” en el backend para atender la solicitud del usuario sin que el desarrollador tenga que pensar exactamente en qué hardware se ejecuta esta función.

Las dapps abstraen el hardware de manera similar. Un contrato inteligente se implementa en la cadena de bloques y se ejecuta en los nodos (servidores) que forman la red de persona – a – persona de esa cadena de bloques. Cuando un usuario interactúa con la dapp, realiza una llamada a la cadena de bloques (una transacción) para ejecutar el contrato inteligente. Cada transacción crea una nueva instancia del contrato inteligente (en el sentido de que no hay un estado en memoria que persista entre transacciones), al igual que con las funciones sin servidor.

A continuación se muestra una imagen tomada directamente del sitio web de Amazon Web Services (AWS) para Lambda (su versión de una opción informática sin servidor).

Es fácil modificar esta imagen para ver cómo el flujo de trabajo en una dapp es similar.

Otra similitud entre la computación sin servidor y los contratos inteligentes es el hecho de que cada transacción tiene un costo. En el caso de AWS se cobra a la cuenta de AWS del desarrollador los recursos consumidos, mientras que en el caso de blockchain se cobra a quien firma la transacción por su ejecución.

Con esta analogía como punto de referencia, analicemos el ejemplo del desarrollo de una dapp que usaremos a lo largo de esta serie.

Nuestro ejemplo: aplicación de chat basada en blockchain

El ejemplo que usaremos a lo largo de esta serie es una aplicación de chat basada en blockchain. Este no es un ejemplo del mundo real, en el sentido de que no existe un buen caso comercial para usar una cadena de bloques pública para chatear (en mi opinión). El hecho de que todos los mensajes sean completamente públicos e incluidos de forma irreversible en un registro permanente es un inconveniente, no una característica. Sin embargo, la razón para elegir este ejemplo es que ilustra varios conceptos importantes en el desarrollo de una dapp y, al mismo tiempo, es lógicamente fácil de seguir para cualquiera que haya usado algo como Facebook, Messenger, Telegram o Signal.

El código de este ejemplo está disponible en mi GitHub. El LÉAME (README) en ese repositorio brinda algunas instrucciones para configurar un entorno de desarrollo para interactuar con el código y una idea básica de cómo usar el contrato. En esta serie de publicaciones nos sumergiremos mucho más profundamente en el código y su funcionamiento.

Para fundamentar la discusión de los principios del desarrollo de contratos inteligentes, aquí hay una descripción general de cómo funciona el contrato de chat.

  • Cada individuo que quiere participar en la red de chat implementa su propia versión del contrato inteligente.
  • Cada instancia del contrato mantiene una lista de cuentas que conoce (contactos, solicitudes de contactos pendientes, etc.). También almacena los mensajes que ha recibido (y algunos metadatos sobre esos mensajes).
  • Para enviar un mensaje a otra persona, primero debe tenerla como “contacto”. Esto funciona como era de esperar: Alice le envía a Bob una solicitud de contacto, si Bob acepta, Alice y Bob se vuelven contactos entre sí, de lo contrario, no son contactos.
  • Cada instancia del contrato tiene un “propietario” que puede enviar mensajes y enviar/aceptar solicitudes de contacto.

Principios del desarrollo de contratos inteligentes

Hay tres conceptos relacionados que quiero enfatizar que son importantes para el desarrollo de contratos inteligentes, pero que pueden no aparecer en el desarrollo de software típico. Ellos son:

  1. una mentalidad adversaria
  2. economía
  3. asegurar invariantes antes de hacer llamadas de contratos cruzados

Una mentalidad adversaria

Lo primero que debe recordar al implementar en una cadena de bloques pública es que cualquier persona en todo el mundo puede interactuar con tu código. Si hay alguna acción delicada que pueda tomar tu contrato inteligente (por ejemplo, al enviar mensajes en el contrato de chat, seguramente no deseas que alguien pueda hacerse pasar por ti), entonces el contrato debe verificar explícitamente la autorización para que solo las cuentas autorizadas puedan realizar con éxito la acción (es por eso que nuestro contrato de chat tiene la propiedad “propietario – owner”). Si tiene algún método que toma entrada de texto, debe validarlo antes de continuar con cualquier lógica comercial porque cualquier usuario aleatorio podría enviar cualquier entrada de texto que desee. De hecho, la idea de una mentalidad contradictoria va aún más allá; un usuario no solo puede enviar una entrada de texto basura, sino que también puede crear una entrada cuidadosamente para desencadenar una vulnerabilidad en su código. La única forma de evitar que esto suceda es no tener tales vulnerabilidades en primer lugar.

De manera similar, la lógica del contrato inteligente a menudo depende de algún protocolo para coordinar diferentes componentes (por ejemplo, el protocolo para agregar contactos en nuestro contrato de chat). ¿Un usuario tiene poder en este protocolo? ¿Qué pasa si no lo siguen correctamente? Estas son preguntas que debes responder al desarrollar un contrato inteligente porque los piratas informáticos intentarán explotar tu contrato.

Para resumir, siempre debes asumir que cualquier entrada externa es bizantina y verificar explícitamente lo contrario antes de continuar. Debes practicar notando qué suposiciones estás haciendo y siempre pensar “¿cómo podría romper esta suposición?”, cada vez que te das cuenta de que estás haciendo una.

Economía

La economía de una aplicación web típica es bastante simple. Debe generar suficientes ingresos para cubrir el costo de alojar cualquier servidor que contenga el código y los datos que utiliza su aplicación. Los ingresos que necesita generar pueden provenir de varias fuentes, pero las más comunes son los ingresos publicitarios y las suscripciones pagas de los usuarios.

Para la blockchain, la situación es un poco más complicada porque cada transacción debe pagarse de forma independiente. Los productos de la cadena de bloques más nuevos buscan simplificar esta historia, por ejemplo, Aurora+ proporciona algo así como una “suscripción de cadena de bloques” que permite una cantidad de transacciones de forma gratuita. Pero hasta que esto se convierta en un estándar en el espacio de la cadena de bloques, sigue siendo importante responder a la pregunta “¿quién está pagando por esto?”.

A menudo, es el usuario quien paga por cada transacción porque el pago está vinculado a la cuenta de firma (es decir, el pago está vinculado con la identidad/autorización). Un modelo alternativo es utilizar “meta-transacciones” (transacciones dentro de transacciones) para que el pago lo realice el “firmante externo” mientras que la autorización se basa en el “firmante interno”. Así es como funciona Aurora+ por ejemplo. Desafortunadamente, dado que esta no es la forma predeterminada en que operan las transacciones de blockchain, se requiere un trabajo adicional por parte del desarrollador para que esto suceda.

Para propósito de nuestro ejemplo de aplicación de chat, seguiremos el camino de menor dificultad y cada usuario tendrá que pagar los costos en los que incurra por su uso. Después de haber tomado esta decisión, debemos revisar qué posibles costos podría haber y asegurarnos de que se cubran adecuadamente. Por ejemplo, en Near, el pago del almacenamiento se maneja mediante la “participación de almacenamiento“. Esencialmente, esto significa que cada cuenta tiene parte de su saldo bloqueado según la cantidad de almacenamiento que esté utilizando. Esto es relevante en nuestro contrato de chat porque almacena los mensajes recibidos de otros usuarios, por lo tanto, debemos asegurarnos de que esos otros usuarios cubran el costo de participación de almacenamiento adjuntando un depósito suficiente con su mensaje. Del mismo modo, las solicitudes de contacto crean una entrada en el almacenamiento, por lo que también deben venir con un depósito. Si no aplicáramos estos requisitos de depósito, los usuarios podrían robarse dinero unos a otros, enviando muchos mensajes y bloqueando todo el saldo de la víctima (observe cómo esto se relaciona con la mentalidad contradictoria anterior).

En resumen, al diseñar una dapp, siempre es importante pensar en los costos que implicará y cómo se pagarán, ya sea agregando revisiones extras a los depósitos o utilizando meta-transacciones.

Asegurar invariantes antes de hacer llamadas de contratos cruzados

Este último punto es sutil. En una aplicación típica, todo el código está vinculado al mismo binario. Cuando llamas a una función en una biblioteca, esto generalmente no activa ninguna comunicación, sino que simplemente agrega un nuevo marco en la pila y ejecuta algún código de otra parte del binario. En una configuración de ejecución dentro de la cadena de bloques, las cosas son un poco diferentes.

Hacer una llamada a otro contrato se parece más a realizar una llamada a un proceso completamente diferente que a llamar a una biblioteca. Una vez más, debemos aplicar una mentalidad adversaria y darnos cuenta de que no tenemos idea de lo que podría estar haciendo ese otro proceso; de hecho, podría estar tratando de hacer algo deliberadamente malicioso. Un vector de ataque común es hacer que el otro proceso vuelva a llamar a nuestro contrato y lo explote porque nuestro contrato no esperaba que entrara una nueva llamada mientras esperaba una respuesta a la llamada que inició. Esto se llama un “ataque de reentrada” y fue la fuente de uno de los hacks más famosos en Ethereum, el que resultó en la creación de “Ethereum Classic” (Ethereum classic rechazó la “bifurcación dura” que fue la respuesta de la Fundación Ethereum a el hackeo).

En Near, este problema es aún más pronunciado porque existe la cuestión adicional de la atomicidad. En la máquina virtual de Ethereum (EVM), cada transacción es “atómica” en el sentido de que todas las acciones como resultado de la transacción se comprometen con el estado de la cadena de bloques o ninguna de ellas lo hace (toda la transacción se “revierte”). Esto significa que un ataque de reingreso se puede frustrar mediante el uso de una reversión; todo lo sucedido se deshará, manteniendo el contrato a salvo. Este patrón incluso se incluye en el ejemplo de Mutex en la documentación oficial de Solidity. Sin embargo, en el tiempo de ejecución de Near, la ejecución de los contratos es independiente entre sí; no son atómicos. Entonces, si una transacción hace que el contrato A llame al contrato B, y B encuentra un error, los cambios de estado que ocurrieron en A permanecerán.

Esto ha sido mucha historia y teoría, pero ¿cuál es la conclusión práctica? El punto es que debe asegurarse de que tu contrato esté en “buen estado” cuando realizas una llamada a otro contrato. Es decir, si hay invariantes en los que se basa la lógica de tu contrato, entonces debe ser verificado que sean correctos en el momento en que se realiza la llamada. Como ejemplo simple, supongamos que tenemos un contrato con una función de transferencia. La invariante a mantener es que los tokens no se crean ni se destruyen en una transferencia. Si por alguna razón fuera necesario realizar una llamada a otro contrato durante la transferencia, sería incorrecto debitar una cuenta y luego realizar la llamada sin acreditar la otra primero. Esto se debe a que la invariante de que los tokens no se destruyen se rompería cuando se realizara la llamada externa y esto podría ser explotable. Un ejemplo en este sentido también se incluye en la documentación de Near.

Resumen

Para concluir, en esta publicación de blog presentamos una nueva serie de publicaciones que brindan una introducción al desarrollo de contratos inteligentes en Near usando Rust. Aquí discutimos el ejemplo de contrato de chat que usaremos a lo largo de la serie, así como algunos principios generales a tener en cuenta al desarrollar aplicaciones basadas en blockchain. En la próxima publicación, profundizaremos más en el código para analizar los detalles técnicos de cómo se implementa el contrato. Esto servirá como un ejemplo del Rust SDK de Near, ilustrando conceptos que se aplicarán a todo tipo de contratos del mundo real que desees crear.

Si estás disfrutando de esta serie de publicaciones de blog, ponte en contacto con nosotros en la consultoría Type-Driven. Nos complace brindar servicios de desarrollo de software para dapps, así como materiales de capacitación para tus propios ingenieros.

 

Generate comment with AI 2 nL

Deja un comentario


To leave a comment you should to:


Ir arriba
Report a bug👀