Saltar al contenido

El estado del etéreo apátrida

En el última edición de los archivos 1.x, hicimos un resumen rápido del origen de la iniciativa de investigación Eth 1.x, qué está en juego y cuáles son algunas posibles soluciones. Terminamos con el concepto de etéreo apátrida, y dejó un examen más detallado del cliente apátrida para esta publicación.

Apátrida es la nueva dirección de la investigación Eth 1.x, por lo que vamos a hacer una inmersión bastante profunda y tener una idea real de los desafíos y las posibilidades que se esperan en el camino por delante. Para aquellos que quieran sumergirse aún más, haré todo lo posible para vincular a recursos más detallados siempre que sea posible.

Para ver hacia dónde vamos, primero debemos entender dónde estamos con el concepto de "estado". Cuando decimos "estado", es en el sentido de "un estado de cosas".

El "estado" completo de Ethereum describe el estado actual de todas las cuentas y saldos, así como los recuerdos colectivos de todos los contratos inteligentes implementados y en ejecución en el EVM. Cada bloque finalizado en la cadena tiene un solo estado, que es acordado por todos los participantes en la red. Ese estado cambia y se actualiza con cada nuevo bloque que se agrega a la cadena.

En el contexto de la investigación Eth 1.x, es importante no solo saber en qué estado es, pero cómo se representa tanto en el protocolo (como se define en el documento amarillo) como en la mayoría de las implementaciones de clientes (por ejemplo, geth, parity, trinity, besu, etc.).

Dale un trie

La estructura de datos utilizada en Ethereum se llama Merkle-Patricia Trie. Dato curioso: "Trie" se toma originalmente de la palabra "recuperación", pero la mayoría de las personas lo pronuncian como "intentar" para distinguirlo de "árbol" cuando hablan. Pero yo divago. Lo que necesitamos saber sobre Merkle-Patricia Tries es lo siguiente:

En un extremo del trie, hay todos los datos particulares que describen el estado (nodos de valor). Esto podría ser el saldo de una cuenta en particular o una variable almacenada en un contrato inteligente (como el suministro total de un token ERC-20). En el medio son nodos de ramificación, que vinculan todos los valores a través del hash. Un nodo de ramificación es una matriz que contiene los valores hash de sus nodos secundarios, y cada nodo de ramificación se codifica posteriormente y se coloca en la matriz de su nodo primario. Este hashing sucesivo finalmente llega a un solo nodo raíz de estado en el otro extremo del trie.

En el diagrama simplificado anterior, podemos ver cada valor, así como el camino que describe cómo llegar a ese valor. Por ejemplo, para llegar a V-2, recorremos el camino 1,3,3,4. Del mismo modo, se puede llegar a V-3 atravesando el camino 3,2,3,3. Tenga en cuenta que las rutas en este ejemplo siempre tienen 4 caracteres de longitud, y que a menudo solo hay una ruta a seguir para alcanzar un valor.

Esta estructura tiene la propiedad importante de ser determinista y verificable criptográficamente: la única forma de generar una raíz de estado es computándola a partir de cada parte individual del estado, y dos estados que son idénticos se pueden probar fácilmente comparando el hash raíz y los hash que lo llevaron (una prueba de Merkle) Por el contrario, no hay forma de crear dos estados diferentes con el mismo hash raíz, y cualquier intento de modificar el estado con valores diferentes dará como resultado un hash raíz de estado diferente.

Ethereum optimiza la estructura del trie al introducir algunos tipos de nodos nuevos que mejoran la eficiencia: nodos de extensión y nodos hoja. Estas codifican partes de la camino en nodos para que el trie sea más compacto.

En esta estructura modificada de Merkle-Patricia, cada nodo conducirá a una elección entre múltiples nodos siguientes, una parte comprimida de una ruta que comparten nodos posteriores o valores (precedidos por el resto de su ruta, si es necesario). Son los mismos datos y la misma organización, pero este trie solo necesita 9 nodos en lugar de 18. Esto parece más eficiente, pero con el beneficio de la retrospectiva, en realidad no es óptimo. Exploraremos por qué en la siguiente sección.

Para llegar a una parte particular del estado (como el saldo actual de Ether de una cuenta), uno debe comenzar en la raíz del estado y arrastrarse a lo largo del trie de nodo a nodo hasta alcanzar el valor deseado. En cada nodo, los caracteres en el camino se usan para decidir a qué siguiente nodo viajar, como un varilla de adivinación, pero para navegar por estructuras de datos hash.

En la versión "real" utilizada por Ethereum, rutas son los hashes de una dirección de 64 caracteres (256 bits) de longitud, y valores son Datos codificados por RLP. Los nodos de ramificación son matrices que contienen 17 elementos (dieciséis para cada uno de los posibles caracteres hexadecimales y uno para un valor), mientras que los nodos de hoja y los nodos de extensión contienen 2 elementos (una ruta parcial y un valor o el hash del siguiente nodo secundario ) El wiki de Ethereum es probablemente el mejor lugar para lea más sobre esto, o, si desea abrirse camino entre las malezas, Este artículo tiene un gran (pero desafortunadamente desaprobado) ejercicio de bricolaje en Python para jugar.

Pegarlo en una base de datos

En este punto, debemos recordarnos que la estructura del trie es solo un concepto abstracto. Es una forma de agrupar la totalidad del estado de Ethereum en una estructura unificada. Esa estructura, sin embargo, debe ser implementado en el código del cliente y almacenado en un disco (o unos pocos miles de ellos repartidos por todo el mundo). Esto significa tomar un trie multidimensional y guardarlo en una base de datos ordinaria, que solo comprende (valor clave) pares.

En la mayoría de los clientes de Ethereum (todos excepto turbo-geth), el Merkle-Patricia Trie se implementa mediante la creación de un (valor clave) par para cada nodo, donde el valor es el nodo en sí, y la clave es el hash de ese nodo.

El proceso de atravesar el trie, entonces, es más o menos el mismo que el proceso teórico descrito anteriormente. Para buscar el saldo de una cuenta, comenzaríamos con el hash raíz y buscaríamos su valor en la base de datos para obtener el primer nodo de sucursal. Usando el primer carácter de nuestra dirección hash, encontramos el hash del primer nodo. Buscamos ese hash en la base de datos y obtenemos nuestro segundo nodo. Usando el siguiente carácter de la dirección hash, encontramos el hash del tercer nodo. Si tenemos suerte, podríamos encontrar una extensión o un nodo hoja en el camino, y no tendremos que pasar por los 64 nibbles, pero finalmente, llegaremos a nuestra cuenta deseada y podremos recuperar su saldo de la base de datos. .

Calcular el hash de cada nuevo bloque es en gran medida el mismo proceso, pero a la inversa: comenzando con todos los nodos de borde (cuentas), el trie se construye mediante hash sucesivos, hasta que finalmente se construye un nuevo hash raíz y se compara con el último acordado. sobre bloque en la cadena.

Aquí es donde ese poco sobre el aparente entra en juego la eficiencia del estado: reconstruir todo el trie es muy intensivo en disco, y la estructura de trie Merkle-Patricia modificada utilizada por Ethereum es más eficiente en el protocolo a costa de la eficacia de la implementación. Esos tipos de nodos adicionales, hoja y extensión, teóricamente ahorran en la memoria necesaria para almacenar el trie, pero crean los algoritmos que modificar El estado dentro de la base de datos regular es más complejo. Por supuesto, una computadora decentemente potente puede realizar el proceso a una velocidad increíble. La potencia de procesamiento pura, sin embargo, solo llega hasta cierto punto.

Sincronización, bebé, sincronización

Hasta ahora hemos limitado nuestro alcance a lo que sucede en un individual computadora que ejecuta una implementación de Ethereum como geth. Pero Ethereum es una red, y el objetivo de todo esto es mantener el mismo estado unificado consistente en miles de computadoras en todo el mundo, y entre diferentes implementaciones del protocolo.

Las fichas que se barajan constantemente de #Defi, las subastas de criptokitty o las batallas de magos de cheeze, y las transferencias ETH comunes se combinan para crear un estado rápidamente cambiante para que los clientes de Ethereum se mantengan sincronizados, y se vuelve cada vez más difícil a medida que Ethereum se vuelve más popular, y el más profundo se vuelve el estado.

Turbo-geth es una implementación que llega a la raíz del problema: aplana la base de datos trie y usa la ruta de un nodo (en lugar de su hash) como el par (clave, valor). Esto efectivamente hace que la profundidad del árbol sea irrelevante para las búsquedas, y permite una variedad de características ingeniosas que pueden mejorar el rendimiento y reducir la carga en el disco cuando se ejecuta un nodo completo.

El estado de Ethereum es grande, y cambia con cada bloque. ¿Qué tan grande y cuánto cambio? Podemos aumentar el estado actual de Ethereum en alrededor de 400 millones de nodos en el estado trie. De estos, alrededor de 3,000 (pero hasta 6,000) necesitan agregarse o modificarse cada 15 segundos. Mantenerse sincronizado con la cadena de bloques Ethereum es, efectivamente, construir constantemente una nueva versión del estado una y otra vez.

Este proceso de varios pasos de las operaciones de la base de datos de trie de estado es la razón por la cual las implementaciones de Ethereum son tan exigentes en la E / S de disco y la memoria, y por qué incluso una "sincronización rápida" puede tardar hasta 6 horas en completarse, incluso en conexiones rápidas. Para ejecutar un nodo completo en Ethereum, un SSD rápido (a diferencia de un HDD barato y confiable) es un requisito, porque procesar cambios de estado es extremadamente exigente en las lecturas / escrituras de disco.

Aquí es importante tener en cuenta que existe una distinción muy grande y significativa entre establecer un nuevo nodo para sincronizar y mantener un nodo existente sincronizado – Una distinción que, cuando lleguemos a Ethereum sin estado, se desdibujará (con suerte).

La forma directa de sincronizar un nodo es con el método de "sincronización completa": a partir del bloque de génesis, se recupera una lista de cada transacción en cada bloque y se crea un estado trie. Con cada bloque posterior, se modifica el estado trie, agregando y modificando nodos a medida que se reproduce el historial completo de la cadena de bloques. Se tarda una semana completa en descargar y ejecutar un cambio de estado para cada bloque desde el principio, pero es solo cuestión de tiempo antes de que las transacciones que necesite estén pendientes de inclusión en el próximo bloque nuevo, en lugar de estar ya consolidado en uno anterior.

Otro método, llamado acertadamente "sincronización rápida", es más rápido pero más complicado: un nuevo cliente puede, en lugar de solicitar transacciones desde el principio de los tiempos, solicitar entradas de estado de un bloque de "punto de control" reciente y confiable. Es mucho menos total información para descargar, pero aún es mucha información para procesar la sincronización no está actualmente limitada por el ancho de banda, sino por el rendimiento del disco.

Un nodo de sincronización rápida está esencialmente en una carrera con la punta de la cadena. Necesita conseguir todos del estado en el "punto de control" antes de que ese estado se vuelva obsoleto y deje de ser ofrecido por los nodos completos (puede "pivotar" a un nuevo punto de control si eso sucede). Una vez que un nodo de sincronización rápida supera el obstáculo y alcanza su estado completamente atrapado con un punto de control, puede cambiar a sincronización completa, creando y actualizando su propia copia de estado de las transacciones incluidas en cada bloque.

¿Puedo obtener un testigo de bloque?

Ahora podemos comenzar a desempaquetar el concepto de Ethereum sin estado. Uno de los objetivos principales es hacer nuevos nodos menos doloroso para girar. Dado que solo el 0.1% del estado está cambiando de un bloque a otro, parece que debería haber un medio de reducir todas esas "cosas" adicionales que deben descargarse antes de la conmutación de sincronización completa.

Pero este es uno de los desafíos impuestos por la estructura de datos criptográficamente segura de Ethereum: en un momento, un cambio a un solo valor dará como resultado un hash de raíz completamente diferente. ¡Esa es una característica, no un error! Mantiene a todos seguros de que están en la misma página (en el mismo estado) con todos los demás en la red.

Para tomar un atajo, necesitamos una nueva información sobre el estado: un testigo de bloque.

Supongamos que solo un valor en este trie ha cambiado recientemente (resaltado en verde):

Un nodo completo que sincroniza el estado (incluida esta transacción) lo hará a la antigua: tomando todos los elementos de estado y combinándolos para crear un nuevo hash raíz. Luego pueden verificar fácilmente que su estado es el mismo que el de todos los demás (ya que tienen el mismo hash y el mismo historial de transacciones).

¿Pero qué pasa con alguien que acaba de sintonizar? Cual es el pequeñísimo ¿Cuánta información necesita el nuevo nodo para verificar que, al menos durante el tiempo que ha estado observando, sus observaciones son consistentes con todos los demás?

Un nodo nuevo y ajeno necesitará nodos completos más antiguos y más sabios para proporcionar prueba que la transacción observada encaja con todo lo que han visto hasta ahora sobre el estado.

En términos muy abstractos, una prueba de testigo en bloque proporciona todos los hashes faltantes en un estado trie, combinados con alguna información "estructural" acerca de en qué lugar del trie pertenecen esos hashes. Esto permite que un nodo "ajeno" incluya la nueva transacción en su estado y calcule el nuevo hash raíz localmente, sin necesidad de que descarguen una copia completa del estado trie.

Esta es, en pocas palabras, la idea detrás de sincronización de haz. En lugar de esperar para recopilar cada nodo en el punto de control, la sincronización del haz comienza a observar e intentar ejecutar las transacciones a medida que ocurren, solicitando a un testigo con cada bloque de un nodo completo la información que no tiene. A medida que las transacciones nuevas "tocan" cada vez más el estado, el cliente puede confiar cada vez más en su propia copia de estado, que (en la sincronización del haz) se completará gradualmente hasta que finalmente cambie a la sincronización completa.

La apatridia es un espectro

Con la introducción de un testigo de bloque, el concepto de "totalmente apátrida" comienza a definirse más. Al mismo tiempo, es donde comenzamos a encontrarnos con preguntas abiertas y problemas sin una solución obvia.

En contraste con la sincronización del haz, un verdaderamente apátrida el cliente lo haría Nunca mantener una copia del estado; solo tomaría las últimas transacciones junto con el testigo y tendría todo lo que necesita para ejecutar el siguiente bloque.

Puede ver eso, si el toda la red eran apátridas, esto en realidad podría resistir para siempre: se pueden producir testigos de nuevos bloques a partir del bloque anterior. ¡Serían testigos hasta el fondo! Al menos, hasta el último "estado de los afiliados" acordado, y el primer testigo generado a partir de ese estado. Ese es un gran cambio dramático en Ethereum que probablemente no obtenga un apoyo generalizado.

Un enfoque menos dramático es acomodar diversos grados de "estado completo" y tener una red en la que algunos los nodos mantienen una copia completa del estado y pueden servir a todos los demás testigos nuevos.

  • Los nodos de estado completo funcionarían como antes, pero además calcularían un testigo y lo adjuntarían a un nuevo bloque o lo propagarían a través de un sub-protocolo de red secundario.

  • Los nodos de estado parcial podrían mantener un estado completo por solo un pequeño número de bloques, o tal vez simplemente "observar" el estado en el que están interesados ​​y obtener el resto de los datos que necesitan para verificar los bloques de los testigos. Esto ayudaría inmensamente a los desarrolladores de dapp que ejecutan infraestructura.

  • Los nodos de estado cero, que por definición desean mantener a sus clientes funcionando lo más liviano posible, podrían depender completamente de testigos para verificar nuevos bloques.

Hacer que este esquema funcione podría implicar algo así como un comportamiento de fragmentación y enjambre de estilo bittorrent, donde los fragmentos de testigos se propagan según sus necesidades y las mejores conexiones a otros nodos con estado parcial (complementario). O bien, podría implicar la elaboración de una implementación alternativa del estado más adecuada para la generación de testigos. ¡Esto es algo para investigar y crear un prototipo!

Para un análisis mucho más profundo de cuáles son las compensaciones de los nodos con estado y sin estado, vea Alexey Akhunov Las sombras de la estado.

Una característica importante del enfoque semi-sin estado es que estos cambios no necesariamente implican grandes cambios difíciles. A través de mejoras pequeñas, comprobables e incrementales, es posible construir el componente sin estado de Ethereum en un sub-protocolo complementario, o como una serie de EIP no controvertidos en lugar de una gran actualización de "salto de fe".

El camino (mapa) por delante

El elefante en la sala de investigación es tamaño del testigo. Los bloques ordinarios contienen un encabezado y una lista de transacciones, y están en el orden de 100 kB. Esto es lo suficientemente pequeño como para hacer que la propagación de bloques sea rápida en relación con la latencia de la red y el tiempo de bloque de 15 segundos.

Sin embargo, los testigos deben contener los hashes de los nodos tanto en los bordes como en el fondo del estado. Esto significa que son mucho, mucho más grandes: los primeros números sugieren del orden de 1 MB. En consecuencia, la sincronización de un testigo es mucho más lenta en relación con la latencia de la red y el tiempo de bloqueo, lo que podría ser un problema.

El dilema es similar a la diferencia entre descargar una película o transmitirla: si la red es demasiado lenta para mantenerse al día con la transmisión, descargar la película completa es la única opción viable. Si la red es mucho más rápida, la película se puede transmitir sin problemas. En el medio, necesita más datos para decidir. Aquellos con ISP por debajo del par reconocerán la gravedad de intentar transmitir una película del viernes por la noche a través de una red que podría no estar lista para la tarea.

Aquí, en gran medida, es donde comenzamos a entrar en los problemas detallados que el grupo Eth 1x está abordando. En este momento, no se sabe lo suficiente sobre la red de testigos hipotéticos para saber con certeza que funcionará de manera adecuada u óptima, pero el diablo está en los detalles (y los datos).

Una línea de investigación es pensar en formas de comprimir y reducir el tamaño de los testigos cambiando la estructura del propio trie (como un trie binario), para hacerlo más eficiente en el nivel de implicación. Otra es prototipar las primitivas de red (enjambre de estilo bittorrent) que permiten que los testigos pasen eficientemente entre diferentes nodos en la red. Ambos se beneficiarían de una especificación formal de testigos, que aún no existe.

Todas estas instrucciones (y más) se están compilando en una hoja de ruta más organizada, que se destilará y publicará en las próximas semanas. Los puntos resaltados en la hoja de ruta serán temas de futuras inmersiones profundas.

Si ha llegado hasta aquí, debe tener una buena idea de lo que se trata "Stateless Ethereum", y parte del contexto para la emergente I + D de Eth1x.

Como siempre, si tiene preguntas sobre los esfuerzos de Eth1x, solicitudes de temas o desea contribuir, preséntese en ethresear.ch o comuníquese con @gichiba y / o @JHancock en Twitter.

Un agradecimiento especial a Alexey Akhunov por proporcionar comentarios técnicos y algunos de los diagramas trie.

¡Feliz año nuevo y feliz Muir Glacier hardfork!

Fuente: https://blog.ethereum.org/2019/12/30/eth1x-files-state-of-stateless-ethereum/