Un agradecimiento especial a Yoav Weiss, Dan Finlay, Martin Koppelmann y a los equipos de Arbitrum, Optimism, Polygon, Scroll y SoulWallet por sus comentarios y reseñas.
En esta publicación sobre las Tres Transiciones, describí algunas razones clave por las que es valioso comenzar a pensar explícitamente en el soporte L1 + Cross-L2, la seguridad de la billetera y la privacidad como características básicas necesarias de la pila del ecosistema, en lugar de construir cada una de estas cosas como complementos que pueden ser diseñados por separado por billeteras individuales.
Esta publicación se centrará más directamente en los aspectos técnicos de un subproblema específico: cómo facilitar la lectura de L1 de L2, L2 de L1 o L2 de otra L2. Resolver este problema es crucial para implementar una arquitectura de separación de activos / almacén de claves, pero también tiene casos de uso valiosos en otras áreas, sobre todo la optimización de llamadas confiables entre L2, incluidos casos de uso como mover activos entre L1 y L2.
Una vez que las L2 se generalicen, los usuarios tendrán activos en varias L2 y, posiblemente, también en L1. Una vez que las billeteras de contratos inteligentes (multifirma, recuperación social o de otro tipo) se conviertan en la corriente principal, las claves necesarias para acceder a alguna cuenta cambiarán con el tiempo, y las claves antiguas ya no tendrían que ser válidas. Una vez que sucedan ambas cosas, un usuario deberá tener una forma de cambiar las claves que tienen autoridad para acceder a muchas cuentas que viven en muchos lugares diferentes, sin realizar un número extremadamente alto de transacciones.
En particular, necesitamos una forma de manejar las direcciones contrafácticas: direcciones que aún no han sido "registradas" de ninguna manera en la cadena, pero que, sin embargo, necesitan recibir y mantener fondos de forma segura. Todos dependemos de las direcciones contrafácticas: cuando usas Ethereum por primera vez, puedes generar una dirección ETH que alguien puede usar para pagarte, sin "registrar" la dirección en la cadena (lo que requeriría pagar txfees y, por lo tanto, ya tener algo de ETH).
Con las EOA, todas las direcciones comienzan como direcciones contrafactuales. Con las billeteras de contratos inteligentes, las direcciones contrafácticas aún son posibles, en gran parte gracias a CREATE2, que le permite tener una dirección ETH que solo puede ser completada por un contrato inteligente que tiene un código que coincide con un hash en particular.
Algoritmo de cálculo de direcciones EIP-1014 (CREATE2).
Sin embargo, las billeteras de contratos inteligentes introducen un nuevo desafío: la posibilidad de cambiar las claves de acceso. La dirección, que es un hash del código de inicio, solo puede contener la clave de verificación inicial de la billetera. La clave de verificación actual se almacenaría en el almacenamiento de la billetera, pero ese registro de almacenamiento no se propaga mágicamente a otras L2.
Si un usuario tiene muchas direcciones en muchas L2, incluidas direcciones que (porque son contrafácticas) la L2 en la que se encuentra no conoce, parece que solo hay una forma de permitir que los usuarios cambien sus claves: arquitectura de separación de activos/almacén de claves. Cada usuario tiene (i) un "contrato de almacén de claves" (en L1 o en un L2 en particular), que almacena la clave de verificación para todas las billeteras junto con las reglas para cambiar la clave, y (ii) "contratos de billetera" en L1 y muchos L2, que leen la cadena cruzada para obtener la clave de verificación.
Hay dos formas de implementar esto:
Para mostrar toda la complejidad, exploraremos el caso más difícil: donde el almacén de claves está en una L2 y la billetera está en una L2 diferente. Si el almacén de claves o la billetera están en L1, entonces solo se necesita la mitad de este diseño.
Supongamos que el almacén de claves está en Linea y la billetera está en Kakarot. Una prueba completa de las claves de la billetera consiste en:
Aquí hay dos preguntas principales y complicadas de implementación:
Hay cinco opciones principales:
En términos de obras de infraestructura requeridas y costo para los usuarios, los clasifico aproximadamente de la siguiente manera:
"Agregación" se refiere a la idea de agregar todas las pruebas proporcionadas por los usuarios dentro de cada bloque en una gran meta-prueba que las combina todas. Esto es posible para SNARKs y para KZG, pero no para las ramas de Merkle (puede combinar un poco las ramas de Merkle, pero solo le ahorra log(txs por bloque) / log (número total de almacenes de claves), tal vez un 15-30% en la práctica, por lo que probablemente no valga la pena el costo).
La agregación solo vale la pena una vez que el esquema tiene un número sustancial de usuarios, por lo que, siendo realistas, está bien que una implementación de la versión 1 omita la agregación e implemente eso para la versión 2.
Esto es simple: siga el diagrama de la sección anterior directamente. Más precisamente, cada "prueba" (asumiendo el caso de máxima dificultad de probar una L2 en otra L2) contendría:
Desafortunadamente, las pruebas de estado de Ethereum son complicadas, pero existen bibliotecas para verificarlas, y si usa estas bibliotecas, este mecanismo no es demasiado complicado de implementar.
El problema más grande es el costo. Las pruebas de Merkle son largas, y los árboles de Patricia son, por desgracia, ~3,9 veces más largas de lo necesario (precisamente: una prueba de Merkle ideal en un árbol que contiene N objetos tiene 32 log2(N) bytes de largo, y debido a que los árboles Patricia de Ethereum tienen 16 hojas por hijo, las pruebas para esos árboles son de 32 15 log16(N) ~= 125 log2(N) bytes de largo). En un estado con aproximadamente 250 millones (~2²⁸) cuentas, esto hace que cada prueba sea de 125 * 28 = 3500 bytes, o alrededor de 56,000 de gas, más costos adicionales para decodificar y verificar hashes.
Dos pruebas juntas terminarían costando alrededor de 100,000 a 150,000 gas (sin incluir la verificación de la firma si se usa por transacción), significativamente más que la base actual de 21,000 gas por transacción. Pero la disparidad empeora si la prueba se verifica en L2. La computación dentro de una L2 es barata, porque la computación se realiza fuera de la cadena y en un ecosistema con muchos menos nodos que L1. Los datos, por otro lado, tienen que ser contabilizados en L1. Por lo tanto, la comparación no es de 21000 gases frente a 150.000 gases; son 21.000 L2 de gasolina frente a 100.000 L1 de gas.
Podemos calcular lo que esto significa observando las comparaciones entre los costos del gas L1 y los costos del gas L2:
L1 es actualmente entre 15 y 25 veces más caro que L2 para envíos simples, y entre 20 y 50 veces más caro para intercambios de tokens. Los envíos simples son relativamente pesados en cuanto a datos, pero los intercambios son mucho más pesados desde el punto de vista computacional. Por lo tanto, los swaps son un mejor punto de referencia para aproximar el costo del cálculo de L1 frente al cálculo de L2. Teniendo todo esto en cuenta, si asumimos una relación de costo de 30x entre el costo de cálculo de L1 y el costo de cálculo de L2, esto parece implicar que poner una prueba de Merkle en L2 costará el equivalente a quizás cincuenta transacciones regulares.
Por supuesto, el uso de un árbol de Merkle binario puede reducir los costos en ~ 4 veces, pero aún así, el costo en la mayoría de los casos será demasiado alto, y si estamos dispuestos a hacer el sacrificio de ya no ser compatibles con el árbol de estado hexatorio actual de Ethereum, también podríamos buscar opciones aún mejores.
Conceptualmente, el uso de ZK-SNARKs también es fácil de entender: simplemente reemplaza las pruebas de Merkle en el diagrama anterior con un ZK-SNARK que demuestre que esas pruebas de Merkle existen. Un ZK-SNARK cuesta ~400.000 de gas de cálculo, y unos 400 bytes (compare: 21.000 de gas y 100 bytes para una transacción básica, en el futuro reducible a ~25 bytes con compresión). Por lo tanto, desde una perspectiva computacional, un ZK-SNARK cuesta 19 veces el costo de una transacción básica hoy en día, y desde una perspectiva de datos, un ZK-SNARK cuesta 4 veces más que una transacción básica hoy en día, y 16 veces más de lo que puede costar una transacción básica en el futuro.
Estos números son una mejora masiva con respecto a las pruebas de Merkle, pero siguen siendo bastante caros. Hay dos formas de mejorar esto: (i) pruebas KZG de propósito especial, o (ii) agregación, similar a la agregación ERC-4337 pero usando matemáticas más sofisticadas. Podemos investigar ambos.
Advertencia, esta sección es mucho más matemática que otras secciones. Esto se debe a que vamos más allá de las herramientas de propósito general y construimos algo de propósito especial para que sea más barato, por lo que tenemos que ir mucho más "bajo el capó". Si no te gustan las matemáticas profundas, pasa directamente a la siguiente sección.
Primero, un resumen de cómo funcionan los compromisos de KZG:
Algunas propiedades clave que es importante comprender son:
Por lo tanto, tenemos una estructura en la que podemos seguir agregando valores al final de una lista cada vez mayor, aunque con un cierto límite de tamaño (siendo realistas, cientos de millones podrían ser viables). A continuación, lo utilizamos como nuestra estructura de datos para gestionar (i) un compromiso con la lista de claves en cada L2, almacenado en esa L2 y reflejado en L1, y (ii) un compromiso con la lista de compromisos de claves L2, almacenado en la L1 de Ethereum y reflejado en cada L2.
Mantener los compromisos actualizados podría convertirse en parte de la lógica básica de L2, o podría implementarse sin cambios en el protocolo central de L2 a través de puentes de depósito y retiro.
Por lo tanto, una prueba completa requeriría:
De hecho, es posible fusionar las dos pruebas de KZG en una, por lo que obtenemos un tamaño total de solo 100 bytes.
Tenga en cuenta una sutileza: debido a que la lista de claves es una lista, y no un mapa clave/valor como lo es el estado, la lista de claves tendrá que asignar posiciones secuencialmente. El contrato de compromiso de claves contendría su propio registro interno que asignaría cada almacén de claves a un ID, y para cada clave almacenaría hash (clave, dirección del almacén de claves) en lugar de solo clave, para comunicar inequívocamente a otras L2 de qué almacén de claves está hablando una entrada en particular.
La ventaja de esta técnica es que funciona muy bien en L2. Los datos son de 100 bytes, ~4 veces más cortos que un ZK-SNARK y mucho más cortos que una prueba de Merkle. El costo de cálculo es en gran medida una verificación de emparejamiento de tamaño 2, o alrededor de 119,000 gas. En L1, los datos son menos importantes que la computación, por lo que desafortunadamente KZG es algo más caro que las pruebas de Merkle.
Básicamente, los árboles de Verkle implican apilar los compromisos de KZG (o los compromisos de IPA, que pueden ser más eficientes y utilizar una criptografía más sencilla) uno encima del otro: para almacenar valores de 2⁴⁸, puede hacer un compromiso de KZG con una lista de valores de 2²⁴, cada uno de los cuales es a su vez un compromiso de KZG con valores de 2²⁴. Los árboles Verkle están siendo <a href="https://notes.ethereum.org/@vbuterin/verkle_tree_eip">fuertemente considerado para el árbol de estado de Ethereum, porque los árboles de Verkle se pueden usar para contener mapas clave-valor y no solo listas (básicamente, puede hacer un árbol de tamaño 2²⁵⁶ pero comenzarlo vacío, solo completando partes específicas del árbol una vez que realmente necesite llenarlas).
Cómo es un árbol Verkle. En la práctica, puede asignar a cada nodo un ancho de 256 == 2⁸ para los árboles basados en IPA, o 2²⁴ para los árboles basados en KZG.
Las pruebas en los árboles Verkle son algo más largas que las de KZG; Pueden tener unos pocos cientos de bytes de longitud. También son difíciles de verificar, especialmente si intentas agregar muchas pruebas en una.
Siendo realistas, los árboles Verkle deben considerarse como los árboles Merkle, pero más viables sin SNARKing (debido a los menores costos de datos) y más baratos con SNARKing (debido a los menores costos de prueba).
La mayor ventaja de los árboles de Verkle es la posibilidad de armonizar las estructuras de datos: las pruebas de Verkle se pueden usar directamente sobre el estado L1 o L2, sin estructuras superpuestas, y utilizando exactamente el mismo mecanismo para L1 y L2. Una vez que las computadoras cuánticas se conviertan en un problema, o una vez que las ramas de Merkle se vuelvan lo suficientemente eficientes, los árboles Verkle podrían reemplazarse en el lugar con un árbol hash binario con una función hash adecuada compatible con SNARK.
Si N usuarios realizan N transacciones (o más realistamente, N ERC-4337 UserOperations) que necesitan probar N afirmaciones entre cadenas, podemos ahorrar mucho gas agregando esas pruebas: el constructor que combinaría esas transacciones en un bloque o paquete que entra en un bloque puede crear una sola prueba que pruebe todas esas afirmaciones simultáneamente.
Esto podría significar:
En los tres casos, las pruebas solo costarían unos pocos cientos de miles de gas cada una. El constructor tendría que hacer uno de estos en cada L2 para los usuarios de esa L2; por lo tanto, para que esto sea útil de construir, el esquema en su conjunto debe tener suficiente uso como para que muy a menudo haya al menos algunas transacciones dentro del mismo bloque en múltiples L2 principales.
Si se utilizan ZK-SNARK, el principal costo marginal es simplemente la "lógica comercial" de pasar números entre contratos, por lo que tal vez unos pocos miles de L2 de gas por usuario. Si se utilizan pruebas múltiples KZG, el probador necesitaría agregar 48 gas por cada L2 que tenga un almacén de claves que se use dentro de ese bloque, por lo que el costo marginal del esquema por usuario agregaría otro ~ 800 gas L1 por L2 (no por usuario) en la parte superior. Pero estos costos son mucho más bajos que los costos de no agregar, que inevitablemente involucran más de 10,000 gas L1 y cientos de miles de gas L2 por usuario. Para los árboles Verkle, puede usar Verkle multi-proofs directamente, agregando alrededor de 100-200 bytes por usuario, o puede hacer un ZK-SNARK de un Verkle multi-proof, que tiene costos similares a los ZK-SNARK de las ramas de Merkle pero es significativamente más barato de probar.
Desde el punto de vista de la implementación, probablemente sea mejor que los agrupadores agreguen pruebas de cadena cruzada a través del estándar de abstracción de cuentas ERC-4337 . ERC-4337 ya tiene un mecanismo para que los constructores agreguen partes de UserOperations de forma personalizada. Incluso hay una <a href="https://hackmd.io/@voltrevo/BJ0QBy3zi">implementación de esto para la agregación de firmas BLS, que podría reducir los costos de gas en L2 de 1,5 a 3 veces, dependiendo de qué otras formas de compresión se incluyan.
Diagrama de una publicaciónde implementaciónde billetera <a href=" https://hackmd.io/@voltrevo /BJ0QBy3zi">BLS que muestra el flujo de trabajo de las firmas agregadas de BLS dentro de una versión anterior de ERC-4337. Es probable que el flujo de trabajo de agregación de pruebas entre cadenas sea muy similar.
Una última posibilidad, y que solo se puede utilizar para L2 que lee L1 (y no L1 que lee L2), es modificar L2 para permitirles realizar llamadas estáticas a contratos en L1 directamente.
Esto se puede hacer con un código de operación o una precompilación, que permite llamadas a L1 donde proporciona la dirección de destino, gas y calldata, y devuelve la salida, aunque debido a que estas llamadas son llamadas estáticas, en realidad no pueden cambiar ningún estado L1. Los L2 tienen que ser conscientes de L1 ya para procesar depósitos, por lo que no hay nada fundamental que impida que se implemente tal cosa; es principalmente un desafío de implementación técnica (ver: esta RFP de Optimism para admitir llamadas estáticas en L1).
Tenga en cuenta que si el almacén de claves está en L1 y las L2 integran la funcionalidad de llamada estática L1, ¡no se requieren pruebas en absoluto! Sin embargo, si las L2 no integran llamadas estáticas L1, o si el almacén de claves está en L2 (lo que eventualmente tendrá que estar, una vez que L1 se vuelva demasiado caro para que los usuarios lo usen aunque sea un poco), se requerirán pruebas.
Todos los esquemas anteriores requieren que la L2 acceda a la raíz del estado L1 reciente o a todo el estado L1 reciente. Afortunadamente, todas las L2 ya tienen alguna funcionalidad para acceder al estado L1 reciente. Esto se debe a que necesitan una funcionalidad de este tipo para procesar los mensajes que llegan de L1 a L2, sobre todo los depósitos.
Y, de hecho, si una L2 tiene una función de depósito, entonces puede usar esa L2 tal cual para mover las raíces de estado de L1 a un contrato en la L2: simplemente haga que un contrato en L1 llame al código de operación BLOCKHASH y páselo a L2 como un mensaje de depósito. Se puede recibir el encabezado de bloque completo y extraer su raíz de estado en el lado L2. Sin embargo, sería mucho mejor que cada L2 tuviera una forma explícita de acceder directamente al estado L1 reciente completo o a las raíces del estado L1 reciente.
El principal desafío con la optimización de la forma en que las L2 reciben raíces de estado L1 recientes es lograr simultáneamente seguridad y baja latencia:
Además, en sentido inverso (L1s leyendo L2):
Algunas de estas velocidades para las operaciones de cadena cruzada sin confianza son inaceptablemente lentas para muchos casos de uso de Defi; Para esos casos, necesita puentes más rápidos con modelos de seguridad más imperfectos. Sin embargo, para el caso de uso de la actualización de las claves de la billetera, los retrasos más largos son más aceptables: no está retrasando las transacciones por horas, está retrasando los cambios de clave. Solo tendrás que mantener las llaves viejas por más tiempo. Si está cambiando las claves porque las roban, entonces tiene un período significativo de vulnerabilidad, pero esto se puede mitigar, por ejemplo. por billeteras que tienen una función de congelación.
En última instancia, la mejor solución para minimizar la latencia es que las L2 implementen la lectura directa de las raíces de estado L1 de una manera óptima, donde cada bloque L2 (o el registro de cálculo de la raíz de estado) contiene un puntero al bloque L1 más reciente, por lo que si L1 se revierte, L2 también puede revertirse. Los contratos de almacén de claves deben colocarse en la red principal o en las L2 que son ZK-rollups y, por lo tanto, pueden confirmarse rápidamente en L1.
Los bloques de la cadena L2 pueden tener dependencias no solo de bloques L2 anteriores, sino también de un bloque L1. Si L1 revierte más allá de dicho enlace, L2 también se revierte. Vale la pena señalar que así es también como se previó que funcionara una versión anterior (anterior a Dank) de la fragmentación; Consulte aquí el código.
Sorprendentemente, no tanto. En realidad, ni siquiera es necesario que sea un rollup: si es un L3 o un validium, entonces está bien mantener billeteras allí, siempre que tenga almacenes de claves en L1 o en un rollup ZK. Lo que sí se necesita es que la cadena tenga acceso directo a las raíces del estado de Ethereum, y un compromiso técnico y social para estar dispuesta a reorganizarse si Ethereum se reorganiza, y a una bifurcación dura si Ethereum se bifurca.
Un problema de investigación interesante es identificar hasta qué punto es posible que una cadena tenga esta forma de conexión con muchas otras cadenas (p. ej. Ethereum y Zcash). Hacerlo ingenuamente es posible: su cadena podría acordar la reorganización si Ethereum o Zcash se reorganizan (y la bifurcación dura si Ethereum o Zcash se bifurcan duramente), pero entonces los operadores de sus nodos y su comunidad en general tienen el doble de dependencias técnicas y políticas. Por lo tanto, esta técnica podría usarse para conectarse a algunas otras cadenas, pero a un costo cada vez mayor. Los esquemas basados en puentes ZK tienen propiedades técnicas atractivas, pero tienen la debilidad clave de que no son robustos a los ataques del 51% o a las bifurcaciones duras. Puede haber soluciones más inteligentes.
Idealmente, también queremos preservar la privacidad. Si tiene muchas billeteras administradas por el mismo almacén de claves, queremos asegurarnos de que:
Esto crea algunos problemas:
Con los SNARK, las soluciones son conceptualmente fáciles: las pruebas ocultan información de forma predeterminada y el agregador necesita producir un SNARK recursivo para probar los SNARK.
El principal desafío de este enfoque hoy en día es que la agregación requiere que el agregador cree un SNARK recursivo, que actualmente es bastante lento.
Con KZG, podemos usar <a href="https://notes.ethereum.org/@vbuterin/non_index_revealing_proof">this trabajo sobre pruebas de KZG que no revelan índices (véase también: una versión más formalizada de ese trabajo en el artículo de Calulk ) como punto de partida. Sin embargo, la agregación de pruebas ciegas es un problema abierto que requiere más atención.
La lectura directa de L1 desde dentro de L2, desafortunadamente, no preserva la privacidad, aunque la implementación de la funcionalidad de lectura directa sigue siendo muy útil, tanto para minimizar la latencia como por su utilidad para otras aplicaciones.
Un agradecimiento especial a Yoav Weiss, Dan Finlay, Martin Koppelmann y a los equipos de Arbitrum, Optimism, Polygon, Scroll y SoulWallet por sus comentarios y reseñas.
En esta publicación sobre las Tres Transiciones, describí algunas razones clave por las que es valioso comenzar a pensar explícitamente en el soporte L1 + Cross-L2, la seguridad de la billetera y la privacidad como características básicas necesarias de la pila del ecosistema, en lugar de construir cada una de estas cosas como complementos que pueden ser diseñados por separado por billeteras individuales.
Esta publicación se centrará más directamente en los aspectos técnicos de un subproblema específico: cómo facilitar la lectura de L1 de L2, L2 de L1 o L2 de otra L2. Resolver este problema es crucial para implementar una arquitectura de separación de activos / almacén de claves, pero también tiene casos de uso valiosos en otras áreas, sobre todo la optimización de llamadas confiables entre L2, incluidos casos de uso como mover activos entre L1 y L2.
Una vez que las L2 se generalicen, los usuarios tendrán activos en varias L2 y, posiblemente, también en L1. Una vez que las billeteras de contratos inteligentes (multifirma, recuperación social o de otro tipo) se conviertan en la corriente principal, las claves necesarias para acceder a alguna cuenta cambiarán con el tiempo, y las claves antiguas ya no tendrían que ser válidas. Una vez que sucedan ambas cosas, un usuario deberá tener una forma de cambiar las claves que tienen autoridad para acceder a muchas cuentas que viven en muchos lugares diferentes, sin realizar un número extremadamente alto de transacciones.
En particular, necesitamos una forma de manejar las direcciones contrafácticas: direcciones que aún no han sido "registradas" de ninguna manera en la cadena, pero que, sin embargo, necesitan recibir y mantener fondos de forma segura. Todos dependemos de las direcciones contrafácticas: cuando usas Ethereum por primera vez, puedes generar una dirección ETH que alguien puede usar para pagarte, sin "registrar" la dirección en la cadena (lo que requeriría pagar txfees y, por lo tanto, ya tener algo de ETH).
Con las EOA, todas las direcciones comienzan como direcciones contrafactuales. Con las billeteras de contratos inteligentes, las direcciones contrafácticas aún son posibles, en gran parte gracias a CREATE2, que le permite tener una dirección ETH que solo puede ser completada por un contrato inteligente que tiene un código que coincide con un hash en particular.
Algoritmo de cálculo de direcciones EIP-1014 (CREATE2).
Sin embargo, las billeteras de contratos inteligentes introducen un nuevo desafío: la posibilidad de cambiar las claves de acceso. La dirección, que es un hash del código de inicio, solo puede contener la clave de verificación inicial de la billetera. La clave de verificación actual se almacenaría en el almacenamiento de la billetera, pero ese registro de almacenamiento no se propaga mágicamente a otras L2.
Si un usuario tiene muchas direcciones en muchas L2, incluidas direcciones que (porque son contrafácticas) la L2 en la que se encuentra no conoce, parece que solo hay una forma de permitir que los usuarios cambien sus claves: arquitectura de separación de activos/almacén de claves. Cada usuario tiene (i) un "contrato de almacén de claves" (en L1 o en un L2 en particular), que almacena la clave de verificación para todas las billeteras junto con las reglas para cambiar la clave, y (ii) "contratos de billetera" en L1 y muchos L2, que leen la cadena cruzada para obtener la clave de verificación.
Hay dos formas de implementar esto:
Para mostrar toda la complejidad, exploraremos el caso más difícil: donde el almacén de claves está en una L2 y la billetera está en una L2 diferente. Si el almacén de claves o la billetera están en L1, entonces solo se necesita la mitad de este diseño.
Supongamos que el almacén de claves está en Linea y la billetera está en Kakarot. Una prueba completa de las claves de la billetera consiste en:
Aquí hay dos preguntas principales y complicadas de implementación:
Hay cinco opciones principales:
En términos de obras de infraestructura requeridas y costo para los usuarios, los clasifico aproximadamente de la siguiente manera:
"Agregación" se refiere a la idea de agregar todas las pruebas proporcionadas por los usuarios dentro de cada bloque en una gran meta-prueba que las combina todas. Esto es posible para SNARKs y para KZG, pero no para las ramas de Merkle (puede combinar un poco las ramas de Merkle, pero solo le ahorra log(txs por bloque) / log (número total de almacenes de claves), tal vez un 15-30% en la práctica, por lo que probablemente no valga la pena el costo).
La agregación solo vale la pena una vez que el esquema tiene un número sustancial de usuarios, por lo que, siendo realistas, está bien que una implementación de la versión 1 omita la agregación e implemente eso para la versión 2.
Esto es simple: siga el diagrama de la sección anterior directamente. Más precisamente, cada "prueba" (asumiendo el caso de máxima dificultad de probar una L2 en otra L2) contendría:
Desafortunadamente, las pruebas de estado de Ethereum son complicadas, pero existen bibliotecas para verificarlas, y si usa estas bibliotecas, este mecanismo no es demasiado complicado de implementar.
El problema más grande es el costo. Las pruebas de Merkle son largas, y los árboles de Patricia son, por desgracia, ~3,9 veces más largas de lo necesario (precisamente: una prueba de Merkle ideal en un árbol que contiene N objetos tiene 32 log2(N) bytes de largo, y debido a que los árboles Patricia de Ethereum tienen 16 hojas por hijo, las pruebas para esos árboles son de 32 15 log16(N) ~= 125 log2(N) bytes de largo). En un estado con aproximadamente 250 millones (~2²⁸) cuentas, esto hace que cada prueba sea de 125 * 28 = 3500 bytes, o alrededor de 56,000 de gas, más costos adicionales para decodificar y verificar hashes.
Dos pruebas juntas terminarían costando alrededor de 100,000 a 150,000 gas (sin incluir la verificación de la firma si se usa por transacción), significativamente más que la base actual de 21,000 gas por transacción. Pero la disparidad empeora si la prueba se verifica en L2. La computación dentro de una L2 es barata, porque la computación se realiza fuera de la cadena y en un ecosistema con muchos menos nodos que L1. Los datos, por otro lado, tienen que ser contabilizados en L1. Por lo tanto, la comparación no es de 21000 gases frente a 150.000 gases; son 21.000 L2 de gasolina frente a 100.000 L1 de gas.
Podemos calcular lo que esto significa observando las comparaciones entre los costos del gas L1 y los costos del gas L2:
L1 es actualmente entre 15 y 25 veces más caro que L2 para envíos simples, y entre 20 y 50 veces más caro para intercambios de tokens. Los envíos simples son relativamente pesados en cuanto a datos, pero los intercambios son mucho más pesados desde el punto de vista computacional. Por lo tanto, los swaps son un mejor punto de referencia para aproximar el costo del cálculo de L1 frente al cálculo de L2. Teniendo todo esto en cuenta, si asumimos una relación de costo de 30x entre el costo de cálculo de L1 y el costo de cálculo de L2, esto parece implicar que poner una prueba de Merkle en L2 costará el equivalente a quizás cincuenta transacciones regulares.
Por supuesto, el uso de un árbol de Merkle binario puede reducir los costos en ~ 4 veces, pero aún así, el costo en la mayoría de los casos será demasiado alto, y si estamos dispuestos a hacer el sacrificio de ya no ser compatibles con el árbol de estado hexatorio actual de Ethereum, también podríamos buscar opciones aún mejores.
Conceptualmente, el uso de ZK-SNARKs también es fácil de entender: simplemente reemplaza las pruebas de Merkle en el diagrama anterior con un ZK-SNARK que demuestre que esas pruebas de Merkle existen. Un ZK-SNARK cuesta ~400.000 de gas de cálculo, y unos 400 bytes (compare: 21.000 de gas y 100 bytes para una transacción básica, en el futuro reducible a ~25 bytes con compresión). Por lo tanto, desde una perspectiva computacional, un ZK-SNARK cuesta 19 veces el costo de una transacción básica hoy en día, y desde una perspectiva de datos, un ZK-SNARK cuesta 4 veces más que una transacción básica hoy en día, y 16 veces más de lo que puede costar una transacción básica en el futuro.
Estos números son una mejora masiva con respecto a las pruebas de Merkle, pero siguen siendo bastante caros. Hay dos formas de mejorar esto: (i) pruebas KZG de propósito especial, o (ii) agregación, similar a la agregación ERC-4337 pero usando matemáticas más sofisticadas. Podemos investigar ambos.
Advertencia, esta sección es mucho más matemática que otras secciones. Esto se debe a que vamos más allá de las herramientas de propósito general y construimos algo de propósito especial para que sea más barato, por lo que tenemos que ir mucho más "bajo el capó". Si no te gustan las matemáticas profundas, pasa directamente a la siguiente sección.
Primero, un resumen de cómo funcionan los compromisos de KZG:
Algunas propiedades clave que es importante comprender son:
Por lo tanto, tenemos una estructura en la que podemos seguir agregando valores al final de una lista cada vez mayor, aunque con un cierto límite de tamaño (siendo realistas, cientos de millones podrían ser viables). A continuación, lo utilizamos como nuestra estructura de datos para gestionar (i) un compromiso con la lista de claves en cada L2, almacenado en esa L2 y reflejado en L1, y (ii) un compromiso con la lista de compromisos de claves L2, almacenado en la L1 de Ethereum y reflejado en cada L2.
Mantener los compromisos actualizados podría convertirse en parte de la lógica básica de L2, o podría implementarse sin cambios en el protocolo central de L2 a través de puentes de depósito y retiro.
Por lo tanto, una prueba completa requeriría:
De hecho, es posible fusionar las dos pruebas de KZG en una, por lo que obtenemos un tamaño total de solo 100 bytes.
Tenga en cuenta una sutileza: debido a que la lista de claves es una lista, y no un mapa clave/valor como lo es el estado, la lista de claves tendrá que asignar posiciones secuencialmente. El contrato de compromiso de claves contendría su propio registro interno que asignaría cada almacén de claves a un ID, y para cada clave almacenaría hash (clave, dirección del almacén de claves) en lugar de solo clave, para comunicar inequívocamente a otras L2 de qué almacén de claves está hablando una entrada en particular.
La ventaja de esta técnica es que funciona muy bien en L2. Los datos son de 100 bytes, ~4 veces más cortos que un ZK-SNARK y mucho más cortos que una prueba de Merkle. El costo de cálculo es en gran medida una verificación de emparejamiento de tamaño 2, o alrededor de 119,000 gas. En L1, los datos son menos importantes que la computación, por lo que desafortunadamente KZG es algo más caro que las pruebas de Merkle.
Básicamente, los árboles de Verkle implican apilar los compromisos de KZG (o los compromisos de IPA, que pueden ser más eficientes y utilizar una criptografía más sencilla) uno encima del otro: para almacenar valores de 2⁴⁸, puede hacer un compromiso de KZG con una lista de valores de 2²⁴, cada uno de los cuales es a su vez un compromiso de KZG con valores de 2²⁴. Los árboles Verkle están siendo <a href="https://notes.ethereum.org/@vbuterin/verkle_tree_eip">fuertemente considerado para el árbol de estado de Ethereum, porque los árboles de Verkle se pueden usar para contener mapas clave-valor y no solo listas (básicamente, puede hacer un árbol de tamaño 2²⁵⁶ pero comenzarlo vacío, solo completando partes específicas del árbol una vez que realmente necesite llenarlas).
Cómo es un árbol Verkle. En la práctica, puede asignar a cada nodo un ancho de 256 == 2⁸ para los árboles basados en IPA, o 2²⁴ para los árboles basados en KZG.
Las pruebas en los árboles Verkle son algo más largas que las de KZG; Pueden tener unos pocos cientos de bytes de longitud. También son difíciles de verificar, especialmente si intentas agregar muchas pruebas en una.
Siendo realistas, los árboles Verkle deben considerarse como los árboles Merkle, pero más viables sin SNARKing (debido a los menores costos de datos) y más baratos con SNARKing (debido a los menores costos de prueba).
La mayor ventaja de los árboles de Verkle es la posibilidad de armonizar las estructuras de datos: las pruebas de Verkle se pueden usar directamente sobre el estado L1 o L2, sin estructuras superpuestas, y utilizando exactamente el mismo mecanismo para L1 y L2. Una vez que las computadoras cuánticas se conviertan en un problema, o una vez que las ramas de Merkle se vuelvan lo suficientemente eficientes, los árboles Verkle podrían reemplazarse en el lugar con un árbol hash binario con una función hash adecuada compatible con SNARK.
Si N usuarios realizan N transacciones (o más realistamente, N ERC-4337 UserOperations) que necesitan probar N afirmaciones entre cadenas, podemos ahorrar mucho gas agregando esas pruebas: el constructor que combinaría esas transacciones en un bloque o paquete que entra en un bloque puede crear una sola prueba que pruebe todas esas afirmaciones simultáneamente.
Esto podría significar:
En los tres casos, las pruebas solo costarían unos pocos cientos de miles de gas cada una. El constructor tendría que hacer uno de estos en cada L2 para los usuarios de esa L2; por lo tanto, para que esto sea útil de construir, el esquema en su conjunto debe tener suficiente uso como para que muy a menudo haya al menos algunas transacciones dentro del mismo bloque en múltiples L2 principales.
Si se utilizan ZK-SNARK, el principal costo marginal es simplemente la "lógica comercial" de pasar números entre contratos, por lo que tal vez unos pocos miles de L2 de gas por usuario. Si se utilizan pruebas múltiples KZG, el probador necesitaría agregar 48 gas por cada L2 que tenga un almacén de claves que se use dentro de ese bloque, por lo que el costo marginal del esquema por usuario agregaría otro ~ 800 gas L1 por L2 (no por usuario) en la parte superior. Pero estos costos son mucho más bajos que los costos de no agregar, que inevitablemente involucran más de 10,000 gas L1 y cientos de miles de gas L2 por usuario. Para los árboles Verkle, puede usar Verkle multi-proofs directamente, agregando alrededor de 100-200 bytes por usuario, o puede hacer un ZK-SNARK de un Verkle multi-proof, que tiene costos similares a los ZK-SNARK de las ramas de Merkle pero es significativamente más barato de probar.
Desde el punto de vista de la implementación, probablemente sea mejor que los agrupadores agreguen pruebas de cadena cruzada a través del estándar de abstracción de cuentas ERC-4337 . ERC-4337 ya tiene un mecanismo para que los constructores agreguen partes de UserOperations de forma personalizada. Incluso hay una <a href="https://hackmd.io/@voltrevo/BJ0QBy3zi">implementación de esto para la agregación de firmas BLS, que podría reducir los costos de gas en L2 de 1,5 a 3 veces, dependiendo de qué otras formas de compresión se incluyan.
Diagrama de una publicaciónde implementaciónde billetera <a href=" https://hackmd.io/@voltrevo /BJ0QBy3zi">BLS que muestra el flujo de trabajo de las firmas agregadas de BLS dentro de una versión anterior de ERC-4337. Es probable que el flujo de trabajo de agregación de pruebas entre cadenas sea muy similar.
Una última posibilidad, y que solo se puede utilizar para L2 que lee L1 (y no L1 que lee L2), es modificar L2 para permitirles realizar llamadas estáticas a contratos en L1 directamente.
Esto se puede hacer con un código de operación o una precompilación, que permite llamadas a L1 donde proporciona la dirección de destino, gas y calldata, y devuelve la salida, aunque debido a que estas llamadas son llamadas estáticas, en realidad no pueden cambiar ningún estado L1. Los L2 tienen que ser conscientes de L1 ya para procesar depósitos, por lo que no hay nada fundamental que impida que se implemente tal cosa; es principalmente un desafío de implementación técnica (ver: esta RFP de Optimism para admitir llamadas estáticas en L1).
Tenga en cuenta que si el almacén de claves está en L1 y las L2 integran la funcionalidad de llamada estática L1, ¡no se requieren pruebas en absoluto! Sin embargo, si las L2 no integran llamadas estáticas L1, o si el almacén de claves está en L2 (lo que eventualmente tendrá que estar, una vez que L1 se vuelva demasiado caro para que los usuarios lo usen aunque sea un poco), se requerirán pruebas.
Todos los esquemas anteriores requieren que la L2 acceda a la raíz del estado L1 reciente o a todo el estado L1 reciente. Afortunadamente, todas las L2 ya tienen alguna funcionalidad para acceder al estado L1 reciente. Esto se debe a que necesitan una funcionalidad de este tipo para procesar los mensajes que llegan de L1 a L2, sobre todo los depósitos.
Y, de hecho, si una L2 tiene una función de depósito, entonces puede usar esa L2 tal cual para mover las raíces de estado de L1 a un contrato en la L2: simplemente haga que un contrato en L1 llame al código de operación BLOCKHASH y páselo a L2 como un mensaje de depósito. Se puede recibir el encabezado de bloque completo y extraer su raíz de estado en el lado L2. Sin embargo, sería mucho mejor que cada L2 tuviera una forma explícita de acceder directamente al estado L1 reciente completo o a las raíces del estado L1 reciente.
El principal desafío con la optimización de la forma en que las L2 reciben raíces de estado L1 recientes es lograr simultáneamente seguridad y baja latencia:
Además, en sentido inverso (L1s leyendo L2):
Algunas de estas velocidades para las operaciones de cadena cruzada sin confianza son inaceptablemente lentas para muchos casos de uso de Defi; Para esos casos, necesita puentes más rápidos con modelos de seguridad más imperfectos. Sin embargo, para el caso de uso de la actualización de las claves de la billetera, los retrasos más largos son más aceptables: no está retrasando las transacciones por horas, está retrasando los cambios de clave. Solo tendrás que mantener las llaves viejas por más tiempo. Si está cambiando las claves porque las roban, entonces tiene un período significativo de vulnerabilidad, pero esto se puede mitigar, por ejemplo. por billeteras que tienen una función de congelación.
En última instancia, la mejor solución para minimizar la latencia es que las L2 implementen la lectura directa de las raíces de estado L1 de una manera óptima, donde cada bloque L2 (o el registro de cálculo de la raíz de estado) contiene un puntero al bloque L1 más reciente, por lo que si L1 se revierte, L2 también puede revertirse. Los contratos de almacén de claves deben colocarse en la red principal o en las L2 que son ZK-rollups y, por lo tanto, pueden confirmarse rápidamente en L1.
Los bloques de la cadena L2 pueden tener dependencias no solo de bloques L2 anteriores, sino también de un bloque L1. Si L1 revierte más allá de dicho enlace, L2 también se revierte. Vale la pena señalar que así es también como se previó que funcionara una versión anterior (anterior a Dank) de la fragmentación; Consulte aquí el código.
Sorprendentemente, no tanto. En realidad, ni siquiera es necesario que sea un rollup: si es un L3 o un validium, entonces está bien mantener billeteras allí, siempre que tenga almacenes de claves en L1 o en un rollup ZK. Lo que sí se necesita es que la cadena tenga acceso directo a las raíces del estado de Ethereum, y un compromiso técnico y social para estar dispuesta a reorganizarse si Ethereum se reorganiza, y a una bifurcación dura si Ethereum se bifurca.
Un problema de investigación interesante es identificar hasta qué punto es posible que una cadena tenga esta forma de conexión con muchas otras cadenas (p. ej. Ethereum y Zcash). Hacerlo ingenuamente es posible: su cadena podría acordar la reorganización si Ethereum o Zcash se reorganizan (y la bifurcación dura si Ethereum o Zcash se bifurcan duramente), pero entonces los operadores de sus nodos y su comunidad en general tienen el doble de dependencias técnicas y políticas. Por lo tanto, esta técnica podría usarse para conectarse a algunas otras cadenas, pero a un costo cada vez mayor. Los esquemas basados en puentes ZK tienen propiedades técnicas atractivas, pero tienen la debilidad clave de que no son robustos a los ataques del 51% o a las bifurcaciones duras. Puede haber soluciones más inteligentes.
Idealmente, también queremos preservar la privacidad. Si tiene muchas billeteras administradas por el mismo almacén de claves, queremos asegurarnos de que:
Esto crea algunos problemas:
Con los SNARK, las soluciones son conceptualmente fáciles: las pruebas ocultan información de forma predeterminada y el agregador necesita producir un SNARK recursivo para probar los SNARK.
El principal desafío de este enfoque hoy en día es que la agregación requiere que el agregador cree un SNARK recursivo, que actualmente es bastante lento.
Con KZG, podemos usar <a href="https://notes.ethereum.org/@vbuterin/non_index_revealing_proof">this trabajo sobre pruebas de KZG que no revelan índices (véase también: una versión más formalizada de ese trabajo en el artículo de Calulk ) como punto de partida. Sin embargo, la agregación de pruebas ciegas es un problema abierto que requiere más atención.
La lectura directa de L1 desde dentro de L2, desafortunadamente, no preserva la privacidad, aunque la implementación de la funcionalidad de lectura directa sigue siendo muy útil, tanto para minimizar la latencia como por su utilidad para otras aplicaciones.