Desarrollo de Contratos Inteligentes en Near usando Rust – Parte 3
Esta es la tercera parte de una serie de publicaciones sobre la creación de una aplicación de chat con Rust on the Near blockchain. Puedes encontrar las publicaciones anteriores de la serie aquí y aquí.
En esta publicación nos centraremos en las partes fuera de la cadena del código. Discutiremos la necesidad de “indexers (indexadores)” y recorreremos algunas partes de la implementación del indexador en este ejemplo. Puedes encontrar el repositorio completo con todo el código que discutiremos hoy en mi GitHub.
Indexadores, qué son y por qué los necesitamos
En términos de la cadena de bloques, un indexador es un servicio que consume datos sin procesar de una fuente (por lo general, una instancia de nodo completo co-localizada para esa cadena de bloques) y los analiza en un formato que es más útil para una aplicación específica. Por ejemplo, en el caso de nuestra aplicación de chat, el indexador consume un flujo de bloques Near y produce un flujo de eventos (por ejemplo, mensajes recibidos y solicitudes de contacto).
Los indexadores son importantes porque las bases de datos utilizadas para operar la propia cadena de bloques generalmente no están optimizadas para realizar los tipos de consultas que interesan a las aplicaciones. Por ejemplo, obtener el saldo de un usuario para un token ERC-20 en Ethereum generalmente se realiza ejecutando la consulta a través de EVM porque esa es la única forma en que la información está disponible desde un nodo típico de Ethereum. Esta es una operación extremadamente costosa en comparación con buscar una entrada en una base de datos relacional tradicional. Por lo tanto, una optimización simple para cualquier aplicación que necesite un acceso rápido a los saldos ERC-20 sería ejecutar un indexador en los datos brutos de Ethereum que llena una base de datos tradicional con los saldos (o datos) que le interesan. Luego, la aplicación usaría esta base de datos como fuente para los saldos en lugar de un nodo Ethereum directamente. Así es como funciona el explorador de bloques Etherscan; Etherscan ejecuta un indexador para llenar una base de datos que luego se usa para llenar los campos en las páginas web que sirve Etherscan.
Los indexadores no solo son importantes para Ethereum, cualquier dapp de alto rendimiento en cualquier cadena de bloques deberá incluir un indexador en algún lugar de su arquitectura. La aplicación de chat de ejemplo que hemos estado discutiendo en Near no es una excepción, así que profundicemos en cómo se implementa el indexador.
Obtener los datos sin procesar
Los indexadores solo procesan datos de blockchain en bruto (no procesados con anterioridad) en un formato que la aplicación asociada puede usar; ellos no generan los datos en primer lugar. Por lo tanto, la primera pregunta que debemos responder al crear un indexador es: ¿de dónde provienen los datos de la cadena de bloques?
Near proporciona algunas fuentes de datos diferentes, como se describe a continuación.
Ejecutar un nodo nearcore
La mejor fuente de datos (en términos de descentralización y seguridad) para cualquier cadena de bloques es la red peer-to-peer (persona a persona) de la propia cadena de bloques. Para acceder a esta fuente de datos, debe ejecutar un nodo que entienda el protocolo de la cadena de bloques. En el caso de Near, la implementación del nodo se llama nearcore. Su código fuente está abierto en GitHub. Hay documentación disponible sobre cómo ejecutar su propio nodo nearcore. La principal barrera de entrada aquí es la cantidad de espacio en disco requerido para esto; se recomienda tener 1 TB de almacenamiento dedicado para su nodo y se tarda bastante tiempo en sincronizar con la cadena como resultado de la necesidad de descargar todos esos datos.
Una vez que tengas una configuración del nodo nearcore, Near proporciona un indexer framework de indexador convenientemente creado en Rust que se puede usar para crear indexadores con nearcore como fuente de datos. Para un proyecto real, esta sería la mejor manera de crear un indexador. Sin embargo, nuestro ejemplo es solo una demostración, por lo que no queremos pasar horas descargando datos de la cadena a un servidor dedicado de 1 TB. Afortunadamente hay otras opciones.
Near data lake
Para facilitar a los desarrolladores la puesta en marcha de sus proyectos, Near creó el data lake framework (framework del lago de datos) como una fuente alternativa de datos para que la utilicen los indexadores. El framework del lago de datos se basa en el framework del indexador mencionado anteriormente, utilizando un nodo nearcore como fuente de datos. El indexador que alimenta el lago de datos no está procesando los datos para una aplicación específica, simplemente está pasando los datos para almacenarlos en el almacenamiento de AWS S3. Sin embargo, esto permite a los desarrolladores obtener acceso a estos datos utilizando su propia cuenta de AWS y luego crear sus propios indexadores para proyectos en específico utilizando este almacenamiento S3 como fuente de datos.
La ventaja de esto para los desarrolladores es que este método es mucho más rápido para que funcione. Sin embargo, la desventaja es que los datos provienen de una fuente centralizada y, por lo tanto, en teoría, es más fácil corromperlos que usar la red punto a punto directamente.
El acceso al lago de datos requiere que pagues por los recursos de AWS que se utilizan para entregarte esos datos. Una vez más, a los efectos del ejemplo de la aplicación de chat, no quiero que las personas se registren en AWS y gasten dinero para ejecutar el indexador. Por lo tanto, elegí la opción de fuente de datos final.
Nodos RPC públicos
La forma final de acceder a los datos de la cadena de bloques, si no estás ejecutando tu propio nodo o accediendo al almacén de datos preconstruido de otra persona, es usar los nodos de otra persona. Los nodos RPC son nodos en la red blockchain que están destinados a atender las solicitudes de los usuarios. Cada cadena de bloques tiene proveedores de nodos RPC (algunos gratuitos, otros de pago). Puedes encontrar una lista de los proveedores de RPC para Near aquí.
Esta es la forma menos eficiente de acceder a los datos de la cadena de bloques porque se necesitan varias solicitudes de RPC para obtener los datos que suelen utilizar los indexadores. Cada solicitud de RPC incurre en latencia de red, lo que hace que el indexador sea lento para responder a los eventos que ocurren en la cadena. La única ventaja de este enfoque es que es gratis configurar una demostración siempre que haya un proveedor de RPC gratuito para la cadena (como es el caso de Near). Por lo tanto, esta es la fuente de datos que usa el indexador en nuestro ejemplo.
Dicho todo esto, al indexador en sí mismo no le importa de dónde provienen sus datos. En consecuencia, aunque nuestro ejemplo utiliza la peor fuente de datos, vale la pena explorar su implementación porque los conceptos que utiliza este indexador son los mismos que los de un indexador creado con el lago de datos de Near o frameworks de indexación basados en nodos.
Implementación del indexador
Nuestro indexador está construido como una aplicación de tokio en Rust. Tokio es un framework de Rust para escribir aplicaciones de alto rendimiento donde las operaciones de I/O (Entrada / Salida, por sus siglas en inglés) son el principal cuello de botella. Nuestro indexador es una aplicación de este tipo porque el cálculo real que realiza es extremadamente rápido en comparación con el tiempo que lleva solicitar datos de los nodos RPC. Las características principales de tokio son que utiliza pimitivos asincrónicos sin bloqueo y tiene multithreading incorporado para permitir la ejecución en paralelo. Y además está en Rust, por lo que, naturalmente, tiene las garantías de concurrencia segura y memoria segura que proporciona Rust.
Si tokio es el escenario en el que se establece nuestra aplicación, lo que sigue son los actores de la obra (juego de palabras intencionado; esta aplicación sigue el modelo de actor, pero elijo hacerlo directamente en tokio en lugar de usar una biblioteca como actix porque creo que los canales de tokio proporcionan una tipificación más fuerte que los mensajes genéricos utilizados en la mayoría de los frameworks de actores).
El indexador tiene cuatro roles principales: el administrador, el descargador de bloques, el descargador de fragmentos y el manejador de recibos.
El administrador
El proceso del administrador es supervisar todo el indexador. Es responsable de delegar trabajo a los otros procesos y decirles que se apaguen cuando el programa se está cerrando (por ejemplo, en el caso de que se encuentre un error). Por ejemplo, el administrador maneja el equilibrio de carga de los descargadores de fragmentos, alternándolos cuando asigna un fragmento para descargar.
El descargador de bloques
Como su nombre lo indica, el propósito del proceso de descarga de bloques es descargar bloques. Sondea periódicamente el Near RPC para verificar si hay bloques nuevos y, si los hay, los descarga y los envía al administrador. Si no usáramos el RPC como nuestra fuente de datos, este proceso se reemplazaría con una conexión a un nodo de Near o al lago de datos.
Los descargadores de fragmentos
En Near, los bloques no contienen datos sobre transacciones; los fragmentos sí. Los bloques solo brindan información sobre qué nuevos fragmentos están disponibles. La razón de esto es la fragmentación de Near (puede leer más sobre eso aquí). Por lo tanto, necesitamos procesos separados para descargar los datos de fragmentos para cada bloque. Los descargadores de fragmentos cumplen esta función. Nuestro indexador tiene varias instancias de descarga de fragmentos para permitir la descarga de fragmentos en paralelo.
Si no estuviéramos usando el RPC como nuestra fuente de datos, dependiendo de cómo se tengan en cuenta los datos en la fuente de datos que estuviéramos usando, es posible que estos procesos no necesiten existir (por ejemplo, el near-indexer-framework incluye todos los datos de bloques y fragmentos en un mensaje único). Pero para nuestro caso, dado que estamos usando el RPC, estos procesos son necesarios.
El manejador de recibos
Los fragmentos contienen “recibos” que se crean cuando se procesa una transacción. Cuando el administrador recibe un nuevo fragmento de un descargador de fragmentos, envía todos los recibos al proceso del controlador de recibos (podríamos tener varias instancias del controlador de recibos para procesar los recibos en paralelo, al igual que tenemos varios descargadores de fragmentos, pero el procesamiento de recibos es lo suficientemente rápido como para no suponer que esto añadiera una gran mejora en el rendimiento). Este proceso filtra los recibos solo a los que nos interesan, luego descarga el resultado de la ejecución de los recibos y finalmente procesa los eventos de esos resultados. En el caso de este ejemplo, simplemente escribimos los eventos en un archivo (para una demostración en vivo, puedes ver el archivo con algo como el comando tail -f de Unix para ver cómo entran los eventos), pero puedes imaginar que una implementación de producción podría reenviar estos eventos como notificaciones automáticas a una versión móvil de la aplicación.
Observaciones
Puedes notar a lo largo del código del indexador que existe cierta complejidad en el envío de fragmentos/recibos con el hash de bloque después del bloque que incluía esos fragmentos. Esta es una peculiaridad de Near RPC donde quiere saber que estás al tanto de los bloques posteriores para servir al resultado de la ejecución. Nuevamente, esto se manejaría mucho mejor si se usara una mejor fuente de datos.
Es intencional que no haya panics (“panic” de RUST, para detener una función) en ninguna de las funciones del actor. Cuando encuentran un error, lo registran y envían un mensaje de apagado al administrador (y el administrador lo envía a todos los demás actores). Esto es importante porque entrar en pánico en una aplicación de subprocesos múltiples puede causar un comportamiento inesperado (en general, Tokio es bastante bueno para deshabilitar toda la aplicación con eficacia, pero aún es mejor codificar a la defensiva).
Conclusión
En esta publicación, discutimos por qué los indexadores son importantes para las dapps del mundo real y analizamos algunos de los detalles del indexador de ejemplo implementado para la dapp de chat. Al igual que en la publicación anterior, hay ejercicios en el código del indexador incluidos en los comentarios etiquetados como EXERCISE. Te aliento a que pruebes estos ejercicios si deseas tener experiencia práctica con el código base.
Acerca de la Serie
Esta es la publicación final de esta serie. En la Parte 1, analizamos los principios generales del desarrollo de contratos inteligentes y cómo se aplican a un contrato de ejemplo para una dapp de chat. En la Parte 2, profundizamos en cómo usar near-sdk para escribir contratos inteligentes para Near en Rust. Finalmente, esta publicación discutió cómo se necesitan indexadores para integrar los datos de la cadena de bloques con los componentes fuera de la cadena de nuestra aplicación.
Una parte final del código que no cubrí es la prueba de integración. La prueba de integración usa la librería near-workspaces para simular la cadena de bloques localmente y usa el mismo estilo de Rust asíncrono que el indexador. Aunque las pruebas de integración no son especialmente llamativas o interesantes, son importantes para garantizar que tu contrato funcione correctamente. Te animo a que eches un vistazo a las pruebas de integración para el contrato de mensajería y pruebes el ejercicio allí para obtener algo de experiencia práctica en esa área también.
If you have enjoyed this series of blog posts, please get in touch with us at Type-Driven consulting. We are happy to provide software development services for dapps, as well as training materials for your own engineers.
Si has disfrutado de esta serie de publicaciones de blog, comunícate 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.