Implementación de paquetes acumulativos de Cartesi – State Fold de Gabriel Coutinho de Paula Cartesi Julio de 2021

La primera herramienta independiente de Cartesi Rollups en Testnet

En Cartesi, nos encontramos con problemas relacionados con la falta de herramientas maduras todos los días. es una tecnología incipiente, y la pila de software aún se encuentra en sus inicios y evoluciona a un ritmo rápido. Escribir soluciones ad-hoc para estos problemas no es escalable. Un mejor enfoque es construir abstracciones sobre soluciones establecidas de nivel inferior, de manera que se puedan reutilizar.

Cabe señalar que estas soluciones de nivel inferior son de código abierto, desarrolladas por la comunidad. De la misma manera, nos hemos beneficiado de ellos, estamos publicando nuestras propias herramientas internas que pueden beneficiar a la comunidad.

El problema que vamos a abordar aquí es leer el estado de la cadena de bloques. Este es el primer paso de la interacción con la cadena de bloques: para decidir qué haremos en cada momento, primero necesitamos conocer el estado de nuestros contratos inteligentes.

El estado de la cadena de bloques está en constante movimiento. Con cada nuevo bloque, las cosas pueden cambiar arbitrariamente. El objetivo de un lector es mantenerse al día con estos cambios. Tenga en cuenta que los lectores son específicos de dApp y deben crearse para adaptarse a contratos inteligentes específicos.

La forma canónica de interactuar con la cadena de bloques es a través de la API JSON-RPC de Ethereum. Por lo general, la API está envuelta en una biblioteca de alto nivel como web3.js. La confianza en el servidor remoto que implementa esta API es imperativa. Un servidor malintencionado podría darnos respuestas incorrectas o simplemente no respondernos. Este servidor debe comunicarse con la red Ethereum y, como tal, debe ejecutar un nodo Ethereum.

En general, hay dos formas de configurar este servidor. El primero lo hacemos nosotros mismos. Elegimos una implementación del protocolo Ethereum, como Geth o Parity, y la ejecutamos en alguna máquina. Esta configuración no es trivial. La segunda forma es delegar esto a algún proveedor externo, como Infura o Alchemy. Esta es una solución mucho más sencilla pero tiene costos asociados. Estos servicios cobran por cada consulta más allá de cierto límite. Los lectores que decidan utilizar Infura deben tener esto en cuenta.

Una forma conveniente de que los contratos inteligentes publiquen su estado es a través de eventos de diferencia. La idea es simple: en cada cambio de estado, notificamos solo lo que se cambió. Por ejemplo, imagine un contrato inteligente que almacene todos los precios anteriores de un determinado activo. En lugar de emitir un evento con todos los precios pasados ​​cada vez que se agrega una nueva entrada, emitimos un evento solo con la nueva entrada. De esta manera, el lector puede reconstruir la lista completa acumulando todos los eventos pasados. Las estructuras de datos complejas, como gráficos y colecciones, solo se pueden comunicar de manera eficiente a través de diferencias. Nos encontramos con esos con bastante frecuencia en Cartesi.

El principal obstáculo para crear un lector coherente tiene que ver con la finalidad del consenso. La finalidad está relacionada con la persistencia: una vez que se confirma un bloque, no se puede alterar. Sin embargo, las cosas en Ethereum no están escritas exactamente en piedra. En cualquier momento, la historia puede reescribirse a través de un proceso llamado reorganización de la cadena, y algunos estados que leemos en el pasado pueden ya no ser parte de la cadena de bloques.

Las reorganizaciones en cadena generalmente ocurren cuando se extraen dos o más bloques al mismo tiempo, creando realidades alternativas. En algún momento, una de las realidades se considerará la correcta y las demás morirán. Si leemos el estado de uno que murió, podemos encontrarnos con inconsistencias. Por ejemplo, imagina que estamos lidiando con eventos de diferencias. Si mezclamos eventos de diferentes realidades, terminamos en un estado inconsistente.

El lado positivo es que Ethereum tiene lo que se llama finalidad probabilística. La posibilidad de reorganizaciones se vuelve cada vez más pequeña cuanto más nos adentramos en el pasado. Como tal, una solución simple para crear un lector consistente es esperar a que los bloques se vuelvan viejos. Podemos calibrar la edad de los bloques a algún umbral de seguridad: cuanto más viejo es, más seguro es el lector. Sin embargo, esto da como resultado un lector de alta latencia.

Por lo tanto, crear un lector que sea consistente y en tiempo real no es trivial. El problema se agrava aún más cuando consideramos que cada dApp tiene sus propias especificidades, como eventos y captadores. También queremos un lector que evite hacer llamadas excesivas al servidor remoto, ya que queremos utilizar servicios como Infura. Nuestro lector debería poder ejecutar tanto de forma local como remota y, en general, ser ligero y rápido.

Nos complace presentar el lanzamiento de la implementación de State Fold.

State Fold es nuestra solución a este problema de lectura del estado de la cadena de bloques. Su diseño está fuertemente inspirado en ideas de programación funcional, lo que nos permite crear software más robusto. La idea subyacente es que el estado de la cadena de bloques es inmutable en un hash de bloque específico. El último bloque cambia constantemente y los bloques en un cierto número pueden cambiar debido a reorganizaciones. Sin embargo, los bloques con un hash específico nunca cambian. Eventualmente pueden ser revocados de la cadena de bloques, pero permanecen inmutables. Y con esta inmutabilidad, podemos arrastrar conceptos de la programación funcional.

En programación funcional, fold (a veces llamado reducir) se usa comúnmente para iterar sobre colecciones cuando queremos acumular sus elementos. Es una función de orden superior, que toma un valor inicial y un operador de combinación, y produce un valor final. Si no está familiarizado con este concepto, le recomendamos que obtenga más información al respecto. Aquí está el artículo de Wikipedia sobre este tema, que debería ser una buena introducción.

El primer problema que tenemos que abordar es la flexibilidad. Dado que los lectores son específicos de dApp, debe haber una forma de configurar State Fold para adaptarse a las necesidades de cada contrato inteligente. State Fold logra esto a través de la programabilidad: el desarrollador especifica su comportamiento a través de un objeto de devolución de llamada que actúa como delegado. Estos delegados dependen de dApp e implementan la lógica de obtención de estado de necesaria para cada contrato inteligente específico.

Para ello, el desarrollador debe proporcionar un par de funciones: sincronizar y plegar. Tanto la sincronización como el plegado crean un estado de bloque, correspondiente al estado de un contrato inteligente en un bloque específico. La sincronización crea un nuevo estado desde cero al consultar la cadena de bloques. Fold avanza el estado en un paso, utilizando tanto el estado anterior como las consultas de . Tenga en cuenta que estas consultas solo se pueden realizar en un hash de bloque específico, lo que las hace inmutables.

En un nivel alto, la lógica de State Fold es razonablemente fácil. State Fold llama a la sincronización para producir el primer estado y luego usa fold para actualizar el estado un bloque a la vez. Como tal, cuando activamos el State Fold por primera vez, llamará a la sincronización para generar el estado inicial. Luego, en cada nuevo bloque, llamará a fold en el estado anterior, generando el siguiente estado. La propiedad que estas dos funciones deben satisfacer es que la sincronización con un bloque y el plegado hasta ese bloque deben producir el mismo resultado. Podemos pensar en la sincronización en un bloque como una vía rápida de plegado desde la génesis hasta este bloque.

Entrando en más detalles, la naturaleza ramificada de la cadena de bloques produce una estructura de bloques en forma de árbol. Por ejemplo, si varios mineros crean un bloque al mismo tiempo, la cadena de bloques se bifurcará temporalmente, creando realidades separadas. State Fold almacena estas realidades internamente en una estructura de datos en forma de árbol, que representa la propia cadena de bloques. Esta estructura asocia bloques a estados de bloque, lo que describe completamente el estado de nuestros contratos inteligentes en cada bloque de la cadena de bloques. Esta estructura es simplemente una caché, ya que podemos reconstruirla completamente a partir de la cadena de bloques.

Cuando se agrega un nuevo bloque a la cadena de bloques, State Fold encuentra el padre de este nuevo bloque en la caché y avanza su estado un paso a través del plegado. Si hay huecos en la caché (por ejemplo, solo tiene el bloque abuelo), este procedimiento se aplica de forma recursiva. Si la caché no contiene un bloque ancestro, usamos sincronización en su lugar. Este proceso mantiene elegantemente la coherencia cuando se enfrenta a reorganizaciones, lo que permite que State Fold funcione en tiempo real sin temor a estados inconsistentes.

Dentro del delegado, el desarrollador tiene disponibles todos los métodos de lectura JSON-RPC, aunque restringidos a consultas solo en ciertos bloques hash (debido a nuestra restricción de inmutabilidad). Esto permite la misma flexibilidad que el uso de web3.js sin procesar, pero en tiempo real, con garantías de coherencia y sin complejidad adicional.

Nuestra implementación está en Rust, disponible como una biblioteca (en el lenguaje de Rust, una caja). Los delegados también están escritos en Rust y deben compilarse con la biblioteca. Como tal, puede ejecutarse tanto de forma local como remota.

Una buena herramienta que resuelve el mismo problema de una manera diferente se llama The Graph. Sin embargo, The Graph tomó ciertas decisiones de diseño con un perfil de compensación desagradable para ciertas partes de nuestra pila de software. Lo primero que debemos tener en cuenta es que nuestros objetivos son mucho más modestos que The Graph, lo que a su vez ha hecho que The Graph sea mucho más complejo de lo necesario para nuestras necesidades. Aquí hacemos un breve análisis de los objetivos que alcanza nuestro lector, para justificar nuestra elección de implementar nuestra propia solución.

El primer objetivo es la flexibilidad. La flexibilidad es la capacidad en la que el lector es capaz de leer estados de observables arbitrarios. Consideramos un estado observable si podemos leerlo a través de la API Ethereum JSON-RPC. Nuestra solución expone toda la API al desarrollador, lo que la hace máximamente flexible.

La programación de nuestro State Fold se realiza íntegramente en Rust. Proporcionamos un lenguaje de programación real fuertemente tipado, en el que el desarrollador obtiene datos de forma activa utilizando la API estándar. Creemos que esto impone menos restricciones, simplifica el modelo computacional general y reduce la carga cognitiva.

Tenga en cuenta que esta flexibilidad, a su vez, permite múltiples optimizaciones para hacer un uso más ligero de servicios como Infura. Estos servicios, más allá de cierto límite, cobran a los usuarios por cada consulta. El State Fold, si se programa correctamente, es muy ligero. Además, al plegar, naturalmente reutilizamos estados pasados, evitando la necesidad de consultar demasiado a Infura.

Cabe señalar que State Fold funciona con cualquier proveedor. Todo lo que necesita es un punto final Ethereum JSON-RPC. Diseñamos nuestro lector para que sea liviano y fácil para el proveedor. Una consecuencia de esto es que no hay mucha sobrecarga al sincronizar. También se puede implementar fácilmente de forma local o remota en nuestra propia infraestructura, lo que simplifica su uso.

El segundo objetivo es la coherencia en tiempo real. La consistencia es el grado en el que el estado observado es correcto en relación con la cadena de bloques, y el tiempo real es la capacidad de hacerlo en el último bloque. El desafío es crear un lector que pueda operar en la "espuma cuántica" mientras se enfrenta con seguridad a las reorganizaciones. Logramos estas propiedades mediante el uso del pliegue de programación funcional.

Otro objetivo de diseño que no debe pasarse por alto es la capacidad de componer State Folds. Esto permite un enfoque modular al escribir delegados, lo que simplifica enormemente la implementación de una lógica compleja y permite la reutilización del código. La capacidad de crear abstracciones es una de las herramientas más importantes en la caja de herramientas de un programador.

Sobre Cartesi

Cartesi es una infraestructura multicadena de capa 2 que permite a cualquier desarrollador de software crear contratos inteligentes con las herramientas de software convencionales y los lenguajes a los que está acostumbrado, mientras logra una escalabilidad masiva y bajos costos. Cartesi combina una máquina virtual innovadora, acumulaciones optimistas y cadenas laterales para revolucionar la forma en que los desarrolladores crean aplicaciones .