Vai al contenuto principale

Ridimensionare i contratti per combattere i limiti di dimensioni

Soliditycontratto intelligentearchiviazionetruffle
Intermedio
Markus Waas
soliditydeveloper.com(opens in a new tab)
26 giugno 2020
6 minuti letti minute read

Perché c'è un limite?

Il 22 Novembre 2016(opens in a new tab), la diramazione permanente Spurious Dragon ha introdotto EIP-170(opens in a new tab), che ha aggiunto un limite di dimensioni per gli smart contract di 24.576 kb. Per gli sviluppatori in Solidity, significa che quando si aggiungono più funzionalità al contratto, a un certo punto si raggiunge il limite e, in fase di implementazione, si vedrà l'errore:

Attenzione: La dimensione del codice del contratto eccede i 24576 byte (un limite introdotto in Spurious Dragon). Questo contratto potrebbe non esser distribuibile sulla Mainnet. Considera di abilitare l'ottimizzatore (con un valore di "esecuzioni" basso!), disattivare le stringhe di ripristino o usare le librerie.

Questo limite è stato introdotto per prevenire gli attacchi DOS (denial-of-service). Qualsiasi chiamata a un contratto è relativamente economica in termini di gas. Tuttavia, l'impatto della chiamata di un contratto per i nodi di Ethereum aumenta sproporzionatamente in base alla dimensione del codice del contratto chiamato (lettura del codice dal disco, pre-elaborazione del codice, aggiunta di dati alla prova di Merkle). Ogni volta che ti trovi in una situazione in cui il malintenzionato richiede poche risorse per causare molto lavoro per altri, esiste il potenziale di attacchi DOS.

In origine, questo era un problema minore, dato che il limite naturale di dimensioni del contratto è il limite di gas del blocco. Ovviamente, un contratto dev'esser distribuito entro una transazione che detenga tutto il codice del byte del contratto. Se includi solo quella transazione in un blocco, puoi usare anche tutto il gas, ma non è infinito. Dall'Aggiornamento di Londra, il limite di gas del blocco è stato capace di variare tra le 15M e le 30M unità, a seconda della domanda di rete.

Affrontare la lotta

Sfortunatamente, non esiste un modo facile per ottenere la dimensione del bytecode dei tuoi contratti. Un ottimo strumento per aiutarti è il plugin truffle-contract-size(opens in a new tab), se utilizzi Truffle.

  1. npm install truffle-contract-size
  2. Aggiungi il plugin al the truffle-config.js: plugins: ["truffle-contract-size"]
  3. Esegui truffle run contract-size

Questo ti aiuterà a capire come le tue modifiche influiscono sulle dimensioni totali del contratto.

Di seguito, passeremo in rassegna alcuni metodi, ordinati in base al loro impatto potenziale. Pensiamo ad esempio alla perdita di peso: la strategia migliore per raggiungere il proprio peso target (nel nostro caso 24kb) consiste nel concentrarsi prima sui metodi a maggiore impatto. In gran parte dei casi è sufficiente adattare la propria dieta, mentre in altri serve qualcosa di più. Si può aggiungere un po' di esercizio fisico (impatto medio) o persino degli integratori (impatto ridotto).

Impatto elevato

Separa i tuoi contratti

Questo dovrebbe sempre essere l'approccio preferenziale. Come fare per separare un unico contratto in diversi contratti più piccoli? Generalmente è necessario trovare un'architettura efficace per i propri contratti. I contratti più piccoli sono sempre preferibili a livello di leggibilità del codice. Per dividere i contratti, chiediti:

  • Quali funzioni devono rimanere insieme? Ogni serie di funzioni potrebbe risultare più efficace in un contratto a sé stante.
  • Quali funzioni non richiedono la lettura dello stato del contratto o solo un sottoinsieme specifico dello stato?
  • Puoi dividere archiviazione e funzionalità?

Librerie

Un modo semplice per togliere il codice di funzionalità dall'archiviazione consiste nell'utilizzare una libreria(opens in a new tab). Evita di dichiarare le funzioni della libreria come interne, poiché verranno aggiunte al contratto(opens in a new tab) direttamente durante la compilazione. Se invece usi funzioni pubbliche, queste si troveranno nel contratto di una libreria separata. Considera using for(opens in a new tab) per rendere l'utilizzo delle librerie più pratico.

Proxy

Una strategia più avanzata è il 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 scoprire di più sui sistemi proxy. Offono maggiore funzionalità, ad es. consentono l'aggiornabilità ma aggiungono anche una notevole complessità. Non li aggiungerei solo per ridurre le dimensioni del contratto, a meno che non sia la sola opzione per qualche motivo.

Impatto medio

Rimuovi le funzioni

Questo punto dovrebbe essere ovvio. Le funzioni aumentano di un bel po' le dimensioni di un contratto.

  • Esterne: talvolta aggiungiamo molte funzioni di visualizzazione per motivi di comodità, il che va perfettamente bene finché non si supera il limite di dimensioni. A quel punto si potrebbe seriamente pensare di rimuovere tutte le funzioni tranne quelle essenziali.
  • Interne: puoi rimuovere anche le funzioni interne/private e limitarti a mettere il codice in linea, a condizione che la funzione venga chiamata solo una volta.

Evita le variabili aggiuntive

Una semplice modifica come questa:

1function get(uint id) returns (address,address) {
2 MyStruct memory myStruct = myStructs[id];
3 return (myStruct.addr1, myStruct.addr2);
4}
Copia
1function get(uint id) returns (address,address) {
2 return (myStructs[id].addr1, myStructs[id].addr2);
3}
Copia

crea una differenza di 0,28kb. È facile incontrare numerose situazioni simili nei propri contratti, con il rischio che si sommino fino a raggiungere volumi significativi.

Abbrevia i messaggi d'errore

I messaggi di ripristino lunghi e, in particolare, la presenza di numerosi messaggi diversi possono fare espandere il contratto. Invece, è meglio usare brevi codici d'errore e decodificali nel contratto. In questo modo è possibile rendere brevi i messaggi più lunghi:

1require(msg.sender == owner, "Only the owner of this contract can call this function");
2
Copia
1require(msg.sender == owner, "OW1");
Copia

Utilizza gli errori personalizzati invece dei messaggi d'errore

Gli errori personalizzati sono stati introdotti in Solidity 0.8.4(opens in a new tab). Sono ottimi modi per ridurre le dimensioni dei tuoi contratti, poiché sono codificati in ABI come selettori (proprio come le funzioni).

1error Unauthorized();
2
3if (msg.sender != owner) {
4 revert Unauthorized();
5}
Copia

Considera un valore d'esecuzione basso nell'ottimizzatore

Puoi anche cambiare le impostazioni dell'ottimizzatore. Il valore predefinito di 200 significa che sta provando a ottimizzare il bytecode come se una funzione fosse chiamata 200 volte. Se lo modifichi a 1, fondamentalmente dici all'ottimizzatore di ottimizzare nel caso dell'esecuzione di ogni funzione una sola volta. Una funzione ottimizzata per essere eseguita solo una volta è ottimizzata per la distribuzione stessa. Sappi che ciò aumenta i costi del gas per l'esecuzione delle funzioni, quindi meglio non farlo.

Piccolo impatto

Evita il passaggio di struct alle funzioni

Se utilizzi l'ABIEncoderV2(opens in a new tab), può essere utile evitare il passaggio di struct a una funzione. Anziché passare il parametro come struct...

1function get(uint id) returns (address,address) {
2 return _get(myStruct);
3}
4
5function _get(MyStruct memory myStruct) private view returns(address,address) {
6 return (myStruct.addr1, myStruct.addr2);
7}
Copia
1function get(uint id) returns(address,address) {
2 return _get(myStructs[id].addr1, myStructs[id].addr2);
3}
4
5function _get(address addr1, address addr2) private view returns(address,address) {
6 return (addr1, addr2);
7}
Copia

... passa direttamente i parametri richiesti. In questo esempio abbiamo risparmiato altri 0,1kb.

Dichiara la visibilità corretta per funzioni e variabili

  • Funzioni o variabili chiamate solo dall'esterno? Dichiarale come external anziché public.
  • Funzioni o variabili chiamate dall'interno del contratto? Dichiarale come private o internal anziché public.

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() {}
2
3function doSomething() checkStuff {}
Copia
1function checkStuff() private {}
2
3function doSomething() { checkStuff(); }
Copia

Questi suggerimenti dovrebbero aiutarti a ridurre significativamente le dimensioni del contratto. Ancora una volta, non smetterò mai di insistere sull'importanza di concentrarsi sempre sulla divisione dei contratti, se possibile, per avere il massimo impatto.

Ultima modifica: @Herbie_23(opens in a new tab), 15 novembre 2023

Questo tutorial è stato utile?