rounded

Escrito por: Certik

Las tarifas de Gas en la red principal de Ethereum han sido un problema persistente, especialmente notorio durante la congestión de la red. Durante los picos, los usuarios a menudo deben pagar tarifas de transacción extremadamente altas. Por lo tanto, la optimización de tarifas de Gas durante la fase de desarrollo de contratos inteligentes es especialmente importante. Optimizar el consumo de Gas no solo puede reducir eficazmente los costos de transacción, sino también mejorar la eficiencia de las transacciones, proporcionando a los usuarios una experiencia de uso de blockchain más económica y eficiente.

Este artículo 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, así como las mejores prácticas para la optimización de tarifas de Gas al desarrollar contratos inteligentes. Esperamos que a través de este contenido, podamos inspirar y ayudar a los desarrolladores, además de ayudar a los usuarios comunes a comprender mejor cómo funcionan las tarifas de Gas en la EVM, enfrentando juntos los desafíos en el ecosistema blockchain.

Introducción al mecanismo de tarifas de Gas de la EVM

En redes compatibles con EVM, 'Gas' es la unidad utilizada para medir la capacidad de cómputo requerida para ejecutar una operación específica.

La imagen a continuación ilustra la disposición estructural de la EVM. En la imagen, el consumo de Gas se divide en tres partes: ejecución de operaciones, llamadas a mensajes externos y lectura/escritura de memoria y almacenamiento.

Fuente: Sitio web oficial de Ethereum [1]

Dado que cada transacción requiere recursos computacionales para su ejecución, se cobrará una tarifa para prevenir bucles infinitos y ataques de denegación de servicio (DoS). El costo asociado con completar una transacción se denomina 'tarifa de Gas'.

Desde que entró en vigor EIP-1559 (la bifurcación dura de Londres), las tarifas de Gas se calculan mediante la siguiente fórmula:

Tarifa de Gas = unidades de gas utilizadas * (tarifa base + tarifa de prioridad)

La tarifa base se destruirá, mientras que la tarifa de prioridad se utilizará como incentivo para alentar a los validadores a agregar transacciones a la blockchain. 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 se paga a los validadores.

1. Comprender la optimización de Gas en la EVM

Cuando se compila un contrato inteligente con Solidity, el contrato se convierte en una serie de 'códigos de operación', es decir, opcodes.

Cualquier fragmento de código de operación (como crear contratos, realizar llamadas de mensajes, acceder al almacenamiento de cuentas y realizar operaciones en la máquina virtual) tiene un costo de consumo de Gas reconocido, que se documenta en el libro amarillo de Ethereum [2].

Después de múltiples modificaciones de EIP, algunos de los costos de Gas de los códigos de operación han sido ajustados, y pueden diferir del libro amarillo. Para obtener detalles sobre los costos más recientes de los códigos de operación, consulte aquí [3].

2. Conceptos básicos de la optimización de Gas

La idea central de la optimización de Gas es priorizar operaciones de bajo costo en la cadena EVM y evitar operaciones costosas en Gas.

En la EVM, las siguientes operaciones son de bajo costo:

  • Leer y escribir variables de memoria

  • Leer constantes e inmutables

  • Leer y escribir variables locales

  • Leer variables calldata, como arrays y structs de calldata

  • Llamadas a funciones internas

Las operaciones de alto costo incluyen:

  • Leer y escribir variables de estado almacenadas en el almacenamiento del contrato

  • Llamadas a funciones externas

  • Operaciones de bucle

Mejores prácticas para la optimización de tarifas de Gas en la EVM

Basado en los conceptos básicos mencionados, hemos recopilado una lista de mejores prácticas para la optimización de tarifas de Gas para la comunidad de desarrolladores. Al seguir 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 del 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 mayor 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, incluso en las mejores condiciones, requieren al menos 100 unidades.

Las formas de limitar el uso de almacenamiento incluyen:

  • Almacenar datos no permanentes en memoria

  • Reducir la cantidad de modificaciones de almacenamiento: al mantener resultados intermedios en memoria y luego asignar los resultados a las variables de almacenamiento después de que se completen todos los cálculos.

2. Empaquetado de variables

El número de slots de almacenamiento utilizados en un contrato inteligente y la forma en que los desarrolladores representan los datos influirán en gran medida en el consumo de tarifas de Gas.

El compilador de Solidity empaquetará las variables de almacenamiento continuas durante el proceso de compilación, utilizando slots de almacenamiento de 32 bytes como unidad básica para el almacenamiento de variables. El empaquetado de variables se refiere a organizar las variables de tal manera que múltiples variables puedan caber en un solo slot de almacenamiento.

A la izquierda hay una forma de implementación menos eficiente que consumirá 3 slots de almacenamiento; a la derecha hay una forma de implementación más eficiente.

A través de este ajuste de detalle, los desarrolladores pueden ahorrar 20,000 unidades de Gas (almacenar un slot de almacenamiento no utilizado requiere consumir 20,000 Gas), pero ahora solo se requieren dos slots de almacenamiento.

Dado que cada slot de almacenamiento consume Gas, el empaquetado de variables optimiza el uso de Gas al reducir el número de slots de almacenamiento necesarios.

3. Optimizar tipos de datos

Una variable puede representarse con múltiples tipos de datos, pero los costos de operación correspondientes a diferentes tipos de datos también varían. Elegir el tipo de dato adecuado ayuda a optimizar el uso de Gas.

Por ejemplo, en Solidity, los enteros pueden dividirse 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, y esta conversión consumirá Gas adicional.

Podemos comparar el costo de Gas de uint8 y uint256 en el código de 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 de forma aislada, usar uint256 es más barato que usar uint8. Sin embargo, si se utiliza la optimización de empaquetado de variables que sugerimos anteriormente, la situación cambia. Si los desarrolladores pueden empaquetar cuatro variables uint8 en un solo slot de almacenamiento, el costo total de iterar sobre ellas será menor que el de cuatro variables uint256. De esta manera, el contrato inteligente puede leer y escribir un solo slot de almacenamiento y poner cuatro variables uint8 en la 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 variables de tamaño variable. Si la longitud de los bytes puede limitarse, elija la longitud mínima de bytes que vaya de bytes1 a bytes32.

5. Mappings vs. Arrays

Las listas de datos de Solidity se pueden representar mediante dos tipos de datos: arrays y mappings, pero su sintaxis y estructura son completamente diferentes.

Los mappings son más eficientes y tienen un costo menor en la mayoría de los casos, pero los arrays son iterables y admiten empaquetado de tipos de datos. Por lo tanto, se recomienda priorizar el uso de mappings 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 memory

Las variables declaradas en los parámetros de función pueden almacenarse en calldata o memory. La principal diferencia entre ambas es que memory puede ser modificada por la función, mientras que calldata es inmutable.

Recuerde este principio: si los parámetros de la función son de solo lectura, se debe priorizar el uso de calldata en lugar de memory. Esto puede evitar operaciones de copia innecesarias desde el calldata de la función a la memoria.

Ejemplo 1: usar memory

Al usar la palabra clave memory, los valores de los arrays se copian desde el calldata codificado a la memoria durante el proceso de decodificación de 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 de memoria intermedias. Esta forma de optimización reduce el costo de ejecución a solo 2,413 unidades de Gas, mejorando la eficiencia de Gas en un 35%.

7. Usar la palabra 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, su costo de acceso es mucho menor que el de almacenamiento, y se recomienda usarlas siempre que sea posible.

8. Usar Unchecked cuando se esté seguro de que no habrá desbordamiento/subdesbordamiento

Cuando los desarrolladores pueden asegurarse de que las operaciones aritméticas no causarán desbordamiento o subdesbordamiento, pueden usar la palabra clave unchecked introducida en Solidity v0.8.0 para evitar comprobaciones innecesarias de desbordamiento o subdesbordamiento, ahorrando así costos de Gas.

En la imagen a continuación, bajo la restricción condicional i<length, la variable i nunca puede desbordarse. Aquí, length se define como uint256, lo que significa que el valor máximo de i es max(uint)-1. Por lo tanto, incrementar i en el bloque de código sin verificar se considera seguro y ahorra más Gas.

Además, los compiladores de versiones 0.8.0 y superiores ya no necesitan usar la biblioteca SafeMath, ya que el propio compilador ha incorporado funciones de protección contra desbordamiento y subdesbordamiento.

9. Optimizar modificadores

El código del modificador se incrusta en la función modificada y, cada vez que se usa el 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 un modificador:


Antes de la optimización:

Optimizado:

En este caso, al reestructurar la lógica en la 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 el costo de Gas.

10. Optimización de evaluación corta

Para los operadores || y &&, la evaluación lógica se lleva a cabo mediante evaluación corta, lo que significa que si la primera condición ya puede determinar el resultado de la expresión lógica, no se evaluará la segunda condición.

Para optimizar el consumo de Gas, se deben poner las condiciones de bajo costo al principio, lo que puede permitir omitir cálculos costosos.

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 el costo de despliegue del contrato y mantener el tamaño del contrato pequeño.

A continuación, se presentan algunos consejos prácticos:

Usar algoritmos de mayor eficiencia para realizar cálculos. Si se utilizan directamente los resultados de ciertos cálculos en el contrato, es recomendable eliminar esos procesos de cálculo redundantes. Esencialmente, cualquier cálculo no utilizado debería eliminarse.

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 por defecto.

Optimización de bucles: evitar operaciones de bucle costosas, combinar bucles siempre que sea posible y mover cálculos redundantes fuera del cuerpo del bucle.

2. Usar contratos precompilados

Los contratos precompilados ofrecen funciones de biblioteca complejas, como operaciones criptográficas y de hash. Dado que el código no se ejecuta en la EVM, sino que se ejecuta localmente en los nodos del cliente, se requiere menos Gas. Usar contratos precompilados puede ahorrar Gas al reducir la carga 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 operativa de las aplicaciones.

Para obtener una lista completa de los contratos precompilados admitidos por la red Ethereum, consulte aquí [4].

3. Usar código de ensamblador en línea

El ensamblador en línea permite a los desarrolladores escribir código de bajo nivel pero eficiente que puede ser ejecutado directamente por la EVM, sin necesidad de utilizar códigos de operación de Solidity costosos. El ensamblador en línea también permite un control más preciso sobre el uso de memoria y almacenamiento, lo que reduce aún más las tarifas de Gas. Además, el ensamblador en línea puede realizar algunas operaciones complejas que son difíciles de lograr solo con Solidity, proporcionando más flexibilidad para optimizar el consumo de Gas.

A continuación se muestra un ejemplo de código que ahorra Gas utilizando ensamblador en línea:

Como se puede ver en la imagen anterior, en comparación con el caso de uso estándar, el segundo caso de uso que utiliza la técnica de ensamblador en línea tiene una mayor eficiencia de Gas.

Sin embargo, el uso de ensamblador en línea también puede conllevar riesgos y ser propenso a errores. Por lo tanto, debe utilizarse con precaución, reservado para desarrolladores con experiencia.

4. Usar soluciones de Capa 2

Usar soluciones de Capa 2 puede reducir la cantidad de datos que necesitan almacenarse y calcularse en la red principal de Ethereum.

Las soluciones de Capa 2, como rollups, cadenas laterales y canales de estado, pueden descargar el procesamiento de transacciones de la cadena principal de Ethereum, lo que permite transacciones más rápidas y baratas.

Al agrupar una gran cantidad de transacciones, estas soluciones reducen el número 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 que la congestión causada por la sobrecarga de la red.

5. Usar herramientas y bibliotecas de optimización

Hay varias 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 requeridas para ejecutar contratos inteligentes. Combinadas con otras bibliotecas de optimización de Gas, como 'solmate', los desarrolladores pueden reducir eficazmente 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 minimiza los costos de transacción y mejora la eficiencia de los contratos inteligentes en redes compatibles con EVM. Al priorizar la ejecución de operaciones de bajo costo, reducir el uso de almacenamiento, utilizar ensamblador en línea y seguir otras mejores prácticas discutidas en este artículo, los desarrolladores pueden reducir eficazmente el consumo de Gas de sus contratos.

Sin embargo, es importante tener en cuenta que durante el proceso de optimización, los desarrolladores deben proceder con precaución para no introducir vulnerabilidades de seguridad. Durante la optimización del código y la reducción del 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