As taxas de Gas da mainnet Ethereum sempre foram um grande desafio, especialmente durante períodos de congestionamento da rede. Em horários de pico, os usuários frequentemente precisam pagar taxas de transação extremamente altas. Portanto, otimizar os custos de Gas durante a fase de desenvolvimento de contratos inteligentes é especialmente importante. A otimização do consumo de Gas não apenas reduz efetivamente os custos de transação, mas também melhora a eficiência das transações, proporcionando aos usuários uma experiência de uso de blockchain mais econômica e eficiente.
Este artigo abordará o mecanismo de taxas de Gas da máquina virtual Ethereum (EVM), os conceitos principais relacionados à otimização de taxas de Gas e as melhores práticas para otimizar taxas de Gas ao desenvolver contratos inteligentes. Esperamos que, através deste conteúdo, possamos fornecer inspiração e ajuda prática aos desenvolvedores, e ao mesmo tempo ajudar os usuários comuns a entender melhor como as taxas de Gas da EVM funcionam, enfrentando juntos os desafios no ecossistema blockchain.
Introdução ao mecanismo de taxas de Gas da EVM
Em redes compatíveis com EVM, 'Gas' refere-se à unidade usada para medir a capacidade computacional necessária para executar operações específicas.
A imagem abaixo ilustra o layout estrutural da EVM. No gráfico, o consumo de Gas é dividido em três partes: execução de operações, chamadas de mensagens externas e leitura/escrita em memória e armazenamento.
Fonte: Site oficial do Ethereum[1]
Como cada transação requer recursos computacionais para sua execução, uma taxa é cobrada para evitar loops infinitos e ataques de negação de serviço (DoS). A taxa necessária para concluir uma transação é chamada de 'taxa de Gas'.
Desde a entrada em vigor do EIP-1559 (hard fork Londres), as taxas de Gas são calculadas pela seguinte fórmula:
Taxa de Gas = unidades de Gas utilizadas * (taxa base + taxa de prioridade)
A taxa base é destruída, enquanto a taxa de prioridade é usada como incentivo para encorajar os validadores a adicionar transações ao blockchain. Ao definir uma taxa de prioridade mais alta ao enviar uma transação, aumenta-se a probabilidade de que a transação seja incluída no próximo bloco. Isso é semelhante a uma 'gorjeta' que o usuário paga ao validador.
1. Compreender a otimização de Gas na EVM
Quando um contrato inteligente é compilado com Solidity, ele é convertido em uma série de 'opcodes'.
Cada opcode (como a criação de contratos, execução de chamadas de mensagens, acesso ao armazenamento de contas e execução de operações na máquina virtual) tem um custo de consumo de Gas reconhecido, que é registrado no livro amarelo do Ethereum[2].
Após várias modificações do EIP, o custo de Gas de algumas operações foi ajustado, podendo divergir do que está no livro amarelo. Para obter detalhes sobre os custos mais recentes das operações, consulte aqui[3].
2. Conceitos básicos da otimização de Gas
O conceito central da otimização de Gas é priorizar operações de custo eficiente na blockchain EVM, evitando operações que custam caro em Gas.
Na EVM, as seguintes operações têm custo reduzido:
Ler e gravar variáveis de memória
Ler variáveis constantes e imutáveis
Ler e gravar variáveis locais
Ler variáveis de calldata, como arrays e structs de calldata
Chamadas de funções internas
Operações de custo mais elevado incluem:
Ler e gravar variáveis de estado armazenadas no armazenamento do contrato
Chamadas de funções externas
Operações de loop
Melhores práticas para otimização de taxas de Gas na EVM
Com base nos conceitos básicos acima, compilamos uma lista de melhores práticas para otimização de taxas de Gas para a comunidade de desenvolvedores. Ao seguir essas práticas, os desenvolvedores podem reduzir o consumo de taxas de Gas de seus contratos inteligentes, diminuindo os custos de transação e criando aplicativos mais eficientes e amigáveis ao usuário.
1. Tente minimizar o uso de armazenamento
No Solidity, o Storage (armazenamento) é um recurso limitado, cujo consumo de Gas é muito maior que o da Memory (memória). Cada vez que um contrato inteligente lê ou grava dados do armazenamento, gera altos custos de Gas.
De acordo com a definição do livro amarelo do Ethereum, o custo das operações de armazenamento é mais de 100 vezes maior que o das operações de memória. Por exemplo, os comandos OPcodesmload e mstore consomem apenas 3 unidades de Gas, enquanto operações de armazenamento como sload e sstore, mesmo nas melhores condições, custam pelo menos 100 unidades.
Métodos para limitar o uso de armazenamento incluem:
Armazene dados não permanentes na memória
Reduza o número de modificações de armazenamento: ao manter resultados intermediários na memória, aguarde até que todos os cálculos estejam concluídos antes de atribuir o resultado às variáveis de armazenamento.
2. Empacotamento de variáveis
O número de slots de armazenamento usados em contratos inteligentes e a maneira como os desenvolvedores representam os dados têm um grande impacto no consumo de taxas de Gas.
O compilador Solidity agrupa variáveis de armazenamento consecutivas durante o processo de compilação, usando um slot de armazenamento de 32 bytes como a unidade básica de armazenamento das variáveis. O empacotamento de variáveis refere-se à organização adequada das variáveis para que várias delas possam caber em um único slot de armazenamento.
À esquerda está uma implementação menos eficiente, que consumirá 3 slots de armazenamento; à direita está uma implementação mais eficiente.
Com esse ajuste de detalhe, os desenvolvedores podem economizar 20.000 unidades de Gas (armazenar um slot de armazenamento não utilizado consome 20.000 Gas), mas agora apenas dois slots de armazenamento são necessários.
Como cada slot de armazenamento consome Gas, o empacotamento de variáveis otimiza o uso de Gas ao reduzir a quantidade de slots de armazenamento necessários.
3. Otimize tipos de dados
Uma variável pode ser representada por vários tipos de dados, mas o custo das operações correspondentes a diferentes tipos de dados também varia. Escolher o tipo de dado apropriado ajuda a otimizar o uso de Gas.
Por exemplo, em Solidity, os inteiros podem ser subdivididos em diferentes tamanhos: uint8, uint16, uint32, etc. Como a EVM executa operações em unidades de 256 bits, usar uint8 significa que a EVM deve primeiro convertê-lo em uint256, e essa conversão consome Gas adicional.
Podemos comparar os custos de Gas de uint8 e uint256 no código da imagem. A função UseUint() consome 120.382 unidades de Gas, enquanto a função UseUInt8() consome 166.111 unidades de Gas.
Vistos isoladamente, aqui o uso de uint256 é mais barato do que uint8. No entanto, se utilizarmos a otimização de empacotamento de variáveis sugerida anteriormente, a situação muda. Se os desenvolvedores puderem empacotar quatro variáveis uint8 em um slot de armazenamento, o custo total de iterar sobre elas será menor do que o de quatro variáveis uint256. Assim, o contrato inteligente pode ler e gravar um slot de armazenamento de uma só vez e colocar quatro variáveis uint8 em memória/armazenamento em uma única operação.
4. Use variáveis de tamanho fixo em vez de variáveis dinâmicas
Se os dados puderem ser mantidos dentro de 32 bytes, recomenda-se usar o tipo de dados bytes32 em vez de bytes ou strings. De modo geral, variáveis de tamanho fixo consomem menos Gas do que variáveis de tamanho variável. Se o comprimento em bytes puder ser limitado, escolha o menor comprimento possível, de bytes1 a bytes32.
5. Mapeamentos versus arrays
A lista de dados da Solidity pode ser representada por dois tipos de dados: arrays e mapeamentos, mas sua sintaxe e estrutura são completamente diferentes.
Os mapeamentos são geralmente mais eficientes e custam menos na maioria dos casos, mas os arrays têm a vantagem de serem iteráveis e suportarem empacotamento de tipos de dados. Portanto, recomenda-se usar mapeamentos ao gerenciar listas de dados, a menos que a iteração seja necessária ou a otimização de consumo de Gas através do empacotamento de tipos de dados seja viável.
6. Use calldata em vez de memory
As variáveis declaradas nos parâmetros da função podem ser armazenadas em calldata ou memory. A principal diferença entre os dois é que memory pode ser modificado pela função, enquanto calldata é imutável.
Lembre-se deste princípio: se os parâmetros da função são somente leitura, deve-se priorizar o uso de calldata em vez de memory. Isso evita operações desnecessárias de cópia de calldata para memory na função.
Exemplo 1: Usando memory
Ao usar a palavra-chave memory, os valores de um array são copiados do calldata codificado para a memória durante o processo de decodificação ABI. O custo de execução desse bloco de código é de 3.694 unidades de Gas.
Exemplo 2: Usando calldata
Ao ler valores diretamente de calldata, evite operações de memory intermediárias. Essa otimização reduz o custo de execução para apenas 2.413 unidades de Gas, melhorando a eficiência de Gas em 35%.
7. Sempre que possível, use as palavras-chave Constant/Immutable
Variáveis Constant/Immutable não são armazenadas no armazenamento do contrato. Essas variáveis são calculadas em tempo de compilação e armazenadas no bytecode do contrato. Portanto, seu custo de acesso é muito menor do que o do armazenamento, e recomenda-se usar as palavras-chave Constant ou Immutable sempre que possível.
8. Use Unchecked quando garantir que não ocorrerá estouro/subfluxo
Quando os desenvolvedores podem garantir que operações aritméticas não resultarão em estouro ou subfluxo, o uso da palavra-chave unchecked introduzida na Solidity v0.8.0 pode evitar verificações desnecessárias de estouro ou subfluxo, economizando assim custos de Gas.
Na imagem abaixo, sob a restrição de condição i
Além disso, compiladores da versão 0.8.0 e superior não precisam mais usar a biblioteca SafeMath, pois o compilador já incorporou proteções contra estouro e subfluxo.
9. Otimização de modificadores
O código do modificador é incorporado nas funções modificadas, e toda vez que o modificador é usado, seu código é copiado. Isso aumenta o tamanho do bytecode e eleva o consumo de Gas. Aqui está uma maneira de otimizar os custos de Gas do modificador:
Antes da otimização:
Otimizado:
Neste exemplo, ao reformular a lógica para a função interna _checkOwner(), permite-se reutilizar essa função interna no modificador, reduzindo o tamanho do bytecode e diminuindo os custos de Gas.
10. Otimização de curto-circuito
Para os operadores || e &&, a avaliação lógica ocorre com curto-circuito, ou seja, se a primeira condição já puder determinar o resultado da expressão lógica, a segunda condição não será avaliada.
Para otimizar o consumo de Gas, as condições de baixo custo de computação devem ser colocadas primeiro, possibilitando assim pular cálculos de alto custo.
Sugestões gerais adicionais
1. Remova código desnecessário
Se houver funções ou variáveis não utilizadas no contrato, recomenda-se removê-las. Esta é a maneira mais direta de reduzir os custos de implantação do contrato e manter o tamanho do contrato pequeno.
Aqui estão algumas sugestões úteis:
Utilize os algoritmos mais eficientes para cálculos. Se certos resultados de cálculos são usados diretamente no contrato, esses cálculos redundantes devem ser eliminados. Essencialmente, qualquer cálculo não utilizado deve ser removido.
No Ethereum, os desenvolvedores podem receber recompensas em Gas ao liberar espaço de armazenamento. Se uma variável não for mais necessária, deve-se usar a palavra-chave delete para removê-la ou defini-la como um valor padrão.
Otimização de loops: evite operações de loop de alto custo, combine loops sempre que possível e mova cálculos duplicados para fora do corpo do loop.
2. Utilize contratos pré-compilados
Contratos pré-compilados oferecem funções de biblioteca complexas, como operações de criptografia e hashing. Como o código não é executado na EVM, mas sim localmente nos nós clientes, menos Gas é necessário. O uso de contratos pré-compilados pode economizar Gas ao reduzir a carga de trabalho computacional necessária para executar contratos inteligentes.
Exemplos de contratos pré-compilados incluem o algoritmo de assinatura digital de curva elíptica (ECDSA) e o algoritmo de hash SHA2-256. Ao usar esses contratos pré-compilados em contratos inteligentes, os desenvolvedores podem reduzir os custos de Gas e melhorar a eficiência da aplicação.
Para uma lista completa dos contratos pré-compilados suportados pela rede Ethereum, consulte aqui[4].
3. Use código de assembly inline
Assembly inline permite que os desenvolvedores escrevam código de baixo nível, mas eficiente, que pode ser executado diretamente pela EVM, sem a necessidade de usar os caros códigos de operação Solidity. O assembly inline também permite um controle mais preciso sobre o uso de memória e armazenamento, reduzindo ainda mais as taxas de Gas. Além disso, o assembly inline pode realizar operações complexas que são difíceis de implementar apenas com Solidity, proporcionando mais flexibilidade para otimizar o consumo de Gas.
Aqui está um exemplo de código que economiza Gas usando assembly inline:
A partir da imagem acima, pode-se ver que, em comparação ao caso padrão, o segundo caso que utiliza a técnica de assembly inline possui uma eficiência de Gas superior.
No entanto, o uso de assembly inline também pode trazer riscos e ser propenso a erros. Portanto, deve ser usado com cautela, restrito a desenvolvedores experientes.
4. Use soluções Layer 2
Usar soluções Layer 2 pode reduzir a quantidade de dados que precisam ser armazenados e calculados na mainnet Ethereum.
Soluções Layer 2, como rollups, sidechains e canais de estado, podem descarregar o processamento de transações da cadeia principal do Ethereum, resultando em transações mais rápidas e baratas.
Ao agrupar um grande número de transações, essas soluções reduzem a quantidade de transações on-chain, diminuindo assim as taxas de Gas. Usar soluções Layer 2 também pode aumentar a escalabilidade do Ethereum, permitindo que mais usuários e aplicativos participem da rede sem causar congestionamentos devido à sobrecarga da rede.
5. Utilize ferramentas e bibliotecas de otimização
Existem várias ferramentas de otimização disponíveis, como o otimizador solc, o otimizador de construção do Truffle e o compilador Solidity do Remix.
Essas ferramentas podem ajudar a minimizar o tamanho do bytecode, remover código inútil e reduzir o número de operações necessárias para executar contratos inteligentes. Combinando com outras bibliotecas de otimização de Gas, como 'solmate', os desenvolvedores podem efetivamente reduzir os custos de Gas e melhorar a eficiência dos contratos inteligentes.
Conclusão
Otimizar o consumo de Gas é um passo importante para os desenvolvedores, pois minimiza os custos de transação e aumenta a eficiência dos contratos inteligentes em redes compatíveis com EVM. Ao priorizar a execução de operações que economizam custos, reduzir o uso de armazenamento, utilizar assembly inline e seguir outras melhores práticas discutidas neste artigo, os desenvolvedores podem efetivamente reduzir o consumo de Gas de seus contratos.
No entanto, deve-se ter cuidado, pois durante o processo de otimização, os desenvolvedores devem agir com cautela para evitar a introdução de vulnerabilidades de segurança. Nunca deve-se sacrificar a segurança inerente do contrato inteligente ao otimizar o código e reduzir o consumo de Gas.
[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