Cover photo

Precios de gas multidimensional

Traducción del artículo de Vitalik Buterin del 9 de mayo de 2024.

Agradecimientos especiales a Ansgar Dietrichs, Barnabe Monnot y Davide Crapis por sus comentarios y revisión.

En Ethereum, los recursos estaban limitados y tarifados hasta hace poco mediante un único recurso denominado "gas". El gas mide la cantidad de "esfuerzo computacional" necesario para procesar una transacción o bloque dado. El gas agrupa múltiples tipos de "esfuerzo", notablemente:

  • Computación cruda (por ejemplo, ADD, MULTIPLY)

  • Lectura y escritura en el almacenamiento de Ethereum (por ejemplo, SSTORE, SLOAD, transferencias de ETH)

  • Ancho de banda de datos

  • Costo de generar una prueba ZK-SNARK del bloque

Por ejemplo, esta transacción que envié costó un total de 47,085 gas. Esto se divide entre (i) un "costo base" de 21000 gas, (ii) 1556 gas por los bytes en los datos de llamada incluidos como parte de la transacción (iii) 16500 gas por la lectura y escritura en almacenamiento, (iv) 2149 gas por hacer un “log” o registro, y el resto por la ejecución EVM. La tarifa de la transacción que un usuario debe pagar es proporcional al gas que consume la transacción. Un bloque puede contener hasta un máximo de 30 millones de gas, y los precios del gas se ajustan constantemente a través del mecanismo de targeting de EIP-1559, asegurando que en promedio, los bloques contengan 15 millones de gas.

Este enfoque tiene una gran eficiencia: debido a que todo se fusiona en un recurso virtual, conduce a un diseño de mercado muy simple. Optimizar una transacción para minimizar costos es fácil, optimizar un bloque para recolectar las tarifas más altas posibles es relativamente fácil (sin incluir MEV), y no hay incentivos extraños que alienten a algunas transacciones a agruparse con otras para ahorrar en tarifas.

Sin embargo, este enfoque también tiene una gran ineficiencia: trata diferentes recursos como si fueran mutuamente convertibles, cuando los límites subyacentes reales de lo que la red puede manejar no lo son. Una forma de entender este problema es mirar este diagrama:

post image

El límite de gas impone una restricción de x1 * data+x2 * computation < N. La restricción de seguridad subyacente real a menudo está más cerca de max (x1 * data,x2 * computation) < N. Esta discrepancia lleva a que el límite de gas excluya innecesariamente bloques que son realmente seguros, o acepte bloques que son realmente inseguros, o alguna mezcla de ambos.

Si hay recursos que tienen límites de seguridad distintos, entonces el gas unidimensional plausiblemente reduce el rendimiento hasta en un factor de n. Por esta razón, ha habido interés durante mucho tiempo en el concepto de gas multidimensional, y con el EIP-4844 ahora tenemos gas multidimensional funcionando en Ethereum hoy. Esta publicación explora los beneficios de este enfoque y las perspectivas para aumentarlo aún más.

Blobs: gas multidimensional en Dencun

Al inicio de este año, el bloque promedio tenía un tamaño de 150 kB. Una gran parte de ese tamaño corresponde a datos de rollup: protocolos de capa 2 que almacenan datos en la cadena por seguridad. Estos datos eran costosos: aunque las transacciones en los rollups costaban aproximadamente 5-10x menos que las transacciones correspondientes en la capa 1 de Ethereum, incluso ese costo era demasiado alto para muchos casos de uso.

¿Por qué no reducir el costo del gas para los "calldata” o datos de llamada (actualmente 16 gas por byte no cero y 4 gas por zero byte) para abaratar los rollups? Ya lo hicimos antes, podríamos hacerlo de nuevo. La respuesta aquí es: el tamaño máximo de un bloque en el peor de los casos era de 30,000,000 / 16 = 1,875,000 nonzero bites, y la red ya apenas puede manejar bloques de ese tamaño. Reducir los costos por otro factor de 4x aumentaría el máximo a 7.5 MB, lo que sería un gran riesgo para la seguridad.

Este problema terminó siendo manejado mediante la introducción de un espacio separado para datos amigables con los rollups, conocidos como "blobs", en cada bloque. Los dos recursos tienen precios y límites separados: después del hard fork de Dencun, un bloque de Ethereum puede contener como máximo (i) 30 millones de gas, y (ii) 6 blobs, que pueden contener aproximadamente 125 kB de datos de llamada cada uno. Ambos recursos tienen precios separados, ajustados por mecanismos de fijación de precios similares a EIP-1559, con el objetivo de un uso promedio de 15 millones de gas y 3 blobs por bloque.

Como resultado, los rollups se han vuelto 100 veces más baratos, el volumen de transacciones en los rollups ha aumentado más de 3 veces, y el tamaño máximo teórico del bloque solo aumentó ligeramente: de aproximadamente 1.9 MB a aproximadamente 2.6 MB.

Las tarifas de transacción en los rollups, cortesía de growthepie.xyz. El fork de Dencun, que introdujo blobs con precios multidimensionales, ocurrió el 13 de marzo de 2024. https://www.growthepie.xyz/fundamentals/transaction-costs
Las tarifas de transacción en los rollups, cortesía de growthepie.xyz. El fork de Dencun, que introdujo blobs con precios multidimensionales, ocurrió el 13 de marzo de 2024. https://www.growthepie.xyz/fundamentals/transaction-costs

Gas multidimensional y clientes sin estado

En un futuro cercano, surgirá un problema similar en cuanto a las pruebas de almacenamiento para los “stateless clients” o clientes sin estado. Los clientes sin estado son un nuevo tipo de cliente que podrá verificar la cadena sin almacenar muchos datos o ninguno localmente. Los clientes sin estado logran esto aceptando pruebas de las piezas específicas del estado de Ethereum que las transacciones en ese bloque necesitan tocar.

Un cliente sin estado recibe un bloque, junto con pruebas que demuestran los valores actuales en partes específicas del estado (por ejemplo, saldos de cuentas, código, almacenamiento) que el bloque en ejecución toca. Esto permite a un nodo verificar un bloque sin tener almacenamiento propio.
Un cliente sin estado recibe un bloque, junto con pruebas que demuestran los valores actuales en partes específicas del estado (por ejemplo, saldos de cuentas, código, almacenamiento) que el bloque en ejecución toca. Esto permite a un nodo verificar un bloque sin tener almacenamiento propio.

Una lectura de almacenamiento cuesta entre 2100 y 2600 gas dependiendo del tipo de lectura, y las escrituras de almacenamiento cuestan más. En promedio, un bloque realiza algo así como 1000 lecturas y escrituras de almacenamiento (incluyendo verificaciones de saldo de ETH, llamadas a SSTORE y SLOAD, lectura de código de contrato y otras operaciones). El máximo teórico, sin embargo, es de 30,000,000 / 2,100 = 14,285 lecturas. La carga de ancho de banda de un cliente sin estado es directamente proporcional a este número.

Hoy, el plan para apoyar a los clientes sin estado consiste en cambiar el diseño del árbol de estado de Ethereum de árboles Merkle Patricia a árboles Verkle. Sin embargo, los árboles Verkle no son resistentes a la computación cuántica y no son óptimos para las nuevas generaciones de sistemas de prueba STARK. Como resultado, muchas personas están interesadas en apoyar a los clientes sin estado mediante árboles Merkle binarios y STARKs en su lugar, ya sea omitiendo completamente Verkle o actualizando un par de años después de la transición a Verkle, una vez que los STARKs hayan madurado más.

Las pruebas STARK de las ramas de árboles de hash binarios tienen muchas ventajas, pero tienen la debilidad clave de que las pruebas tardan mucho en generarse: mientras que los árboles Verkle pueden probar más de cien mil valores por segundo, los STARK basados en hash típicamente pueden probar solo un par de miles de hashes por segundo, y probar cada valor requiere una "rama" que contiene muchos hashes.

Dada la información proyectada hoy de sistemas de prueba hiper-optimizados como Binius y Plonky3, y hashes especializados como Vision-Mark-32, parece probable que por algún tiempo estemos en un régimen donde sea práctico probar 1,000 valores en menos de un segundo, pero no 14,285 valores. Los bloques promedio estarían bien, pero los bloques en el peor caso, potencialmente publicados por un atacante, romperían la red.

La forma "predeterminada" en que hemos manejado tal escenario es mediante la re-precificación: hacer la lectura de almacenamiento más cara para reducir el máximo por bloque a algo más seguro. Sin embargo, ya hemos hecho esto muchas veces, y hacerlo de nuevo haría que muchas aplicaciones fueran demasiado caras. Un enfoque mejor sería el gas multidimensional: limitar y cobrar por el acceso al almacenamiento por separado, manteniendo el uso promedio en 1,000 accesos de almacenamiento por bloque, pero estableciendo un límite por bloque de, por ejemplo, 2,000.

Gas multidimensional más generalmente

Otro recurso que vale la pena considerar es el crecimiento del tamaño del estado: operaciones que aumentan el tamaño del estado de Ethereum, que los nodos completos necesitarán mantener de ahí en adelante. La propiedad única del crecimiento del tamaño del estado es que la razón para limitarlo proviene completamente del uso sostenido a largo plazo, y no de picos. Por lo tanto, podría ser valioso agregar una dimensión de gas separada para operaciones que aumentan el tamaño del estado (por ejemplo, SSTORE de cero a no cero, creación de contratos), pero con un objetivo diferente: podríamos establecer un precio flotante para apuntar a un uso promedio específico, pero no establecer ningún límite por bloque en absoluto.

Esto muestra una de las propiedades poderosas del gas multidimensional: nos permite preguntar por separado (i) cuál es el uso promedio ideal y (ii) cuál es el máximo uso seguro por bloque, para cada recurso. En lugar de establecer precios de gas basados en máximos por bloque, y permitir que el uso promedio siga, tenemos 2n grados de libertad para establecer 2n parámetros, ajustando cada uno según lo que sea seguro para la red.

Situaciones más complicadas, como aquellas en las que dos recursos tienen consideraciones de seguridad que son parcialmente aditivas, podrían manejarse haciendo que un opcode o recurso cueste cierta cantidad de múltiples tipos de gas (por ejemplo, un SSTORE de cero a no cero podría costar 5000 gas de prueba de cliente sin estado y 20000 gas de expansión de almacenamiento).

Máximo por transacción: la forma más débil pero más fácil de implementar el gas multidimensional

Sea x1 el costo en gas de los datos y x2 el costo en gas de la computación, entonces en un sistema de gas unidimensional podemos escribir el costo en gas de una transacción como:

gas = x1 * data + x2 * computación

En este esquema, en lugar de ello, definimos el costo en gas de una transacción como:

gas = max ( x1 * data, x2 * computación)

Es decir, en lugar de que se cobre a una transacción por datos más computación, la transacción se cobra en base a cuál de los dos recursos consume más. Esto puede extenderse fácilmente para cubrir más dimensiones. Por ejemplo:

max(…, x3 * storage_access)

Debería ser fácil ver cómo esto mejora el rendimiento mientras preserva la seguridad. La cantidad máxima teórica de datos en un bloque sigue siendo GASLIMIT / x1, exáctamente la misma que en el esquema de gas unidimensional. De manera similar, la cantidad máxima teórica de cómputo es GASLIMIT / x2, de nuevo, la misma que en el esquema de gas unidimensional. Sin embargo, el costo en gas de cualquier transacción que consume tanto datos como cómputo disminuye.

Este es aproximadamente el esquema empleado en la propuesta EIP-7623, para reducir el tamaño máximo del bloque mientras se aumenta aún más el número de blobs. El mecanismo preciso en EIP-7623 es ligeramente más complicado: mantiene el precio actual de los datos de llamada de 16 gas por byte, pero agrega un "precio mínimo" de 48 gas por byte; una transacción paga el mayor entre (16 * bytes + execution_gas) y (48 * bytes). Como resultado, EIP-7623 disminuye el calldata máximo teórico de transacción en un bloque de aproximadamente 1.9 MB a aproximadamente 0.6 MB, mientras deja los costos de la mayoría de las aplicaciones sin cambios. El beneficio de este enfoque es que es un cambio muy pequeño desde el esquema actual de gas unidimensional, por lo que es muy fácil de implementar.

Hay dos inconvenientes:

  1. Las transacciones que consumen muchos recursos en una sola área todavía se cargan innecesariamente con una gran cantidad, incluso si todas las otras transacciones en el bloque usan poco de ese recurso.

  2. Crea incentivos para que las transacciones con gran cantidad de datos y las que requieren mucho cálculo se fusionen en un paquete para ahorrar costos.

Podría argumentar que una regla al estilo de EIP-7623, tanto para los datos de llamada de las transacciones como para otros recursos, puede traer beneficios suficientemente grandes como para valer la pena a pesar de estos inconvenientes. Sin embargo, si estamos dispuestos a invertir un esfuerzo de desarrollo significativamente mayor, hay un enfoque más ideal.

EIP-1559 multidimensional: la estrategia ideal pero más compleja

Primero, repasemos cómo funciona el EIP-1559 "regular". Nos centraremos en la versión que se introdujo en el EIP-4844 para blobs, ya que es matemáticamente más elegante.

Rastreamos un parámetro, excess_blobs. Durante cada bloque, establecemos:

excess_blobs <-- max(excess_blobs + len(block.blobs) - TARGET, 0)

Donde TARGET = 3. Es decir, si un bloque tiene más blobs que el objetivo, excess_blobs aumenta, y si un bloque tiene menos que el objetivo, disminuye. Luego establecemos blob_basefee = exp(excess_blobs / 25.47), donde exp es una aproximación de la función exp(x) = 2.71828x.

post image

Es decir, cada vez que excess_blobs aumenta en aproximadamente 25, la tarifa base de los blobs aumenta en un factor de aproximadamente 2.7. Si los blobs se vuelven demasiado caros, el uso promedio disminuye y excess_blobs comienza a disminuir, bajando automáticamente el precio de nuevo. El precio de un blob se ajusta constantemente para asegurarse de que, en promedio, los bloques estén medio llenos, es decir, contengan un promedio de 3 blobs cada uno.

Si hay un pico de uso a corto plazo, entonces el límite entra en juego: cada bloque solo puede contener un máximo de 6 blobs, y en tal circunstancia las transacciones pueden competir entre sí aumentando sus tarifas de prioridad. En el caso normal, sin embargo, cada blob solo necesita pagar el blob_basefee más una pequeña tarifa de prioridad extra como incentivo para ser incluido.

Este tipo de precio existió en Ethereum para el gas durante años: un mecanismo muy similar se introdujo con el EIP-1559 en 2020. Con el EIP-4844, ahora tenemos dos precios flotantes separados para el gas y para los blobs.

La tarifa base del gas durante el transcurso de una hora el 8 de mayo de 2024, en gwei, según la fuente ultrasound.money.
La tarifa base del gas durante el transcurso de una hora el 8 de mayo de 2024, en gwei, según la fuente ultrasound.money.

En principio, podríamos agregar más tarifas flotantes separadas para la lectura de almacenamiento y otros tipos de operaciones, aunque con una advertencia que expandiré en la siguiente sección.

Para los usuarios, la experiencia es notablemente similar a la de hoy: en lugar de pagar una tarifa base, pagas dos tarifas base, pero tu billetera puede abstraer eso de ti y simplemente mostrarte la tarifa esperada y la tarifa máxima que puedes esperar pagar.

Para los constructores de bloques, la mayoría del tiempo la estrategia óptima es la misma que hoy: incluir cualquier cosa que sea válida. La mayoría de los bloques no están llenos, ni en gas ni en blobs. El caso desafiante es cuando hay suficiente gas o suficientes blobs para superar el límite del bloque, y el constructor necesita resolver potencialmente un problema de la mochila multidimensional para maximizar sus ganancias. Sin embargo, incluso allí existen algoritmos de aproximación bastante buenos, y las ganancias de hacer algoritmos propietarios para optimizar las ganancias en este caso son mucho menores que las ganancias de hacer lo mismo con MEV.

Para los desarrolladores, el principal desafío es la necesidad de rediseñar características del EVM y su infraestructura circundante, que actualmente están diseñadas alrededor de un precio y un límite, en un diseño que acomode múltiples precios y múltiples límites. Un problema para los desarrolladores de aplicaciones es que la optimización se vuelve un poco más difícil: en algunos casos, ya no puedes decir de manera unívoca que A es más eficiente que B, porque si A usa más datos de llamada pero B usa más ejecución, entonces A podría ser más barato cuando los datos de llamada son baratos, y más caro cuando los datos de llamada son caros. Sin embargo, los desarrolladores todavía podrían obtener resultados razonablemente buenos al optimizar basándose en precios promedio históricos a largo plazo.

Precios multidimensionales, la EVM y las sub-llamadas

Existe un problema que no apareció con los blobs, y que no aparecerá con el EIP-7623 o incluso con una implementación "completa" de precios multidimensionales para los datos de llamada, pero que sí aparecerá si intentamos tarificar por separado los accesos al estado, o cualquier otro recurso: los límites de gas en las sub-llamadas.

Los límites de gas en la EVM existen en dos lugares. Primero, cada transacción establece un límite de gas, que limita la cantidad total de gas que se puede usar en esa transacción. Segundo, cuando un contrato llama a otro contrato, la llamada puede establecer su propio límite de gas. Esto permite que los contratos llamen a otros contratos en los que no confían, y aún así garantizar que tendrán gas suficiente para realizar otras computaciones después de esa llamada.

Una traza de una transacción de abstracción de cuenta, donde una cuenta llama a otra cuenta, y solo le da al llamado una cantidad limitada de gas, para asegurar que la llamada exterior pueda continuar funcionando incluso si el llamado consume todo el gas que se le asignó.
Una traza de una transacción de abstracción de cuenta, donde una cuenta llama a otra cuenta, y solo le da al llamado una cantidad limitada de gas, para asegurar que la llamada exterior pueda continuar funcionando incluso si el llamado consume todo el gas que se le asignó.

El desafío es el siguiente: hacer que el gas sea multidimensional entre diferentes tipos de ejecución parece requerir que las sub-llamadas proporcionen múltiples límites para cada tipo de gas, lo que requeriría un cambio muy profundo en el EVM, y no sería compatible con las aplicaciones existentes.

Esta es una razón por la cual las propuestas de gas multidimensional a menudo se detienen en dos dimensiones: datos y ejecución. Los datos (ya sea calldata de transacciones o blobs) solo se asignan fuera del EVM, por lo que nada dentro del EVM necesita cambiar para que el calldata o los blobs tengan precios separados.

Podemos pensar en una "solución al estilo EIP-7623" para este problema. Aquí hay una implementación simple: durante la ejecución, se cobra 4 veces más por las operaciones de almacenamiento; para simplificar el análisis, digamos 10000 gas por operación de almacenamiento. Al final de la transacción, se reembolsa min(7500 * storage_operations, execution_gas). El resultado sería que, después de restar el reembolso, se le cobraría al usuario:

execution_gas + 10000 * storage_operations - min(7500 * storage_operations, execution_gas)

Lo que es igual a:

max(execution_gas + 2500 * storage_operations, 10000 * storage_operations)

Esto refleja la estructura del EIP-7623. Otra manera de hacerlo es rastrear storage_operations y execution_gas en tiempo real, y cobrar 2500 o 10000 dependiendo de cuánto aumente max(execution_gas + 2500 * storage_operations, 10000 * storage_operations) en el momento en que se llama al opcode. Esto evita la necesidad de que las transacciones sobreasignen gas que en su mayoría recuperarán a través de reembolsos.

No obtenemos permisos detallados para sub-llamadas: una sub-llamada podría consumir toda la "asignación" de una transacción para operaciones de almacenamiento baratas. Pero sí obtenemos algo suficientemente bueno, donde un contrato que realiza una sub-llamada puede establecer un límite y asegurarse de que, una vez que la sub-llamada termina de ejecutarse, la llamada principal todavía tiene suficiente gas para hacer cualquier post-procesamiento que necesite realizar.

La solución "más sencilla a la tarificación multidimensional completa" que puedo pensar es tratamos los límites de gas de sub-llamadas como proporcionales. Es decir, supongamos que hay k diferentes tipos de ejecución, y cada transacción establece un límite multidimensional L1… Lk. Supongamos que, en el punto actual de la ejecución, el gas restante es g1… gk. Supongamos que se llama a un opcode CALL, con un límite de gas de sub-llamada S. Dejemos que s1 = S y luego s2 = s1 / g1 * g2, s3 = s1 / g1 * g3 y así sucesivamente.

Es decir, tratamos el primer tipo de gas (realísticamente, ejecución de VM) como una especie de "unidad de cuenta" privilegiada, y luego asignamos los otros tipos de gas de manera que la sub-llamada obtenga el mismo porcentaje de gas disponible en cada tipo. Esto es algo feo, pero maximiza la compatibilidad con versiones anteriores. Si queremos hacer el esquema más "neutral" entre diferentes tipos de gas, a costa de sacrificar la compatibilidad con versiones anteriores, simplemente podríamos hacer que el parámetro del límite de gas de sub-llamada represente una fracción (por ejemplo, [1...63] / 64) del gas restante en el contexto actual).

En cualquier caso, sin embargo, vale la pena enfatizar que una vez que comienzas a introducir gas de ejecución multidimensional, el nivel inherente de fealdad aumenta, y esto parece difícil de evitar. Por lo tanto, nuestra tarea es hacer un intercambio complicado: ¿aceptamos algo más de fealdad a nivel de EVM, con el fin de desbloquear de manera segura ganancias significativas de escalabilidad en L1, y si es así, cuál propuesta específica funciona mejor para la economía del protocolo y los desarrolladores de aplicaciones? Es muy probable que no sea ninguna de las que mencioné anteriormente, y todavía hay espacio para idear algo más elegante y mejor.