rounded

Автор: Certik

Витрати на газ в основній мережі Ethereum залишаються важливою проблемою, особливо в умовах перевантаження мережі. У години пік користувачі часто вимушені платити надзвичайно високі комісії за транзакції. Тому оптимізація витрат на газ на етапі розробки смарт-контрактів є надзвичайно важливою. Оптимізація споживання газу не тільки дозволяє ефективно знижувати витрати на транзакції, але й підвищує ефективність транзакцій, забезпечуючи користувачам більш економічний та ефективний досвід використання блокчейну.

У цій статті буде охарактеризовано механізм витрат газу в Ethereum Virtual Machine (EVM), основні концепції оптимізації витрат газу та найкращі практики оптимізації витрат газу під час розробки смарт-контрактів. Сподіваємось, що ці матеріали надихнуть розробників та нададуть практичну допомогу, а також допоможуть звичайним користувачам краще зрозуміти механізми витрат газу EVM, щоб спільно вирішити виклики в екосистемі блокчейн.

Введення механізму витрат газу EVM

У мережах, сумісних з EVM, «газ» - це одиниця, що використовується для вимірювання обчислювальної потужності, необхідної для виконання конкретної операції.

Наступна діаграма ілюструє структуру EVM. У діаграмі споживання газу поділено на три частини: виконання операцій, виклики зовнішніх повідомлень, а також читання та запис пам'яті та зберігання.

Джерело: Офіційний сайт Ethereum[1]

Оскільки виконання кожної транзакції вимагає обчислювальних ресурсів, стягується певна плата, щоб запобігти безкінечним циклам та атакам відмови в обслуговуванні (DoS). Вартість, необхідна для виконання транзакції, називається «платою за газ».

З моменту набуття чинності EIP-1559 (лондонський хард-форк), плата за газ розраховується за наступною формулою:

Плата за газ = одиниці газу, що використовуються * (базова плата + пріоритетна плата)

Базова плата буде знищена, а пріоритетна плата слугуватиме заохоченням, стимулюючи валідаторів додавати транзакції до блокчейна. Встановлення вищої пріоритетної плати під час відправлення транзакції може підвищити ймовірність включення транзакції в наступний блок. Це подібно до «чайових», які користувач платить валідатору.

1. Розуміння оптимізації витрат газу в EVM

При компіляції смарт-контрактів за допомогою Solidity контракт перетворюється на серію «операційних кодів», тобто opcodes.

Кожен блок коду (наприклад, створення контракту, виконання викликів повідомлень, доступ до зберігання облікових записів та виконання операцій на віртуальній машині) має визнану вартість споживання газу, ці витрати зафіксовані в жовтій книзі Ethereum[2].

Після численних змін EIP, вартість газу деяких операційних кодів була відкоригована і може відрізнятися від вказаної в жовтій книзі. Для отримання детальної інформації про останні витрати на операційні коди, будь ласка, зверніться сюди[3].

2. Основні концепції оптимізації витрат газу

Основна концепція оптимізації газу полягає в пріоритетному виборі затратних операцій на блокчейні EVM, уникнення дорогих операцій з газом.

В EVM наступні операції є менш затратними:

  • Читання та запис змінних пам'яті

  • Читання констант та незмінних змінних

  • Читання та запис локальних змінних

  • Читання змінних calldata, таких як масиви calldata та структури

  • Виклики внутрішніх функцій

Дорогі операції включають:

  • Читання та запис стану змінних, що зберігаються в контракті

  • Виклики зовнішніх функцій

  • Циклічні операції

Найкращі практики оптимізації витрат газу в EVM

На основі вищевказаних базових концепцій, ми підготували для спільноти розробників список найкращих практик оптимізації витрат газу. Дотримуючись цих практик, розробники можуть знизити споживання газу смарт-контрактів, зменшити витрати на транзакції та створити більш ефективні та зручні для користувачів програми.

1. Зменшуйте використання зберігання

У Solidity зберігання є обмеженим ресурсом, витрати газу на яке значно перевищують витрати газу на пам'ять. Кожного разу, коли смарт-контракт читає або записує дані зі зберігання, виникають високі витрати на газ.

Згідно з визначенням жовтої книги Ethereum, вартість операцій зберігання в 100 разів перевищує вартість операцій з пам'яттю. Наприклад, інструкції OPcodes mload та mstore споживають лише 3 одиниці газу, тоді як операції зберігання, такі як sload та sstore, навіть в найкращих умовах, коштують не менше 100 одиниць.

Методи обмеження використання зберігання включають:

  • Зберігайте непостійні дані в пам'яті

  • Зменшуйте кількість змін зберігання: зберігайте проміжні результати в пам'яті, а після завершення всіх обчислень розподіліть результати змінним зберігання.

2. Упаковка змінних

Кількість слотів зберігання, що використовуються в смарт-контракті, а також спосіб, яким розробники представляють дані, значно вплине на витрати газу.

Компілятор Solidity під час компіляції упакує послідовні змінні зберігання і використовує 32 байти слота зберігання як базову одиницю для зберігання змінних. Упаковка змінних - це процес, що дозволяє розмістити кілька змінних в одному слоті зберігання.

Зліва представлено менш ефективний спосіб реалізації, який споживатиме 3 слоти зберігання; справа - більш ефективний спосіб реалізації.

Завдяки цьому коригуванню розробник може заощадити 20,000 одиниць газу (зберігання невикористаного слота зберігання потребує 20,000 газу), але тепер потрібно лише два слоти зберігання.

Оскільки кожен слот зберігання споживає газ, упаковка змінних оптимізує використання газу, зменшуючи кількість необхідних слотів зберігання.

3. Оптимізація типів даних

Змінна може бути представлена кількома типами даних, але витрати на операції різних типів даних також різні. Вибір відповідного типу даних допомагає оптимізувати використання газу.

Наприклад, в Solidity цілі числа можуть бути розділені на різні розміри: uint8, uint16, uint32 тощо. Оскільки EVM виконує операції по 256 біт, використання uint8 означає, що EVM спершу повинна перетворити його на uint256, що додатково споживає газ.

Ми можемо порівняти витрати газу для uint8 та uint256 на прикладі коду на малюнку. Функція UseUint() споживає 120,382 одиниць газу, тоді як функція UseUInt8() споживає 166,111 одиниць газу.

Окремо, використання uint256 є дешевшим, ніж uint8. Проте, якщо використати раніше запропоновану оптимізацію упаковки змінних, то ситуація змінюється. Якщо розробник може упакувати чотири змінні типу uint8 в один слот зберігання, то загальна вартість ітерації їх буде нижчою, ніж чотирьох змінних типу uint256. Таким чином, смарт-контракт може читати та записувати один слот зберігання та в рамках одного операційного процесу помістити чотири змінні типу uint8 в пам'ять/зберігання.

4. Використовуйте змінні фіксованого розміру замість динамічних змінних

Якщо дані можна утримувати в межах 32 байтів, слід використовувати тип даних bytes32 замість bytes або strings. Як правило, змінні фіксованого розміру споживають менше газу, ніж змінні змінного розміру. Якщо довжину байтів можна обмежити, вибирайте найменшу довжину від bytes1 до bytes32.

5. Мапи і масиви

Списки даних Solidity можуть бути представлені двома типами даних: масивами (Arrays) та мапами (Mappings), але їх синтаксис і структура кардинально відрізняються.

В більшості випадків мапи є більш ефективними та менш затратними, але масиви мають можливість ітерації та підтримують упаковку типів даних. Тому рекомендується надавати перевагу використанню мап при управлінні списком даних, якщо не потрібна ітерація або не можна оптимізувати споживання газу за допомогою упаковки типів даних.

6. Використовуйте calldata замість memory

Змінні, оголошені в параметрах функції, можуть зберігатися в calldata або memory. Основна різниця полягає в тому, що пам'ять може бути змінена функцією, тоді як calldata є незмінною.

Пам'ятайте цю принцип: якщо параметри функції є лише для читання, слід надавати перевагу використанню calldata замість memory. Це дозволяє уникнути непотрібних копіювань з calldata функції до пам'яті.

Приклад 1: Використання memory

При використанні ключового слова memory значення масиву копіюються з закодованого calldata до пам'яті під час декодування ABI. Вартість виконання цього блоку коду становить 3,694 одиниці газу.

Приклад 2: Використання calldata

Коли значення читаються безпосередньо з calldata, пропустіть проміжні операції в пам'яті. Цей спосіб оптимізації знижує витрати на виконання до лише 2,413 одиниць газу, підвищуючи ефективність газу на 35%.

7. Використовуйте ключові слова Constant/Immutable, коли це можливо

Змінні типу Constant/Immutable не зберігаються в пам'яті контракту. Ці змінні обчислюються під час компіляції та зберігаються в байт-коді контракту. Тому їх доступність є набагато дешевшою в порівнянні з пам'яттю, тому рекомендується використовувати ключові слова Constant або Immutable, наскільки це можливо.

8. Використовуйте Unchecked, якщо ви впевнені, що переповнень/недостатності не відбудеться

Коли розробник може впевнено стверджувати, що арифметичні операції не призведуть до переповнення або недостатності, можна використовувати ключове слово unchecked, введене в Solidity v0.8.0, щоб уникнути зайвих перевірок на переповнення або недостатність, тим самим заощаджуючи витрати на газ.

На наступній діаграмі, з обмеженням i<length, змінна i ніколи не може переповнитися. Тут length визначається як uint256, що означає, що максимальне значення i становить max(uint)-1. Тому, збільшуючи i в неконтрольованому блоці, це вважається безпечним і економить газ.

Крім того, компілятори версії 0.8.0 та вище більше не потребують використання бібліотеки SafeMath, оскільки компілятор вже включає механізми захисту від переповнення та недостатності.

9. Оптимізація модифікаторів

Код модифікатора вбудовується в змінені функції, і кожного разу, коли модифікатор використовується, його код копіюється. Це збільшує розмір байт-коду та підвищує споживання газу. Ось один із способів оптимізації витрат газу модифікатора:


До оптимізації:

Оптимізовано:

У цьому прикладі, реформуючи логіку в внутрішню функцію _checkOwner(), що дозволяє повторно використовувати цю внутрішню функцію в модифікаторі, можна зменшити розмір байт-коду та знизити витрати на газ.

10. Оптимізація короткого замикання

Для операторів || та && логічні операції проходять коротке оцінювання, тобто якщо перша умова вже може визначити результат логічного виразу, друга умова не оцінюється.

Щоб оптимізувати споживання газу, слід розміщувати умови з низькою вартістю на початку, щоб мати можливість пропустити дорогі обчислення.

Додаткові загальні рекомендації

1. Видаліть непотрібний код

Якщо в контракті є невикористані функції або змінні, рекомендується їх видалити. Це найпряміший спосіб зменшити витрати на розгортання контракту та зберегти малий обсяг контракту.

Ось кілька практичних порад:

Використовуйте найбільш ефективні алгоритми для обчислень. Якщо в контракті безпосередньо використовуються результати певних обчислень, їх слід видалити. Зокрема, будь-які непотрібні обчислення повинні бути видалені.

В Ethereum розробники можуть отримати винагороду за газ, звільняючи місце для зберігання. Якщо змінна більше не потрібна, її слід видалити за допомогою ключового слова delete або встановити на значення за замовчуванням.

Оптимізація циклів: уникайте дорогих циклічних операцій, по можливості об'єднуйте цикли та виносьте повторювані обчислення за межі циклу.

2. Використовуйте попередньо скомпільовані контракти

Попередньо скомпільовані контракти надають складні бібліотечні функції, такі як криптографічні та хешуючі операції. Оскільки код не виконується на EVM, а виконується локально на клієнтських вузлах, потрібний газ менше. Використання попередньо скомпільованих контрактів може заощадити газ, зменшуючи обчислювальне навантаження, необхідне для виконання смарт-контрактів.

Приклади попередньо скомпільованих контрактів включають алгоритм цифрового підпису еліптичних кривих (ECDSA) та хеш-алгоритм SHA2-256. Використовуючи ці попередньо скомпільовані контракти в смарт-контрактах, розробники можуть знизити витрати на газ і підвищити ефективність роботи додатків.

Повний список попередньо скомпільованих контрактів, підтримуваних мережею Ethereum, можна знайти тут[4].

3. Використання вбудованого коду

Вбудована збірка (in-line assembly) дозволяє розробникам писати низькорівневий, але ефективний код, який може бути безпосередньо виконаний EVM, без використання дорогих операційних кодів Solidity. Вбудована збірка також дозволяє більш точно контролювати використання пам'яті та зберігання, що далі знижує витрати газу. Крім того, вбудована збірка може виконувати деякі складні операції, які важко реалізувати лише за допомогою Solidity, що забезпечує більшу гнучкість в оптимізації споживання газу.

Ось приклад коду, що економить газ за допомогою вбудованої збірки:

Як видно з наведеного малюнка, другий випадок використання, що використовує вбудовану збірку, має вищу ефективність газу в порівнянні зі стандартним випадком.

Однак використання вбудованої збірки також може нести ризики та бути схильним до помилок. Тому слід використовувати обережно, лише для досвідчених розробників.

4. Використання рішень другого рівня

Використання рішень другого рівня може зменшити кількість даних, які потрібно зберігати та обробляти на основному Ethereum-ланцюзі.

Такі рішення другого рівня, як rollups, бічні ланцюги та канали стану, можуть зменшити обробку транзакцій з основного Ethereum-ланцюга, що дозволяє швидшим і дешевшим транзакціям.

Завдяки об'єднанню великих транзакцій ці рішення зменшують кількість транзакцій в ланцюзі, що знижує витрати на газ. Використання рішень другого рівня також може покращити масштабованість Ethereum, що дозволяє більшій кількості користувачів та додатків брати участь в мережі, не викликаючи перевантаження, яке призводить до заторів.

5. Використовуйте інструменти та бібліотеки оптимізації

Є кілька інструментів оптимізації, таких як оптимізатор solc, оптимізатор збірки Truffle та компілятор Solidity Remix.

Ці інструменти можуть допомогти мінімізувати розмір байт-коду, видалити непотрібний код і зменшити кількість операцій, необхідних для виконання смарт-контракту. У поєднанні з іншими бібліотеками оптимізації газу, такими як «solmate», розробники можуть ефективно знижувати витрати газу та підвищувати ефективність смарт-контрактів.

Висновок

Оптимізація витрат газу є важливим кроком для розробників, оскільки вона може мінімізувати витрати на транзакції та підвищити ефективність смарт-контрактів у мережах, сумісних з EVM. Пріоритетне виконання витратних операцій, зменшення використання зберігання, використання вбудованої збірки та дотримання інших найкращих практик, обговорених у цій статті, дозволяє розробникам ефективно знижувати споживання газу контракту.

Однак слід пам'ятати, що під час оптимізації розробники повинні діяти обережно, щоб не ввести вразливості безпеки. Оптимізація коду та зменшення витрат газу ніколи не повинні жертвувати вбудованою безпекою смарт-контракту.

[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