La última actualización de Starknet (v0.13.2) llamada Bolt trae dos cambios importantes: ejecución paralela y empaquetamiento de bloques . Aunque son independientes entre sí, ambas características respaldan el avance hacia el objetivo de un espacio de bloques rápido, barato y protegido criptográficamente por Ethereum.
La ejecución paralela permite que transacciones no conflictivas (es decir, transacciones que no tocan el mismo estado) se ejecuten simultáneamente. Al implementar la ejecución paralela, las capas 2 como Starknet pueden reducir el tiempo de ejecución sin aumentar el uso de recursos. Esto significa tarifas de transacción más bajas para los usuarios y tiempos de confirmación de transacciones significativamente mejorados.
El empaquetamiento de bloques optimiza el uso del espacio de blobs de Starknet en Ethereum L1: con el empaquetamiento de bloques, los secuenciadores pueden generar una prueba para verificar varios bloques L2 de Starknet simultáneamente. Esto desvincula el uso del espacio de blobs de la frecuencia de producción de bloques L2 y amortiza los costos de verificación de pruebas. Ambos reducen los costos operativos del secuenciador de Starknet, lo que significa que los usuarios pagan menos por transacción.
Como dijimos, Bolt hace que Starknet sea más barata y rápida. Este informe proporcionará un análisis detallado de la actualización de Bolt (centrándose en la ejecución paralela y el empaquetamiento de bloques) y explorará las implicaciones para el rendimiento de Starknet.
Los rollups son soluciones de escalado de capa dos (L2) que apuntan a escalar la cadena de bloques subyacente de capa uno (L1) moviendo la computación fuera de la cadena. Al mover la ejecución fuera de la cadena, los rollups pueden optimizar la escalabilidad (transacciones baratas y rápidas), mientras que la capa 1 brinda seguridad para las transacciones de capa 2.
A menudo se dice que los rollups “heredan la seguridad de la capa 1”. Esto significa, en esencia, que heredan las garantías de consenso y disponibilidad de datos que ofrece la capa 1. Además, la capa 1 también ofrece una garantía de seguridad en forma de puente seguro entre ella y el rollup.
Cuando los secuenciadores publican bloques L2 en L1, L1 ofrece garantías de la disponibilidad y el orden de esta información. A partir de aquí, los nodos L2 pueden calcular sin confianza la cadena L2 canónica con esta información junto con las reglas de la acumulación en torno a la derivación de la cadena y la transición de estado descritas por la implementación del nodo.
Para facilitar la conexión segura entre la L1 y la L2, la L1 requiere una prueba de que la cadena L2 que sigue actualmente es correcta y no incluye cambios de estado ilegales (por ejemplo, doble gasto). Esta necesidad de que los rollups demuestren la validez de los cambios de estado garantiza que la L1 no autorice retiros del rollup en función de un estado ilegal.
Los rollups difieren según cómo prueban la validez de los cambios de estado en la L1:
Los rollups también proporcionan a la capa base datos suficientes para que las partes interesadas reconstruyan el estado de L2. Mientras que los rollups optimistas deben publicar datos completos de transacciones para permitir que los retadores calculen pruebas de fraude, los rollups de validez no tienen tales requisitos (las pruebas de validez garantizan la ejecución correcta). Pero publicar datos completos de transacciones en L1 sigue siendo útil desde una perspectiva de minimización de la confianza (reconstrucción del estado sin confianza y retiros sin permiso).
Starknet es un conjunto de validación que utiliza argumentos de conocimiento transparentes y escalables (STARK) para demostrar la validez de los cambios de estado. La última actualización de Starknet (con nombre en código Bolt) agrega ejecución paralela y empaquetamiento de bloques. En las secciones siguientes, explicaremos cómo funcionan las dos funciones y qué mejoras aportan a los usuarios de Starknet.
A un alto nivel, la actualización de Bolt cambió los mecanismos de ejecución, prueba y disponibilidad de datos de Starknet.
Antes de la actualización de Bolt, las transacciones de Starknet se ejecutaban secuencialmente (una tras otra) mediante el secuenciador. La ejecución secuencial es sencilla, pero también muy ineficiente. Es ineficiente porque no aprovecha las múltiples unidades de procesamiento independientes que ofrecen las computadoras modernas y la posibilidad de paralelizar un conjunto de transacciones.
La paralelización es una medida de cuán independientes son las transacciones en un conjunto determinado. Por ejemplo, considere el siguiente conjunto de tres transacciones:
Transacción 1: Alice quiere enviarle a Bob 1 STRK
Transacción 2: Caitlyn quiere enviarle a Danny 100 ETH
Transacción 3: Caitlyn quiere enviarle a Ella 100 ETH
La transacción 1 es completamente independiente de las transacciones 2 y 3 (porque accede a una parte diferente del estado, el saldo de Alice) y se puede ejecutar simultáneamente. Sin embargo, las transacciones 2 y 3 están en conflicto porque quieren acceder al mismo estado (el saldo de ETH de Caitlyn). Estas transacciones no se pueden ejecutar simultáneamente o terminaremos con resultados conflictivos.
Para ilustrarlo:
Para evitar este tipo de conflictos (y la naturaleza compleja de los mecanismos de mitigación), Ethereum eligió la ejecución secuencial. Sin embargo, si bien la ejecución secuencial reduce la complejidad y mejora la seguridad, da como resultado un uso ineficiente del hardware. Peor aún, la tendencia del diseño de hardware sugiere que la ejecución secuencial se volverá cada vez más ineficiente en los próximos años.
La figura 4 muestra la tendencia del diseño de hardware en los últimos 50 años. ¿Cuál es la conclusión relevante? El rendimiento de un solo subproceso (círculos violeta) se ha estancado desde mediados de la década de 2000, mientras que la cantidad de núcleos lógicos ha aumentado aproximadamente al mismo tiempo. Podemos sacar dos conclusiones en función de estos datos:
Los diseñadores de hardware están escalando los chips de computadora agregando más unidades de procesamiento independientes en lugar de mejorar el rendimiento de una sola unidad.
Cualquier sistema que continúe dependiendo del aumento del rendimiento de una sola unidad de procesamiento experimentará un estancamiento en las ganancias de rendimiento incluso en hardware más nuevo.
En los últimos años han aparecido algoritmos sofisticados para gestionar los conflictos de transacciones y garantizar la corrección de la ejecución paralela. Block-STM (basado en un artículo de Fikunmi et al*) es uno de esos algoritmos y forma la parte central del nuevo motor de ejecución paralela de Starknet. Analizaremos el algoritmo Block-STM en secciones posteriores.
SHARP (abreviatura de Shared Prover) de Starknet siempre ha aprovechado las pruebas recursivas para reducir los costos de verificación tanto como sea posible. Una prueba recursiva es esencialmente una "prueba de prueba" en la que una prueba verifica que una o más pruebas son correctas. A continuación se muestra un esquema de cómo SHARP genera una prueba recursiva:
El sistema SHARP toma como entrada un conjunto de programas que se van a ejecutar (un “trabajo”) y genera una prueba de ejecución del trabajo. Estos “programas” son bloques L2 y la prueba da fe de la corrección de las transacciones.
La prueba se envía a otro programa que la verifica y convierte el programa de verificación de pruebas en un trabajo. SHARP toma el nuevo trabajo como entrada y genera otra prueba (esta prueba confirma la validez de la prueba anterior).
El proceso (prueba → trabajo → prueba) se reinicia y continúa hasta que se alcanza un objetivo, momento en el que la prueba final (que ahora es una versión altamente comprimida de la prueba original) se envía a L1.
Este diseño amortiza en gran medida los costos por dos razones principales:
Si bien el sistema de pruebas era bueno, se desaprovecharon oportunidades de ahorrar costos. Por ejemplo, cada trabajo consistía en un solo bloque de Starknet y cada uno de estos bloques estaba diseñado para ocupar una gota en la L1. Esto generó ciertas ineficiencias, como se describe a continuación:
El empaquetamiento de bloques resuelve estos problemas mediante el uso de un árbol binario de pruebas recursivas. Analizaremos el empaquetamiento de bloques en una sección posterior del artículo.
Como se mencionó anteriormente, la ejecución secuencial es ineficiente (y lo será aún más a medida que pase el tiempo) y la ejecución paralela ingenua produce resultados no válidos. Sin embargo, los motores de ejecución paralela de producción se encargan de evitar resultados inconsistentes.
Existen dos enfoques para abordar la ejecución paralela: el control de concurrencia pesimista (PCC) y el control de concurrencia optimista (OCC) . El PCC y el OCC son unidades de procesamiento de transacciones (TPU). A continuación, se incluye una definición de unidad de procesamiento de transacciones de Block-STM frente a SVM: una comparación de motores de ejecución paralela:
La TPU suele estar acoplada a la máquina virtual (VM), pero es distinta de ella. Las VM de blockchain como EVM, SVM y MoveVM son VM de lenguaje de alto nivel... La TPU, que suele ser el tema de interés, subsume a la VM. Se encarga de la gestión de todo el proceso de ejecución de transacciones, incluida la creación y gestión de instancias de la VM.
El control de concurrencia pesimista está diseñado a partir del supuesto de que muchas de las transacciones dentro del conjunto de transacciones a ejecutar entrarán en conflicto, es decir, tocarán el mismo estado. El PCC evita estos conflictos.
Para evitar conflictos, PCC requiere que una transacción declare por adelantado a qué partes del estado accederá durante las operaciones de lectura/escritura. La unidad de procesamiento de transacciones puede usar esta información para programar transacciones de manera que se asegure que las transacciones conflictivas se ejecuten de manera secuencial (en lugar de hacerlo de manera simultánea). Algunas TPU también usan bloqueos para hacer cumplir este comportamiento (un bloqueo (también conocido como mutex) es un mecanismo que se usa para evitar el acceso simultáneo a una ubicación de memoria).
Dicho esto, la ejecución basada en PCC implica ciertas desventajas. En primer lugar, el requisito de proporcionar listas de acceso (que identifican la ranura de memoria que toca una transacción) degrada la experiencia del desarrollador y reduce la variedad de posibles aplicaciones. En segundo lugar, la programación de transacciones puede generar una sobrecarga innecesaria, especialmente cuando no hay conflictos.
El control de concurrencia optimista está diseñado con la suposición de que muchas de las transacciones dentro del conjunto dado no entrarán en conflicto, es decir, no escribirán en el mismo estado. Como tal, las TPU OCC ejecutan el conjunto de transacciones con todos los recursos disponibles y solo intentan detectar conflictos. Si se detecta un conflicto, las transacciones que entran en conflicto se ejecutan y se vuelven a verificar hasta que todo el conjunto pase y pueda confirmarse.
Las TPU de OCC no incurren en gastos generales por la programación, por lo que tienden a funcionar mejor cuando hay pocos conflictos. Las unidades de procesamiento de transacciones basadas en OCC también ofrecen una mejor experiencia para los desarrolladores y una gama más amplia de casos de uso porque no es necesario conocer las dependencias de estado de antemano.
Sin embargo, cuando el conjunto de transacciones es muy contencioso, la OCC tiene un peor desempeño que la PCC. Analizamos los diseños de TPU (con mucho más detalle) y comparamos los enfoques de OCC y PCC en nuestro artículo sobre ejecución paralela.
La nueva TPU de Starknet utiliza el enfoque OCC. Más específicamente, es una implementación del algoritmo Block-STM. Block-STM ejecuta transacciones de manera optimista con todos los recursos disponibles asumiendo que ninguno de ellos entrará en conflicto y verifica después de la ejecución que no se ejecuten transacciones conflictivas simultáneamente. Antes de profundizar en la nueva arquitectura de Starknet, es importante repasar algunas definiciones clave:
txj
depende de (o es una dependencia de) una transacción txi
si y solo si ambas transacciones escriben en la misma ubicación de memoria y txj
viene después de txi
en el orden serial. Si txi
viniera después de txj
, txi
dependería de txj
.Con las definiciones fuera del camino, podemos pasar a cubrir cómo funciona Block-STM.
La entrada a Block-STM es una cola (una lista ordenada) de transacciones, a esta lista a menudo se la llama BLOQUE. Esta lista se puede ordenar de cualquier manera; el único requisito es que exista un orden claramente definido. Por lo tanto, dado un conjunto de transacciones T que contiene transacciones {t0…tn}
, las transacciones se ordenan de manera que {t0 > t1 > t2 … > tn}
(léase: t0
tiene mayor prioridad que t1
, t1
tiene mayor prioridad que t2
etc.)
Al comienzo del proceso de ejecución, se crean dos conjuntos: un conjunto de ejecución E y un conjunto de validación V. E rastrea las transacciones que aún no se han ejecutado, mientras que V rastrea las transacciones que se han ejecutado pero aún no se han validado. Cada transacción también está asociada con un número de encarnación n para rastrear cuántas veces se ha ejecutado (y vuelto a ejecutar). El estado inicial de los conjuntos es que E contiene todas las transacciones y V está vacío, es decir, E = {t0,1 > t1,1 > t2,1 > … > tn,1}
y V = {}
.
Con estos conjuntos ordenados de transacciones, cada hilo utilizado para la ejecución recorre un bucle de tres etapas:
Durante este paso, el hilo verifica tanto V como E. Si ambos están vacíos y no se está ejecutando ninguna transacción, entonces el lote actual de transacciones se ha ejecutado completamente y los resultados se pueden confirmar en el almacenamiento.
Si V o E contienen transacciones, Block-STM selecciona la transacción con el índice más bajo (no el número de encarnación) de ambos conjuntos de transacciones, es decir, si E contiene {t1,3 , t3,1 and t5,2}
y V contiene {t0,1, t2,4, t4,3}
, la tarea de validación para la transacción t0
se seleccionaría como la siguiente tarea.
Una vez identificada y seleccionada la siguiente tarea, se ejecuta. Al final de este paso, el algoritmo vuelve a la etapa de verificación realizada. Este proceso continúa hasta que ambos conjuntos de transacciones estén vacíos.
Veamos qué sucede durante la ejecución y la validación:
Durante la ejecución de una transacción, el algoritmo Block-STM llena dos conjuntos (por transacción); un conjunto de lectura ( Ri,n
) y un conjunto de escritura ( Wn,i
). El conjunto de lectura contiene todas las ubicaciones de memoria desde las que una transacción leyó durante su ejecución, mientras que el conjunto de escritura contiene todas las ubicaciones de memoria en las que escribió. Durante la ejecución, las transacciones aplican sus escrituras a la estructura de datos de múltiples versiones, pero la lectura es un poco matizada.
En Block-STM, cuando una transacción desea leer de la estructura de datos, busca el valor escrito por la transacción de menor prioridad que tiene mayor prioridad. Por ejemplo, si tx1
, tx2
y tx7
han escrito en una ubicación de memoria y tx5
desea leer desde esta ubicación, lee la versión de la estructura de datos correspondiente a tx2
.
Esto se hace para reforzar la serialización; dado que tx5
se debe ejecutar después de tx2
y antes de tx7
, debe utilizar los valores escritos por tx2
y no tx7
. En este ejemplo, tx7
tendrá que volver a ejecutarse porque debería haber leído los valores escritos por tx5
, no tx2
ni ninguna transacción de mayor prioridad. Si se utilizó una estructura de datos de una sola versión, el valor escrito por tx2
no estaría disponible y seguramente se produciría un conflicto.
Para una tarea de validación, el conjunto de lectura de la transacción se compara con los valores actuales en las ubicaciones de memoria de las que leyó durante la ejecución. Por ejemplo, si tx2
lee la cuenta B durante la ejecución, durante la validación, se lee la ubicación de memoria de la cuenta B (teniendo en cuenta la definición de lectura que establecimos anteriormente). Si los dos valores son iguales, significa que ninguna transacción de mayor prioridad (por ejemplo, tx0
o tx1
) ha escrito en esa ubicación durante la ejecución de tx2
. Esto da como resultado que tx2
se marque como validado, pero no sea seguro confirmarlo.
No se considera seguro confirmar la transacción porque, por diversos motivos, se podría ejecutar una transacción de menor prioridad después de que se haya validado la transacción. En nuestro ejemplo en ejecución, si tx1
toca la cuenta B y solo la toca después de que tx2
pase la validación, entonces tx2
debe volver a ejecutarse.
Para prever esto, cada vez que una transacción completa su ejecución, todas las transacciones de menor prioridad que habían pasado la validación se vuelven a validar para garantizar que no entren en conflicto con la transacción. Por ejemplo, si tx1
, tx3
y tx4
se han validado y tx2
completa su ejecución, tx3
y tx4
deben volver a validarse para garantizar que no entren en conflicto con tx2
(y que, por lo tanto, no sean dependencias de esta).
Si una transacción falla la validación, es decir, una transacción de mayor prioridad que escribe en el mismo estado se ejecutó simultáneamente con ella, entonces las escrituras que realizó la transacción son sucias (porque los valores son incorrectos). Pero en lugar de eliminar estos valores de la base de datos por completo, se marcan como ESTIMATE.
La bandera ESTIMATE le indica a cualquier transacción que lea esa ubicación de memoria que los valores allí no son correctos y las transacciones pausan su ejecución. Esto se hace en lugar de eliminar, ya que la reejecución de la transacción que no pasó la validación probablemente resultaría en la escritura en las mismas ubicaciones de memoria que la ejecución anterior.
Al marcar la ubicación de la memoria como una estimación en lugar de eliminarla, se pueden detectar las dependencias (de la transacción que no superó la validación) incluso antes de volver a ejecutarla, lo que evita ejecuciones innecesarias. Esta heurística reduce en gran medida el trabajo desperdiciado.
Una descripción completa de cómo Block-STM aborda la paralelización se puede resumir de la siguiente manera:
BLOCK
de transacciones comienza como una lista ordenada con un orden de secuencia claramente definido. Estas transacciones se ejecutan en todos los recursos disponibles en orden de prioridad.
A continuación se muestra un ejemplo:
Esta es una descripción general de cómo funciona Block-STM; puede encontrar más detalles en nuestro informe aquí y en el documento original de Block-STM aquí .
Para cuantificar la importancia de agregar Block-STM, realizamos algunos puntos de referencia para evaluar las mejoras de rendimiento que proporciona en comparación con la ejecución secuencial y los resultados se muestran a continuación.
Los resultados muestran que a medida que aumenta el número de subprocesos (análogos a las unidades de procesamiento independientes) utilizados, también aumenta el rendimiento. Las mejoras son más pronunciadas con bloques más grandes, lo que da como resultado mejoras de rendimiento de hasta 9 veces en comparación con la ejecución secuencial con solo 16 subprocesos. Descubrimos que los resultados son más pronunciados con bloques más grandes.
Nuestras pruebas muestran que el rendimiento de Block-STM se degrada significativamente bajo cargas muy conflictivas, pero la práctica estándar de la industria es volver a la ejecución secuencial durante esos períodos. Recomendamos la misma mecánica a Starknet para preservar el rendimiento bajo cargas de trabajo muy conflictivas. Pero, en general, la incorporación de Block-STM mejorará significativamente y garantizará el futuro de Starknet.
El segundo cambio importante incluido en la actualización v0.13.2 es el empaquetado de bloques y lo abordaremos a continuación.
Como se mencionó anteriormente, antes de Bolt, cada bloque de Starknet era su propio trabajo, lo que daba como resultado un costo fijo por bloque. Además, el sistema estaba diseñado de tal manera que cada bloque requería su propio blob independientemente de la cantidad de datos que consumiera realmente el bloque.
En un mundo en el que siempre hubiera una gran demanda, esto no sería un problema, pero Starknet actualmente ofrece más espacio de bloques del que hay demanda y, por lo tanto, hay una gran cantidad de recursos desperdiciados que podrían resultar en la pérdida de cientos de ETH en el transcurso de un mes. Para poner en contexto aún más la gravedad de la situación antes de Bolt, estos son los costos asociados con la publicación de un bloque en L1:
Esto suma un total de 215 000 de gas por bloque y este costo es fijo, es decir, es el mismo independientemente de la cantidad de datos que contenga cada bloque y está relacionado con la cantidad de bloques mediante $Costo = num blocks * 215 000$. La solución ideal para este problema sería que los costos estuvieran relacionados con la cantidad de datos publicados en lugar de la cantidad de bloques. Y eso es exactamente lo que se logra con el empaquetamiento de bloques a través de árboles SNAR.
Los árboles recursivos aplicativos Starknet (SNAR) son un nuevo tipo de árbol binario introducido en Bolt para abordar los problemas destacados anteriormente. Un árbol SNAR tiene la siguiente estructura: cada hoja es un bloque Starknet y los nodos en todos los demás niveles son pruebas recursivas de sus hijos. El nodo raíz, que es una prueba recursiva de todas las demás pruebas, es el trabajo final que se envía al SHARed Prover (SHARP).
El principal beneficio del árbol SNAR es que en lugar de publicar un bloque por prueba, se pueden amortizar muchos bloques de Starknet en la misma actualización L1. Las raíces del árbol SNAR ahora se publican en L1 solo cuando se alcanza uno de los dos límites configurables: el límite de DA (6 blobs de datos) o después de que se agregue una cierta cantidad de hojas al árbol (donde una hoja es un bloque).
Este diseño desvincula el costo de las transacciones de la cantidad de bloques. Ahora bien, todavía existe un costo fijo por cada bloque que surge de ejecutar el sistema operativo StarkNet (SNOS) en cada bloque, pero en general los costos están desvinculados. Estos son los números actuales:
El gráfico de la Figura 11 a continuación muestra cómo varían los costos del gas según el número de bloque en el diseño anterior y ahora (bajo Bolt):
Es bastante obvio que el empaquetamiento de bloques reduce en gran medida los costos de verificación en la L1, lo que sin duda resultará en precios de gas más bajos para los usuarios de Starknet.
El efecto de los cambios realizados en Bolt: ejecución paralela optimista a través de Block-STM y empaquetamiento de bloques a través del SNAR propietario se puede resumir como: más rápido y más barato.
La ejecución paralela reduce el tiempo de ejecución y, por extensión, la congestión, lo que reducirá las tarifas de gas durante períodos de alto tráfico, mientras que los árboles SNAR abordan los costos asociados de DA y de prueba. Curiosamente, esta actualización convierte a Starknet en la primera L2 con ejecución paralela y la prepara para ser un contendiente importante en el espacio L2. Es importante señalar que es poco probable que el efecto de estos cambios sea evidente de inmediato, especialmente el de la ejecución paralela, pero son cruciales para proteger a Starknet y a todo el ecosistema Ethereum en su conjunto para el futuro.
Nota del autor: Una versión de este artículo fue publicada previamente aquí .