Las tarifas de Gas de la red principal de Ethereum siempre han sido un problema significativo, especialmente durante la congestión de la red. En los picos, los usuarios a menudo deben pagar tarifas de transacción extremadamente altas. Por lo tanto, es especialmente importante optimizar las tarifas de Gas durante la fase de desarrollo de contratos inteligentes. Optimizar el consumo de Gas no solo puede reducir efectivamente los costos de transacción, sino que también puede aumentar la eficiencia de las transacciones, proporcionando a los usuarios una experiencia de uso de blockchain más económica y eficiente.
Este documento describirá el mecanismo de tarifas de Gas de la máquina virtual de Ethereum (EVM), los conceptos clave relacionados con la optimización de tarifas de Gas, y las mejores prácticas para optimizar tarifas de Gas al desarrollar contratos inteligentes. Esperamos que este contenido inspire y brinde ayuda práctica a los desarrolladores, al mismo tiempo que ayude a los usuarios comunes a comprender mejor cómo funcionan las tarifas de Gas de la EVM, enfrentando juntos los desafíos en el ecosistema de blockchain.
Introducción al mecanismo de tarifas de Gas de la EVM
En redes compatibles con EVM, 'Gas' se refiere a la unidad utilizada para medir la capacidad computacional necesaria para ejecutar operaciones específicas.
La imagen a continuación ilustra la disposición de la estructura de la EVM. En el gráfico, el consumo de Gas se divide en tres partes: ejecución de operaciones, llamadas de mensajes externas y lectura/escritura de memoria y almacenamiento.
Fuente: sitio web oficial de Ethereum[1]
Dado que la ejecución de cada transacción requiere recursos de cálculo, se cobra una tarifa para evitar bucles infinitos y ataques de denegación de servicio (DoS). El costo requerido para completar una transacción se denomina 'tarifa de Gas'.
Desde que EIP-1559 (la bifurcación dura de Londres) entró en vigor, la tarifa de Gas se calcula mediante la siguiente fórmula:
Tarifa de Gas = unidades de gas utilizadas * (tarifa base + tarifa de prioridad)
La tarifa base se destruirá, y la tarifa de prioridad se utilizará como incentivo para alentar a los validadores a incluir transacciones en la cadena de bloques. Establecer una tarifa de prioridad más alta al enviar transacciones puede aumentar la probabilidad de que la transacción se incluya en el siguiente bloque. Esto es similar a una 'propina' que los usuarios pagan a los validadores.
1. Entender la optimización de Gas en la EVM
Cuando se compilan contratos inteligentes con Solidity, el contrato se convierte en una serie de 'códigos de operación', es decir, opcodes.
Cada código de operación (por ejemplo, crear un contrato, hacer llamadas de mensaje, acceder al almacenamiento de cuentas y ejecutar operaciones en la máquina virtual) tiene un costo de consumo de Gas reconocido, que está registrado en el libro amarillo de Ethereum[2].
Después de múltiples modificaciones de EIP, algunos costos de Gas de códigos de operación han sido ajustados y pueden diferir de los que se encuentran en el libro amarillo. Para obtener información actualizada sobre los costos de operación, consulte aquí[3].
2. Conceptos básicos de optimización de Gas
La idea central de la optimización de Gas es priorizar operaciones de costo eficiente en la blockchain de EVM y evitar operaciones que son costosas en Gas.
En la EVM, las siguientes operaciones tienen un costo bajo:
Leer y escribir variables de memoria
Leer constantes y variables inmutables
Leer y escribir variables locales
Leer variables de calldata, como arreglos y estructuras de calldata
Llamadas a funciones internas
Las operaciones de alto costo incluyen:
Leer y escribir variables de estado en el almacenamiento del contrato
Llamadas a funciones externas
Operaciones en bucle
Mejores prácticas para la optimización de tarifas de Gas en la EVM
Basado en los conceptos básicos mencionados, hemos compilado una lista de mejores prácticas para la optimización de tarifas de Gas para la comunidad de desarrolladores. Siguiendo estas prácticas, los desarrolladores pueden reducir el consumo de tarifas de Gas de los contratos inteligentes, disminuir los costos de transacción y crear aplicaciones más eficientes y amigables para el usuario.
1. Minimizar el uso de almacenamiento
En Solidity, el almacenamiento es un recurso limitado, cuyo consumo de Gas es mucho mayor que el de la memoria. Cada vez que un contrato inteligente lee o escribe datos desde el almacenamiento, incurre en altos costos de Gas.
Según la definición del libro amarillo de Ethereum, el costo de las operaciones de almacenamiento es más de 100 veces más alto que el de las operaciones de memoria. Por ejemplo, las instrucciones OPcodes mload y mstore solo consumen 3 unidades de Gas, mientras que las operaciones de almacenamiento como sload y sstore requieren al menos 100 unidades incluso en las mejores condiciones.
Métodos para limitar el uso de almacenamiento incluyen:
Almacenar datos no permanentes en memoria
Reducir el número de modificaciones de almacenamiento: al conservar resultados intermedios en memoria y asignar los resultados a variables de almacenamiento solo después de completar todos los cálculos.
2. Empaquetado de variables
El número de ranuras de almacenamiento utilizadas en los contratos inteligentes y la forma en que los desarrolladores representan los datos influirán significativamente en el consumo de tarifas de Gas.
El compilador de Solidity empaquetará variables de almacenamiento continuas durante el proceso de compilación, usando una ranura de almacenamiento de 32 bytes como unidad básica de almacenamiento de variables. El empaquetado de variables se refiere a organizar las variables de manera que varias puedan caber en una sola ranura de almacenamiento.
A la izquierda, hay una implementación menos eficiente que consumirá 3 ranuras de almacenamiento; a la derecha, hay una implementación más eficiente.
A través de este ajuste de detalle, los desarrolladores pueden ahorrar 20,000 unidades de Gas (almacenar una ranura de almacenamiento no utilizada consume 20,000 Gas), pero ahora solo se necesitan dos ranuras de almacenamiento.
Dado que cada ranura de almacenamiento consume Gas, empaquetar variables optimiza el uso de Gas al reducir la cantidad de ranuras de almacenamiento necesarias.
3. Optimizar tipos de datos
Una variable puede representarse en múltiples tipos de datos, pero los costos de operación son diferentes para cada tipo. Elegir el tipo de dato adecuado ayuda a optimizar el uso de Gas.
Por ejemplo, en Solidity, los enteros se pueden dividir en diferentes tamaños: uint8, uint16, uint32, etc. Dado que la EVM ejecuta operaciones en unidades de 256 bits, usar uint8 significa que la EVM debe convertirlo primero a uint256, lo que consume Gas adicional.
Podemos comparar los costos de Gas de uint8 y uint256 a través del código en la imagen. La función UseUint() consume 120,382 unidades de Gas, mientras que la función UseUInt8() consume 166,111 unidades de Gas.
Visto por separado, usar uint256 aquí es más barato que usar uint8. Sin embargo, si se aplica la optimización de empaquetado de variables que sugerimos anteriormente, sería diferente. Si los desarrolladores pueden empaquetar cuatro variables uint8 en una ranura de almacenamiento, el costo total de iterarlas será menor que el de cuatro variables uint256. Así, el contrato inteligente puede leer y escribir una ranura de almacenamiento a la vez, y colocar cuatro variables uint8 en memoria/almacenamiento en una sola operación.
4. Usar variables de tamaño fijo en lugar de variables dinámicas
Si los datos pueden mantenerse dentro de 32 bytes, se recomienda usar el tipo de dato bytes32 en lugar de bytes o strings. En general, las variables de tamaño fijo consumen menos Gas que las de tamaño variable. Si se puede limitar la longitud de los bytes, se debe elegir la longitud mínima de bytes desde bytes1 hasta bytes32.
5. Mapeos frente a arreglos
Las listas de datos de Solidity pueden representarse en dos tipos de datos: arreglos (Arrays) y mapeos (Mappings), pero su sintaxis y estructura son completamente diferentes.
Los mapeos, en la mayoría de los casos, son más eficientes y tienen un costo menor, pero los arreglos tienen la capacidad de iterar y admiten empaquetado de tipos de datos. Por lo tanto, se recomienda priorizar el uso de mapeos al gestionar listas de datos, a menos que se necesite iterar o se pueda optimizar el consumo de Gas mediante el empaquetado de tipos de datos.
6. Usar calldata en lugar de memoria
Las variables declaradas en los parámetros de la función pueden almacenarse en calldata o en memoria. La principal diferencia entre ambos es que la memoria puede ser modificada por la función, mientras que el calldata es inmutable.
Recuerda este principio: si los parámetros de la función son de solo lectura, se debe priorizar el uso de calldata en lugar de memoria. Esto puede evitar copias innecesarias de calldata a memoria.
Ejemplo 1: Usar memoria
Al usar la palabra clave memory, los valores del arreglo se copian de calldata codificado a memory durante el proceso de decodificación ABI. El costo de ejecución de este bloque de código es de 3,694 unidades de Gas.
Ejemplo 2: Usar calldata
Al leer valores directamente desde calldata, se omiten las operaciones intermedias de memoria. Este método de optimización reduce el costo de ejecución a solo 2,413 unidades de Gas, aumentando la eficiencia de Gas en un 35%.
7. Utilizar las palabras clave Constant/Immutable siempre que sea posible
Las variables Constant/Immutable no se almacenan en el almacenamiento del contrato. Estas variables se calculan en tiempo de compilación y se almacenan en el bytecode del contrato. Por lo tanto, en comparación con el almacenamiento, su costo de acceso es mucho menor, se recomienda usar las palabras clave Constant o Immutable siempre que sea posible.
8. Usar Unchecked cuando se asegura que no habrá desbordamiento/subdesbordamiento
Cuando los desarrolladores pueden determinar que las operaciones aritméticas no causarán desbordamiento o subdesbordamiento, pueden usar la palabra clave unchecked introducida en Solidity v0.8.0, evitando verificaciones innecesarias de desbordamiento o subdesbordamiento, ahorrando así costos de Gas.
En la imagen de abajo, sujeto a la condición i
Además, los compiladores de versiones 0.8.0 y superiores ya no requieren el uso de la biblioteca SafeMath, ya que el propio compilador tiene incorporadas funciones de protección contra desbordamientos y subdesbordamientos.
9. Optimización de modificadores
El código de los modificadores se incrusta en las funciones modificadas, y cada vez que se usa un modificador, su código se copia. Esto aumenta el tamaño del bytecode y eleva el consumo de Gas. A continuación, se presenta un método para optimizar los costos de Gas de los modificadores:
Antes de la optimización:
Optimizado:
En este caso, al reestructurar la lógica en una función interna _checkOwner(), se permite reutilizar esa función interna en el modificador, lo que puede reducir el tamaño del bytecode y disminuir los costos de Gas.
10. Optimización de cortocircuitos
Para los operadores || y &&, la evaluación lógica ocurre en corto, es decir, si la primera condición ya puede determinar el resultado de la expresión lógica, no se evalúa la segunda condición.
Para optimizar el consumo de Gas, se deben colocar las condiciones de bajo costo computacional al principio, lo que puede permitir omitir cálculos de alto costo.
Consejos generales adicionales
1. Eliminar código innecesario
Si hay funciones o variables no utilizadas en el contrato, se recomienda eliminarlas. Este es el método más directo para reducir los costos de implementación del contrato y mantener el tamaño del contrato pequeño.
Aquí hay algunos consejos prácticos:
Utilizar los algoritmos más eficientes para los cálculos. Si se utilizan directamente los resultados de ciertos cálculos en el contrato, entonces se deben eliminar esos procesos de cálculo redundantes. En esencia, cualquier cálculo no utilizado debe ser eliminado.
En Ethereum, los desarrolladores pueden obtener recompensas de Gas liberando espacio de almacenamiento. Si ya no se necesita una variable, se debe usar la palabra clave delete para eliminarla o establecerla en su valor predeterminado.
Optimización de bucles: evitar operaciones de bucle de alto costo, combinar bucles siempre que sea posible, y mover cálculos repetidos fuera del cuerpo del bucle.
2. Usar contratos precompilados
Los contratos precompilados ofrecen funciones de biblioteca complejas, como operaciones de cifrado y hash. Dado que el código no se ejecuta en la EVM, sino en nodos cliente locales, se requiere menos Gas. Usar contratos precompilados puede ahorrar Gas al reducir la carga de trabajo computacional necesaria para ejecutar contratos inteligentes.
Ejemplos de contratos precompilados incluyen el algoritmo de firma digital de curva elíptica (ECDSA) y el algoritmo de hash SHA2-256. Al usar estos contratos precompilados en contratos inteligentes, los desarrolladores pueden reducir los costos de Gas y mejorar la eficiencia de las aplicaciones.
Para una lista completa de contratos precompilados admitidos por la red de Ethereum, consulte aquí[4].
3. Usar código de ensambladura en línea
La ensambladura en línea (in-line assembly) permite a los desarrolladores escribir código de bajo nivel pero eficiente que puede ser ejecutado directamente por la EVM, sin necesidad de usar costosos códigos de operación de Solidity. La ensambladura en línea también permite un control más preciso sobre el uso de memoria y almacenamiento, lo que puede reducir aún más las tarifas de Gas. Además, la ensambladura en línea puede realizar algunas operaciones complejas que son difíciles de implementar solo con Solidity, brindando más flexibilidad para optimizar el consumo de Gas.
A continuación se muestra un ejemplo de código que ahorra Gas utilizando ensambladura en línea:
Como se puede ver en el gráfico anterior, en comparación con el caso estándar, el segundo caso que utiliza la técnica de ensambladura en línea tiene una mayor eficiencia de Gas.
Sin embargo, el uso de ensambladura en línea también puede conllevar riesgos y ser propenso a errores. Por lo tanto, se debe usar con precaución y limitarse a desarrolladores con experiencia.
4. Utilizar soluciones de Capa 2
Usar soluciones de Capa 2 puede reducir la cantidad de datos que necesitan ser almacenados y calculados en la cadena principal de Ethereum.
Soluciones de Capa 2 como rollups, cadenas laterales y canales de estado pueden descargar el procesamiento de transacciones de la cadena principal de Ethereum, permitiendo transacciones más rápidas y baratas.
Al agrupar un gran número de transacciones, estas soluciones reducen la cantidad de transacciones en la cadena, lo que disminuye las tarifas de Gas. Usar soluciones de Capa 2 también puede mejorar la escalabilidad de Ethereum, permitiendo que más usuarios y aplicaciones participen en la red sin causar congestión por sobrecarga.
5. Usar herramientas y bibliotecas de optimización
Hay múltiples herramientas de optimización disponibles, como el optimizador solc, el optimizador de construcción de Truffle y el compilador Solidity de Remix.
Estas herramientas pueden ayudar a minimizar el tamaño del bytecode, eliminar código innecesario y reducir el número de operaciones necesarias para ejecutar contratos inteligentes. Combinadas con otras bibliotecas de optimización de Gas, como 'solmate', los desarrolladores pueden reducir efectivamente los costos de Gas y mejorar la eficiencia de los contratos inteligentes.
Conclusión
Optimizar el consumo de Gas es un paso importante para los desarrolladores, ya que puede minimizar los costos de transacción y mejorar la eficiencia de los contratos inteligentes en redes compatibles con EVM. Al priorizar la ejecución de operaciones que ahorran costos, reducir el uso de almacenamiento, utilizar ensambladura en línea y seguir otras mejores prácticas discutidas en este documento, los desarrolladores pueden reducir eficazmente el consumo de Gas de los contratos.
Sin embargo, es importante tener en cuenta que durante el proceso de optimización, los desarrolladores deben actuar con cautela para evitar introducir vulnerabilidades de seguridad. Al optimizar el código y reducir el consumo de Gas, nunca se debe sacrificar la seguridad inherente de los contratos inteligentes.
[1] :https://ethereum.org/en/developers/docs/gas/
[2] :https://ethereum.github.io/yellowpaper/paper.pdf
[3] :https://www.evm.codes/
[4] :https://www.evm.codes/precompiled