Ridurre le dimensioni dei contratti per combattere il limite di dimensione del contratto
Perché c'è un limite?
Il 22 novembre 2016 (opens in a new tab) la biforcazione hard Spurious Dragon ha introdotto l'EIP-170 (opens in a new tab) che ha aggiunto un limite di dimensione del contratto intelligente di 24,576 kb. Per te come sviluppatore Solidity questo significa che quando aggiungi sempre più funzionalità al tuo contratto, a un certo punto raggiungerai il limite e durante la distribuzione vedrai l'errore:
Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on Mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries.
Questo limite è stato introdotto per prevenire attacchi denial-of-service (DOS). Qualsiasi chiamata a un contratto è relativamente economica in termini di gas. Tuttavia, l'impatto di una chiamata al contratto per i nodi di Ethereum aumenta in modo sproporzionato a seconda delle dimensioni del codice del contratto chiamato (lettura del codice dal disco, pre-elaborazione del codice, aggiunta di dati alla prova di Merkle). Ogni volta che si verifica una situazione in cui l'attaccante richiede poche risorse per causare molto lavoro agli altri, si ottiene il potenziale per attacchi DOS.
In origine questo era un problema minore perché un limite naturale alle dimensioni del contratto è il limite del gas del blocco. Ovviamente, un contratto deve essere distribuito all'interno di una transazione che contiene tutto il bytecode del contratto. Se includi solo quella transazione in un blocco, puoi consumare tutto quel gas, ma non è infinito. A partire dall'Aggiornamento di Londra, il limite del gas del blocco ha potuto variare tra 15M e 30M di unità a seconda della domanda della rete.
Di seguito esamineremo alcuni metodi ordinati in base al loro potenziale impatto. Pensaci in termini di perdita di peso. La strategia migliore per raggiungere il proprio peso forma (nel nostro caso 24kb) è concentrarsi prima sui metodi a grande impatto. Nella maggior parte dei casi, basta sistemare la dieta per arrivarci, ma a volte serve un po' di più. Quindi potresti aggiungere un po' di esercizio fisico (impatto medio) o persino degli integratori (impatto basso).
Grande impatto
Separa i tuoi contratti
Questo dovrebbe sempre essere il tuo primo approccio. Come puoi separare il contratto in più contratti più piccoli? In genere ti costringe a ideare una buona architettura per i tuoi contratti. I contratti più piccoli sono sempre preferiti dal punto di vista della leggibilità del codice. Per dividere i contratti, chiediti:
- Quali funzioni appartengono allo stesso gruppo? Ogni insieme di funzioni potrebbe stare meglio nel proprio contratto.
- Quali funzioni non richiedono la lettura dello stato del contratto o solo di un sottoinsieme specifico dello stato?
- Puoi separare l'archiviazione e le funzionalità?
Librerie
Un modo semplice per allontanare il codice delle funzionalità dall'archiviazione è utilizzare una libreria (opens in a new tab). Non dichiarare le funzioni della libreria come interne, poiché queste verranno aggiunte al contratto (opens in a new tab) direttamente durante la compilazione. Ma se usi funzioni pubbliche, allora queste saranno di fatto in un contratto di libreria separato. Considera l'utilizzo di using for (opens in a new tab) per rendere più conveniente l'uso delle librerie.
Proxy
Una strategia più avanzata sarebbe un sistema proxy. Le librerie usano DELEGATECALL in background, che esegue semplicemente la funzione di un altro contratto con lo stato del contratto chiamante. Dai un'occhiata a questo post del blog (opens in a new tab) per saperne di più sui sistemi proxy. Ti offrono maggiori funzionalità, ad esempio consentono l'aggiornabilità, ma aggiungono anche molta complessità. Non li aggiungerei solo per ridurre le dimensioni del contratto, a meno che non sia la tua unica opzione per qualsiasi motivo.
Impatto medio
Rimuovi funzioni
Questo dovrebbe essere ovvio. Le funzioni aumentano un bel po' le dimensioni di un contratto.
- Esterne: Spesso aggiungiamo molte funzioni di visualizzazione (view) per motivi di comodità. Va benissimo finché non si raggiunge il limite di dimensione. A quel punto potresti voler pensare seriamente di rimuovere tutte quelle non assolutamente essenziali.
- Interne: Puoi anche rimuovere le funzioni interne/private e semplicemente inserire il codice in linea, a patto che la funzione venga chiamata solo una volta.
Evita variabili aggiuntive
1function get(uint id) returns (address,address) {2 MyStruct memory myStruct = myStructs[id];3 return (myStruct.addr1, myStruct.addr2);4}1function get(uint id) returns (address,address) {2 return (myStructs[id].addr1, myStructs[id].addr2);3}Un semplice cambiamento come questo fa una differenza di 0,28kb. È probabile che tu possa trovare molte situazioni simili nei tuoi contratti e queste possono davvero sommarsi fino a raggiungere quantità significative.
Accorcia i messaggi di errore
I messaggi di revert lunghi e in particolare molti messaggi di revert diversi possono gonfiare il contratto. Usa invece codici di errore brevi e decodificali nel tuo contratto. Un messaggio lungo potrebbe diventare molto più corto:
1require(msg.sender == owner, "Only the owner of this contract can call this function");1require(msg.sender == owner, "OW1");Usa errori personalizzati invece di messaggi di errore
Gli errori personalizzati sono stati introdotti in Solidity 0.8.4 (opens in a new tab). Sono un ottimo modo per ridurre le dimensioni dei tuoi contratti, perché sono codificati in ABI come selettori (proprio come lo sono le funzioni).
1error Unauthorized();23if (msg.sender != owner) {4 revert Unauthorized();5}Considera un valore di esecuzione basso nell'ottimizzatore
Puoi anche modificare le impostazioni dell'ottimizzatore. Il valore predefinito di 200 significa che sta cercando di ottimizzare il bytecode come se una funzione venisse chiamata 200 volte. Se lo modifichi a 1, in pratica dici all'ottimizzatore di ottimizzare per il caso in cui ogni funzione venga eseguita solo una volta. Una funzione ottimizzata per essere eseguita una sola volta significa che è ottimizzata per la distribuzione stessa. Tieni presente che questo aumenta i costi del gas per l'esecuzione delle funzioni, quindi potresti non volerlo fare.
Impatto basso
Evita di passare struct alle funzioni
Se stai utilizzando l'ABIEncoderV2 (opens in a new tab), può essere utile non passare struct a una funzione. Invece di passare il parametro come struct, passa direttamente i parametri richiesti. In questo esempio abbiamo risparmiato un altro 0,1kb.
1function get(uint id) returns (address,address) {2 return _get(myStruct);3}45function _get(MyStruct memory myStruct) private view returns(address,address) {6 return (myStruct.addr1, myStruct.addr2);7}1function get(uint id) returns(address,address) {2 return _get(myStructs[id].addr1, myStructs[id].addr2);3}45function _get(address addr1, address addr2) private view returns(address,address) {6 return (addr1, addr2);7}Dichiara la visibilità corretta per funzioni e variabili
- Funzioni o variabili che vengono chiamate solo dall'esterno? Dichiarale come
externalinvece dipublic. - Funzioni o variabili chiamate solo dall'interno del contratto? Dichiarale come
privateointernalinvece dipublic.
Rimuovi i modificatori
I modificatori, specialmente se usati intensamente, potrebbero avere un impatto significativo sulle dimensioni del contratto. Considera di rimuoverli e di usare invece le funzioni.
1modifier checkStuff() {}23function doSomething() checkStuff {}1function checkStuff() private {}23function doSomething() { checkStuff(); }Questi suggerimenti dovrebbero aiutarti a ridurre significativamente le dimensioni del contratto. Ancora una volta, non lo sottolineerò mai abbastanza, concentrati sempre sulla divisione dei contratti, se possibile, per ottenere l'impatto maggiore.
Ultimo aggiornamento della pagina: 25 febbraio 2026