Scris de: Certik
Taxele de gaz pe rețeaua principală Ethereum au fost o problemă majoră, mai ales în timpul congestiei rețelei. În perioadele de vârf, utilizatorii trebuie adesea să plătească taxe de tranzacție extrem de mari. Prin urmare, optimizarea costurilor de gaz în etapa de dezvoltare a contractelor inteligente este deosebit de importantă. Optimizarea consumului de gaz nu doar că poate reduce eficient costurile de tranzacție, dar poate îmbunătăți și eficiența tranzacțiilor, oferind utilizatorilor o experiență de utilizare a blockchain-ului mai economică și mai eficientă.
Acest articol va rezuma mecanismul taxelor de gaz din Ethereum Virtual Machine (EVM), conceptele de bază legate de optimizarea gazului și cele mai bune practici pentru optimizarea costurilor de gaz în dezvoltarea contractelor inteligente. Sperăm că prin aceste informații, putem oferi dezvoltatorilor inspirație și ajutor practic, în timp ce ajutăm utilizatorii obișnuiți să înțeleagă mai bine modul în care funcționează taxele de gaz EVM, colaborând pentru a face față provocărilor din ecosistemul blockchain.
Prezentare generală a mecanismului de taxe de gaz din EVM
În rețelele compatibile EVM, „Gaz” se referă la unitatea utilizată pentru a măsura puterea de calcul necesară pentru a executa o operațiune specifică.
Diagrama de mai jos ilustrează structura EVM. În diagramă, consumul de gaz este împărțit în trei părți: execuția operațiunilor, apelurile de mesaje externe, precum și citirile și scrierile din memorie și stocare.
Sursă: site-ul oficial Ethereum[1]
Deoarece executarea fiecărei tranzacții necesită resurse de calcul, se percepe o anumită taxă pentru a preveni buclele infinite și atacurile de tip Denial of Service (DoS). Costul necesar pentru a finaliza o tranzacție se numește „Taxa de gaz”.
De când EIP-1559 (hard fork-ul Londrei) a intrat în vigoare, taxa de gaz este calculată folosind următoarea formulă:
Taxa de gaz = unități de gaz utilizate * (taxa de bază + taxa de prioritate)
Taxa de bază va fi distrusă, iar taxa de prioritate va acționa ca stimulent, încurajând validatorii să adauge tranzacția în blockchain. Setarea unei taxe de prioritate mai mari la trimiterea unei tranzacții poate crește probabilitatea ca tranzacția să fie inclusă în următorul bloc. Acest lucru este similar cu o „bătaie” pe care utilizatorii o plătesc validatorilor.
1. Înțelegeți optimizarea gazului în EVM
Când compilați contracte inteligente folosind Solidity, contractul este transformat într-o serie de „opcode-uri”, adică operațiuni.
Fiecare segment de opcode (de exemplu, crearea de contracte, efectuarea apelurilor de mesaje, accesarea stocării conturilor și executarea operațiunilor pe mașina virtuală) are un cost de consum de gaz recunoscut, iar aceste costuri sunt documentate în cartea galbenă Ethereum[2].
După mai multe modificări ale EIP-urilor, costul de gaz pentru unele opcode-uri a fost ajustat și poate varia față de cartea galbenă. Pentru detalii despre costurile actuale ale opcode-urilor, consultați aici[3].
2. Conceptele fundamentale ale optimizării gazului
Conceptul de bază al optimizării gazului este de a prioritiza operațiile cu costuri scăzute pe blockchain-ul EVM, evitând operațiile cu costuri mari de gaz.
În EVM, următoarele operații au costuri mai mici:
Citiri și scrieri ale variabilelor de memorie
Citiri ale variabilelor constante și imutabile
Citiri și scrieri ale variabilelor locale
Citiri ale variabilelor calldata, cum ar fi array-urile și structurile calldata
Apeluri de funcții interne
Operațiile cu costuri ridicate includ:
Citiri și scrieri ale variabilelor de stare stocate în contract
Apeluri de funcții externe
Operațiuni repetitive
Cele mai bune practici pentru optimizarea costurilor de gaz în EVM
Pe baza conceptelor fundamentale de mai sus, am compilat o listă de cele mai bune practici pentru optimizarea costurilor de gaz pentru comunitatea dezvoltatorilor. Prin respectarea acestor practici, dezvoltatorii pot reduce consumul de taxe de gaz al contractelor inteligente, reduce costurile de tranzacție și crea aplicații mai eficiente și mai prietenoase cu utilizatorul.
1. Reduceți utilizarea stocării
În Solidity, stocarea este o resursă limitată, iar consumul său de gaz este mult mai mare decât cel al memoriei. De fiecare dată când un contract inteligent citește sau scrie date din stocare, generează costuri mari de gaz.
Conform definiției din cartea galbenă Ethereum, costul operațiunilor de stocare este de peste 100 de ori mai mare decât cel al operațiunilor de memorie. De exemplu, instrucțiunile OPcodes mload și mstore consumă doar 3 unități de gaz, în timp ce operațiunile de stocare cum ar fi sload și sstore, chiar și în cele mai ideale condiții, costul este de cel puțin 100 de unități.
Metodele de limitare a utilizării stocării includ:
Stocați datele temporare în memorie
Reduceți numărul de modificări ale stocării: prin păstrarea rezultatelor intermediare în memorie, după finalizarea tuturor calculelor, alocați rezultatul variabilei de stocare.
2. Împachetarea variabilelor
Numărul de slots de stocare utilizate în contractele inteligente și modul în care dezvoltatorii declară datele vor afecta semnificativ consumul de taxe de gaz.
Compilatorul Solidity va împacheta variabilele de stocare consecutive în timpul compilării, folosind un slot de stocare de 32 de biți ca unitate de bază pentru stocarea variabilelor. Împachetarea variabilelor se referă la aranjarea rațională a variabilelor astfel încât mai multe variabile să poată fi adaptate într-un singur slot de stocare.
Partea stângă este o implementare mai puțin eficientă, care va consuma 3 sloturi de stocare; partea dreaptă este o implementare mai eficientă.
Prin această ajustare detaliată, dezvoltatorii pot economisi 20.000 de unități de gaz (stocarea unui slot de stocare neutilizat consumă 20.000 de gaz), dar acum este necesar doar un slot de stocare.
Deoarece fiecare slot de stocare consumă gaz, împachetarea variabilelor optimizează utilizarea gazului prin reducerea numărului de sloturi de stocare necesare.
3. Optimizarea tipurilor de date
O variabilă poate fi reprezentată prin mai multe tipuri de date, dar costurile de operație pentru diferitele tipuri de date sunt diferite. Alegerea tipului de date potrivit ajută la optimizarea utilizării gazului.
De exemplu, în Solidity, numerele întregi pot fi împărțite în dimensiuni diferite: uint8, uint16, uint32 etc. Deoarece EVM execută operații pe unități de 256 de biți, utilizarea uint8 înseamnă că EVM trebuie mai întâi să-l convertească în uint256, iar această conversie va consuma gaz suplimentar.
Putem compara costurile de gaz consumate de uint8 și uint256 în codul din diagramă. Funcția UseUint() consumă 120.382 de unități de gaz, în timp ce funcția UseUInt8() consumă 166.111 de unități de gaz.
Privind separat, utilizarea uint256 este mai ieftină decât uint8. Cu toate acestea, dacă utilizăm optimizarea de împachetare a variabilelor pe care am sugerat-o anterior, situația se schimbă. Dacă dezvoltatorul poate împacheta patru variabile uint8 într-un singur slot de stocare, atunci costul total de iterație va fi mai mic decât costul celor patru variabile uint256. Astfel, contractul inteligent poate citi și scrie un singur slot de stocare, plasând cele patru variabile uint8 în memorie/stocare într-o singură operație.
4. Folosiți variabile de dimensiune fixă în loc de variabile dinamice
Dacă datele pot fi menținute în 32 de biți, se recomandă utilizarea tipului de date bytes32 în loc de bytes sau strings. În general, variabilele de dimensiune fixă consumă mai puțin gaz decât cele de dimensiune variabilă. Dacă lungimea byte-ului poate fi restricționată, încercați să alegeți cea mai mică lungime de la bytes1 la bytes32.
5. Mapări versus array-uri
Listele de date din Solidity pot fi reprezentate prin două tipuri de date: array-uri (Arrays) și mapări (Mappings), dar sintaxa și structura acestora sunt complet diferite.
Mapările sunt mai eficiente și mai ieftine în cele mai multe cazuri, dar array-urile sunt iterabile și suportă împachetarea tipurilor de date. Prin urmare, se recomandă utilizarea mapărilor atunci când gestionați liste de date, cu excepția cazului în care este necesară iterarea sau dacă consumul de gaz poate fi optimizat prin împachetarea tipurilor de date.
6. Folosiți calldata în loc de memory
Variabilele declarate în parametrii funcției pot fi stocate în calldata sau memory. Principala diferență între cele două este că memory poate fi modificat de funcție, în timp ce calldata este imutabil.
Rețineți acest principiu: dacă parametrii funcției sunt doar pentru citire, ar trebui să folosiți prioritar calldata în loc de memory. Astfel se pot evita operațiile de copiere inutile de la calldata la memory.
Exemplu 1: Folosiți memory
Când utilizați cuvântul cheie memory, valorile array-ului sunt copiate din calldata codificat în memorie în timpul decodării ABI. Costul de execuție al acestui bloc de cod este de 3.694 de unități de gaz.
Exemplu 2: Folosiți calldata
Când citiți valori direct din calldata, săriți peste operațiile intermediare de memorie. Această metodă de optimizare reduce costul de execuție la doar 2.413 unități de gaz, iar eficiența gazului a crescut cu 35%.
7. Folosiți cuvântul cheie Constant/Immutable ori de câte ori este posibil
Variabilele constante/imunizate nu sunt stocate în stocarea contractului. Aceste variabile sunt calculate la compilare și stocate în bytecode-ul contractului. Prin urmare, costul de accesare a acestora este mult mai mic în comparație cu stocarea, recomandându-se utilizarea cuvintelor cheie Constant sau Immutable ori de câte ori este posibil.
8. Folosiți Unchecked atunci când sunteți siguri că nu vor exista depășiri / subdepășiri
Când dezvoltatorii pot determina că operațiile aritmetice nu vor duce la depășiri sau subdepășiri, pot folosi cuvântul cheie unchecked introdus în Solidity v0.8.0 pentru a evita verificările suplimentare de depășire sau subdepășire, economisind astfel costul de gaz.
În diagrama de mai jos, sub restricția i<length, variabila i nu va putea depăși niciodată. Aici, length este definit ca uint256, ceea ce înseamnă că valoarea maximă a lui i este max(uint)-1. Prin urmare, incrementarea lui i în blocul de cod necontrolat este considerată sigură și mai economisitoare în gaz.
În plus, compilatoarele de versiuni 0.8.0 și mai recente nu mai necesită utilizarea bibliotecii SafeMath, deoarece compilatorul în sine are deja protecții împotriva depășirilor și subdepășirilor.
9. Optimizarea modificatorilor
Codul modificatorului este încorporat în funcțiile modificate, iar de fiecare dată când modificatorul este utilizat, codul său este copiat. Aceasta va crește dimensiunea bytecode-ului și va crește consumul de gaz. Iată o metodă de optimizare a costurilor de gaz ale modificatorului:
Optimizat anterior:
Optimizat:
În acest exemplu, prin restructurarea logicii într-o funcție internă _checkOwner(), care permite reutilizarea acestei funcții interne în modificator, se poate reduce dimensiunea bytecode-ului și costurile de gaz.
10. Optimizarea scurtcircuitării
Pentru operatorii || și &&, evaluarea logică se va realiza prin scurtcircuitare, adică dacă prima condiție poate deja determina rezultatul expresiei logice, a doua condiție nu va fi evaluată.
Pentru a optimiza consumul de gaz, ar trebui să plasați condițiile cu costuri reduse în față, astfel încât să puteți sări peste calculele costisitoare.
Sugestii generale suplimentare
1. Eliminați codul inutil
Dacă în contract există funcții sau variabile neutilizate, se recomandă ștergerea acestora. Aceasta este cea mai directă metodă de reducere a costurilor de desfășurare a contractului și de menținere a dimensiunii contractului mici.
Iată câteva sugestii utile:
Folosiți cele mai eficiente algoritmi pentru calcule. Dacă contractul utilizează direct rezultatele unor calcule, atunci aceste procese redundante de calcul ar trebui eliminate. În esență, orice calcul neutilizat ar trebui șters.
În Ethereum, dezvoltatorii pot obține recompense de gaz prin eliberarea spațiului de stocare. Dacă o variabilă nu mai este necesară, ar trebui folosit cuvântul cheie delete pentru a o șterge sau să fie setată la valoarea implicită.
Optimizarea buclelor: evitați operațiile de buclă costisitoare, combinați buclele cât mai mult posibil și mutați calculele repetate în afara corpului buclei.
2. Folosiți contracte precompilate
Contractele precompilate oferă funcții de bibliotecă complexe, cum ar fi operațiile criptografice și de hash. Deoarece codul nu rulează pe EVM, ci local pe nodurile clientului, este necesar mai puțin gaz. Utilizarea contractelor precompilate poate economisi gaz prin reducerea sarcinii de calcul necesare pentru executarea contractelor inteligente.
Exemple de contracte precompilate includ algoritmul de semnătură digitală cu curbe eliptice (ECDSA) și algoritmul de hash SHA2-256. Prin utilizarea acestor contracte precompilate în contractele inteligente, dezvoltatorii pot reduce costurile de gaz și îmbunătăți eficiența aplicațiilor.
Pentru o listă completă a contractelor precompilate suportate de rețeaua Ethereum, consultați aici[4].
3. Folosiți cod de asamblare în linie
Asamblarea în linie (in-line assembly) permite dezvoltatorilor să scrie cod de nivel scăzut, dar eficient, care poate fi executat direct de EVM, fără a utiliza opcode-uri costisitoare în Solidity. Asamblarea în linie permite, de asemenea, un control mai precis asupra utilizării memoriei și stocării, reducând astfel costurile de gaz. În plus, asamblarea în linie poate efectua unele operații complexe care sunt greu de realizat folosind doar Solidity, oferind mai multă flexibilitate în optimizarea consumului de gaz.
Iată un exemplu de cod care economisește gaz folosind asamblare în linie:
Din diagrama de mai sus se poate observa că, comparativ cu cazul standard, a doua utilizare a tehnicii de asamblare în linie are o eficiență mai mare a gazului.
Cu toate acestea, utilizarea asamblării în linie poate aduce, de asemenea, riscuri și poate fi ușor de greșit. Prin urmare, ar trebui să fie utilizată cu precauție, limitată la dezvoltatorii cu experiență.
4. Folosiți soluții Layer 2
Utilizarea soluțiilor Layer 2 poate reduce cantitatea de date care trebuie stocate și calculate pe rețeaua principală Ethereum.
Soluțiile Layer 2, cum ar fi rollup-urile, sidechain-urile și canalele de stare, pot descărca procesarea tranzacțiilor de pe lanțul principal Ethereum, realizând astfel tranzacții mai rapide și mai ieftine.
Prin gruparea unui număr mare de tranzacții, aceste soluții reduc numărul de tranzacții pe lanț, scăzând astfel taxele de gaz. Utilizarea soluțiilor Layer 2 poate, de asemenea, să îmbunătățească scalabilitatea Ethereum, permițând mai multor utilizatori și aplicații să participe la rețea fără a provoca supraaglomerarea rețelei.
5. Folosiți instrumente și biblioteci de optimizare
Există mai multe instrumente de optimizare disponibile, cum ar fi optimizer-ul solc, optimizer-ul de construcție Truffle și compilatorul Solidity Remix.
Aceste instrumente pot ajuta la minimizarea dimensiunii bytecode-ului, la eliminarea codului inutil și la reducerea numărului de operații necesare pentru a executa contractele inteligente. Combinate cu alte biblioteci de optimizare a gazului, cum ar fi „solmate”, dezvoltatorii pot reduce eficient costul gazului și pot îmbunătăți eficiența contractelor inteligente.
Concluzie
Optimizarea consumului de gaz este un pas important pentru dezvoltatori, deoarece poate minimiza costurile de tranzacție și poate îmbunătăți eficiența contractelor inteligente pe rețelele compatibile EVM. Prin prioritizarea operațiunilor cu costuri reduse, reducerea utilizării stocării, utilizarea asamblării în linie și respectarea altor cele mai bune practici discutate în acest articol, dezvoltatorii pot reduce semnificativ consumul de gaz al contractelor.
Cu toate acestea, trebuie menționat că, în procesul de optimizare, dezvoltatorii trebuie să fie precauți pentru a nu introduce vulnerabilități de securitate. În timpul optimizării codului și reducerii consumului de gaz, nu ar trebui niciodată să sacrifici securitatea inerentă a contractelor inteligente.
[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