rounded

Scritto da: Certik

 

Le spese di Gas sulla rete principale di Ethereum sono sempre state un problema, specialmente durante i periodi di congestione della rete. Durante i picchi, gli utenti spesso devono pagare commissioni di transazione estremamente elevate. Pertanto, è particolarmente importante ottimizzare i costi del Gas durante la fase di sviluppo dei contratti intelligenti. Ottimizzare il consumo di Gas non solo può ridurre efficacemente i costi di transazione, ma può anche migliorare l'efficienza delle transazioni, fornendo agli utenti un'esperienza blockchain più economica ed efficiente.

 

Questo articolo darà una panoramica del meccanismo delle spese di Gas della Ethereum Virtual Machine (EVM), dei concetti chiave relativi all'ottimizzazione delle spese di Gas e delle migliori pratiche per ottimizzare le spese di Gas durante lo sviluppo di contratti intelligenti. Speriamo che attraverso questi contenuti, possiamo fornire ispirazione e aiuto pratico alla comunità degli sviluppatori, permettendo anche agli utenti comuni di comprendere meglio il funzionamento delle spese di Gas dell'EVM, affrontando insieme le sfide nell'ecosistema blockchain.

 

Introduzione al meccanismo delle spese di Gas dell'EVM

 

Nelle reti compatibili con EVM, il 'Gas' è l'unità utilizzata per misurare la potenza di calcolo necessaria per eseguire operazioni specifiche.

 

Il diagramma sottostante illustra la disposizione strutturale dell'EVM. Nel diagramma, il consumo di Gas è suddiviso in tre parti: esecuzione delle operazioni, chiamate di messaggi esterni e lettura/scrittura in memoria e storage.

 

Fonte: sito ufficiale di Ethereum [1]

 

Poiché l'esecuzione di ogni transazione richiede risorse di calcolo, viene addebitata una certa tassa per prevenire loop infiniti e attacchi di denial of service (DoS). Il costo necessario per completare una transazione è chiamato 'costo del Gas'.

 

Dall'entrata in vigore dell'EIP-1559 (hard fork di Londra), il costo del Gas è calcolato secondo la seguente formula:

 

Costo del Gas = unità di Gas utilizzate * (costo base + costo di priorità)

 

Il costo base sarà distrutto e il costo di priorità funge da incentivo, incoraggiando i validatori ad aggiungere le transazioni alla blockchain. Impostare un costo di priorità più elevato al momento dell'invio di una transazione può aumentare la probabilità che la transazione venga inclusa nel blocco successivo. Questo è simile a una 'mancia' che l'utente paga al validatore.

 

1. Comprendere l'ottimizzazione del Gas nell'EVM

 

Quando un contratto intelligente è compilato con Solidity, viene trasformato in una serie di 'codici operativi', cioè opcodes.

Ogni operazione di codice operativo (come la creazione di contratti, l'esecuzione di chiamate di messaggi, l'accesso allo storage dell'account e l'esecuzione di operazioni sulla macchina virtuale) ha un costo di consumo di Gas riconosciuto, registrato nel libro giallo di Ethereum [2].

 

 

Dopo molte modifiche all'EIP, i costi in Gas di alcuni codici operativi sono stati regolati e potrebbero differire da quanto riportato nel libro giallo. Per dettagli sui costi più recenti dei codici operativi, fare riferimento a questo link [3].

 

2. Concetti di base per l'ottimizzazione del Gas

 

Il concetto centrale dell'ottimizzazione del Gas è scegliere operazioni ad alta efficienza dei costi sulla blockchain EVM, evitando operazioni costose in Gas.

 

Nell'EVM, le seguenti operazioni hanno un costo inferiore:

  • Leggere e scrivere variabili in memoria

  • Leggere variabili costanti e immutabili

  • Leggere e scrivere variabili locali

  • Leggere variabili calldata, come array e strutture calldata

  • Chiamate di funzioni interne

 

Le operazioni costose includono:

  • Leggere e scrivere variabili di stato memorizzate nello storage del contratto

  • Chiamate di funzioni esterne

  • Operazioni cicliche

 

Migliori pratiche per l'ottimizzazione delle spese di Gas in EVM

 

Basandoci sui concetti di base sopra menzionati, abbiamo preparato un elenco di migliori pratiche per l'ottimizzazione delle spese di Gas per la comunità degli sviluppatori. Seguendo queste pratiche, gli sviluppatori possono ridurre il consumo di Gas dei contratti intelligenti, abbattere i costi di transazione e creare applicazioni più efficienti e user-friendly.

 

1. Ridurre al minimo l'uso dello storage

 

In Solidity, lo Storage è una risorsa limitata, il cui consumo di Gas è molto più alto rispetto alla Memory. Ogni volta che un contratto intelligente legge o scrive dati dallo storage, comporta costi di Gas elevati.

 

Secondo la definizione del libro giallo di Ethereum, il costo delle operazioni di storage è superiore di oltre 100 volte rispetto a quello delle operazioni di memoria. Ad esempio, i codici operativi mload e mstore richiedono solo 3 unità di Gas, mentre le operazioni di storage come sload e sstore, anche nelle migliori condizioni, richiedono almeno 100 unità.

 

Metodi per limitare l'uso dello storage includono:

 

  • Memorizzare i dati non permanenti in memoria

  • Ridurre il numero di modifiche allo storage: mantenendo i risultati intermedi in memoria fino al completamento di tutti i calcoli, per poi assegnare il risultato finale alle variabili di storage.

 

2. Packing delle variabili

 

Il numero di slot di storage utilizzati nel contratto e il modo in cui gli sviluppatori rappresentano i dati influenzeranno notevolmente il consumo di Gas.

 

Il compilatore Solidity impacchetta le variabili di storage contigue durante la compilazione e utilizza slot di storage di 32 byte come unità di base per memorizzare le variabili. Il packing delle variabili si riferisce all'organizzazione ragionevole delle variabili in modo che più variabili possano adattarsi a uno slot di storage singolo.

 

A sinistra c'è un modo di implementazione meno efficiente, che consumerà 3 slot di storage; a destra c'è un modo di implementazione più efficiente.

 

 

Con questo dettaglio, gli sviluppatori possono risparmiare 20.000 unità di Gas (memorizzare uno slot di storage non utilizzato richiede 20.000 Gas), ma ora richiede solo due slot di storage.

 

Poiché ogni slot di storage consuma Gas, il packing delle variabili ottimizza l'uso del Gas riducendo il numero di slot di storage richiesti.

 

3. Ottimizzare i tipi di dati

 

Una variabile può essere rappresentata in più tipi di dati, ma i costi operativi corrispondenti ai diversi tipi di dati variano. Scegliere il tipo di dato appropriato aiuta a ottimizzare l'uso del Gas.

 

Ad esempio, in Solidity, gli interi possono essere suddivisi in diverse dimensioni: uint8, uint16, uint32, ecc. Poiché l'EVM esegue operazioni in unità di 256 bit, l'uso di uint8 significa che l'EVM deve prima convertirlo in uint256, e questa conversione consuma ulteriore Gas.

 

 

Possiamo confrontare i costi in Gas di uint8 e uint256 nel codice mostrato. La funzione UseUint() consuma 120.382 unità di Gas, mentre la funzione UseUInt8() consuma 166.111 unità di Gas.

 

Guardando separatamente, qui l'uso di uint256 è più economico rispetto a uint8. Tuttavia, se si utilizza l'ottimizzazione del packing delle variabili che abbiamo suggerito in precedenza, la situazione cambia. Se gli sviluppatori possono impacchettare quattro variabili uint8 in uno slot di storage, il costo totale per iterarle sarà inferiore rispetto a quattro variabili uint256. Così, il contratto intelligente può leggere e scrivere una sola volta nello slot di storage e mettere quattro variabili uint8 in memoria/storage in una sola operazione.

 

4. Utilizzare variabili di dimensioni fisse al posto di variabili dinamiche

 

Se i dati possono essere controllati entro 32 byte, si consiglia di utilizzare il tipo di dato bytes32 anziché bytes o strings. In generale, le variabili di dimensioni fisse consumano meno Gas rispetto alle variabili di dimensioni variabili. Se la lunghezza in byte può essere limitata, si dovrebbe cercare di scegliere la lunghezza minima da bytes1 a bytes32.

 

 

 

5. Mappature e array

 

Le liste di dati in Solidity possono essere rappresentate in due modi: array e mappature, ma la loro sintassi e struttura sono completamente diverse.

Le mappature sono più efficienti e costano meno nella maggior parte dei casi, ma gli array sono iterabili e supportano il packing dei tipi di dati. Pertanto, si consiglia di utilizzare le mappature per gestire elenchi di dati, a meno che non sia necessaria l'iterazione o si possa ottimizzare il consumo di Gas tramite il packing dei tipi di dati.

 

6. Utilizzare calldata al posto di memory

 

Le variabili dichiarate nei parametri della funzione possono essere memorizzate in calldata o memory. La principale differenza tra i due è che la memory può essere modificata dalla funzione, mentre il calldata è immutabile.

 

Ricorda questo principio: se i parametri della funzione sono di sola lettura, si dovrebbe preferire l'uso di calldata piuttosto che memory. Questo può evitare operazioni di copia non necessarie da calldata della funzione a memory.

 

Esempio 1: Utilizzare memory

 

 

Quando si utilizza la parola chiave memory, i valori degli array vengono copiati dal calldata codificato alla memoria durante il processo di decodifica ABI. Il costo di esecuzione di questo blocco di codice è di 3.694 unità di Gas.

 

Esempio 2: Utilizzare calldata

 

 

Quando si leggono valori direttamente da calldata, saltare le operazioni di memoria intermedie. Questo metodo di ottimizzazione riduce il costo di esecuzione a sole 2.413 unità di Gas, migliorando l'efficienza del Gas del 35%.

 

7. Utilizzare le parole chiave Constant/Immutable quando possibile

 

Le variabili Costanti/Immutabili non vengono memorizzate nello storage del contratto. Queste variabili vengono calcolate durante la compilazione e memorizzate nel bytecode del contratto. Pertanto, il loro costo di accesso è molto più basso rispetto allo storage, e si consiglia di utilizzare le parole chiave Constant o Immutable quando possibile.

 

8. Usare Unchecked quando si è certi che non ci saranno overflow/underflow

 

Quando gli sviluppatori possono determinare che le operazioni aritmetiche non causeranno overflow o underflow, possono utilizzare la parola chiave unchecked introdotta in Solidity v0.8.0 per evitare controlli superflui di overflow o underflow, risparmiando così costi in Gas.

 

Nel diagramma sottostante, a causa del vincolo condizionale i<length, la variabile i non potrà mai overfloware. Qui, length è definito come uint256, il che significa che il valore massimo di i è max(uint) - 1. Pertanto, incrementare i in un blocco di codice non controllato è considerato sicuro e consuma meno Gas.

 

 

Inoltre, i compilatori dalla versione 0.8.0 in poi non richiedono più l'uso della libreria SafeMath, poiché il compilatore stesso ha integrato funzionalità di protezione da overflow e underflow.

 

9. Ottimizzazione dei modificatori

 

Il codice del modificatore viene incorporato nelle funzioni modificate e ogni volta che viene utilizzato un modificatore, il suo codice viene copiato. Questo aumenta la dimensione del bytecode e aumenta il consumo di Gas. Ecco un modo per ottimizzare il costo del Gas del modificatore:

 


Ottimizzato prima:

 

Ottimizzato:

 

In questo esempio, ristrutturando la logica in una funzione interna _checkOwner(), che consente di riutilizzare questa funzione interna all'interno del modificatore, è possibile ridurre la dimensione del bytecode e abbattere i costi in Gas.

 

10. Ottimizzazione delle valutazioni a corto

 

Per gli operatori || e &&, l'operazione logica subisce una valutazione short-circuit, cioè se la prima condizione può già determinare il risultato dell'espressione logica, la seconda condizione non verrà valutata.

 

Per ottimizzare il consumo di Gas, le condizioni a basso costo dovrebbero essere poste per prime, in modo da poter saltare i calcoli costosi.

 

Suggerimenti generali aggiuntivi

 

1. Rimuovere codice non utilizzato

 

Se ci sono funzioni o variabili non utilizzate nel contratto, si consiglia di eliminarle. Questo è il modo più diretto per ridurre i costi di distribuzione del contratto e mantenere le dimensioni del contratto contenute.

Ecco alcuni suggerimenti pratici:

 

Utilizzare algoritmi più efficienti per i calcoli. Se alcuni risultati di calcolo vengono utilizzati direttamente nel contratto, è opportuno rimuovere questi processi di calcolo ridondanti. In sostanza, qualsiasi calcolo non utilizzato dovrebbe essere eliminato.

 

In Ethereum, gli sviluppatori possono guadagnare ricompense in Gas liberando spazio di storage. Se una variabile non è più necessaria, dovrebbe essere eliminata usando la parola chiave delete o impostata su un valore predefinito.

 

Ottimizzazione dei cicli: evitare operazioni cicliche ad alto costo, unire i cicli quando possibile e spostare i calcoli ripetuti al di fuori del corpo del ciclo.

 

2. Utilizzare contratti precompilati

 

I contratti precompilati forniscono funzioni di libreria complesse, come operazioni crittografiche e di hashing. Poiché il codice non viene eseguito sulla EVM, ma viene eseguito localmente sui nodi client, richiede meno Gas. Utilizzando contratti precompilati è possibile risparmiare Gas riducendo il carico computazionale necessario per eseguire i contratti intelligenti.

 

Esempi di contratti precompilati includono l'algoritmo di firma digitale a curva ellittica (ECDSA) e l'algoritmo di hashing SHA2-256. Utilizzando questi contratti precompilati nei contratti intelligenti, gli sviluppatori possono ridurre i costi in Gas e migliorare l'efficienza operativa delle applicazioni.

 

Per l'elenco completo dei contratti precompilati supportati dalla rete Ethereum, fare riferimento a questo link [4].

 

3. Utilizzare codice assembly inline

 

L'assemblaggio inline consente agli sviluppatori di scrivere codice a basso livello ma altamente efficiente, eseguibile direttamente dalla EVM, senza dover utilizzare costosi codici operativi Solidity. L'assemblaggio inline consente anche un controllo più preciso sull'uso della memoria e dello storage, riducendo ulteriormente il costo del Gas. Inoltre, l'assemblaggio inline può eseguire alcune operazioni complesse che sarebbero difficili da realizzare solo con Solidity, offrendo maggiore flessibilità per ottimizzare il consumo di Gas.

 

Ecco un esempio di codice che risparmia Gas utilizzando l'assemblaggio inline:

 

 

Dall'immagine sopra, è evidente che, rispetto ai casi d'uso standard, il secondo caso d'uso che utilizza la tecnologia di assemblaggio inline ha una maggiore efficienza in Gas.

 

Tuttavia, l'uso di assemblaggio inline può comportare anche rischi e può facilmente portare a errori. Pertanto, dovrebbe essere utilizzato con cautela e limitato a sviluppatori esperti.

 

4. Utilizzare soluzioni Layer 2

 

Utilizzare soluzioni Layer 2 può ridurre la quantità di dati che devono essere memorizzati e calcolati sulla rete principale di Ethereum.

 

Soluzioni Layer 2 come rollups, sidechain e canali di stato possono scaricare l'elaborazione delle transazioni dalla catena principale di Ethereum, consentendo transazioni più rapide e meno costose.

 

Riducendo il numero di transazioni on-chain aggregando molte transazioni, queste soluzioni riducono le spese per il Gas. L'uso di soluzioni Layer 2 può anche migliorare la scalabilità di Ethereum, consentendo a più utenti e applicazioni di partecipare alla rete, senza sovraccaricare la rete e causare congestione.

 

5. Utilizzare strumenti e librerie di ottimizzazione

 

Ci sono diversi strumenti di ottimizzazione disponibili, come l'ottimizzatore solc, l'ottimizzatore di build di Truffle e il compilatore Solidity di Remix.

 

 

Questi strumenti possono aiutare a ridurre al minimo le dimensioni del bytecode, rimuovere codice non utilizzato e ridurre il numero di operazioni necessarie per eseguire i contratti intelligenti. Combinando con altre librerie di ottimizzazione del Gas, come 'solmate', gli sviluppatori possono ridurre efficacemente i costi del Gas e migliorare l'efficienza dei contratti intelligenti.

 

Conclusione

 

Ottimizzare il consumo di Gas è un passo importante per gli sviluppatori, poiché può ridurre al minimo i costi di transazione e migliorare l'efficienza dei contratti intelligenti sulle reti compatibili con EVM. Prioritizzando l'esecuzione di operazioni a basso costo, riducendo l'uso dello storage, sfruttando l'assemblaggio inline e seguendo altre migliori pratiche discusse in questo articolo, gli sviluppatori possono ridurre efficacemente il consumo di Gas del contratto.

 

Tuttavia, è importante notare che, durante il processo di ottimizzazione, gli sviluppatori devono procedere con cautela per evitare di introdurre vulnerabilità di sicurezza. Durante l'ottimizzazione del codice e la riduzione del consumo di Gas, non si dovrebbe mai sacrificare la sicurezza intrinseca del contratto intelligente.

 

[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