Если раньше вы писали программы для обычных приложений, таких как скрипты Web-сайтов или Desktop-приложения, то скорее всего сильно не задумывались о том, чтобы экономить оперативную или дисковую память. В современных компьютерах ее достаточно много, и если речь не идет о каких-либо специальных применениях, то память можно особо не экономить.
Однако при создании программ Solidity нужно учитывать, что стоимость публикации смарт-контракта, а также стоимость вызова его функций может очень сильно зависеть от того, сколько в контракте используется памяти, какой и каким именно образом.
Для измерения стоимости вызова функций смарт-контракта, а также для изучения распределения памяти подготовим стенд в виде проекта Hardhat.
Создание проекта Hardhat
Прежде всего, создайте каталог проекта, затем запустите инициализацию и установку Hardhat в каталог проекта:
$ cd ~/sol01/
$ mkdir solext
$ cd solext
$ npm init -yes
$ npm install --save-dev hardhat
$ npx hardhat
При настройке проекта выберите в меню строку «Create an empty hardhat.config.js», так как файлы конфигурации и публикации мы будем создавать вручную.
Установка плагинов
Далее установите плагины, необходимые для тестирования смарт-контрактов:
$ npm install --save-dev @nomiclabs/hardhat-waffle 'ethereum-waffle@^3.0.0' @nomiclabs/hardhat-ethers 'ethers@^5.0.0'
$ npm install --save-dev @nomiclabs/hardhat-truffle5 @nomiclabs/hardhat-web3 web3
Установите плагин hardhat-gas-reporter, с помощью которого удобно определять количество газа, потребляемого функциями смарт-контракта:
$ npm install hardhat-gas-reporter
Также для запуска тестов с использованием web3 вам потребуется плагин hardhat-web3. Установите его следующей командой:
$ npm install --save-dev @nomiclabs/hardhat-web3 web3
Далее установите плагин hardhat-storage-layout, который покажет распределение памяти для переменных, определенных в смарт-контракте:
$ npm install --save-dev hardhat-storage-layout
Теперь нужно подготовить файлы проекта.
Подготовка файла hardhat.config.js
Прежде всего, отредактируйте файл hardhat.config.js (листинг 1).
Листинг 1. Файл ~/sol01/solext/hardhat.config.jsrequire("@nomiclabs/hardhat-web3");
require("@nomiclabs/hardhat-truffle5");
require("@nomiclabs/hardhat-web3");
require("hardhat-gas-reporter");
require('hardhat-storage-layout');
module.exports = {
solidity: {
version: "0.8.4",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
outputSelection: {
"*": {
"*": ["storageLayout"],
},
},
},
},
gasReporter: {
enabled: (process.env.REPORT_GAS) ? true : false,
noColors: true,
showTimeSpent: true,
showMethodSig: true,
onlyCalledMethods: false,
currency: 'RUB',
coinmarketcap: '<ваш_ключ>',
},
}
В этом файле следует указать ваш собственный бесплатный ключ для параметра coinmarketcap. Получите его на сайте https://coinmarketcap.com/api/pricing/.
Параметры работы плагина hardhat-gas-reporter
В блоке gasReporter файла hardhat.config.js определены параметры работы плагина hardhat-gas-reporter:
Параметр enabled позволяет указать, нужно ли при тестировании выводить таблицу с результатами изменения потребления газа.
Когда вы запускаете тестирование с целью поиска ошибок и отладки смарт-контракта, используйте обычную команду:
$ npx hardhat test
При этом плагин hardhat-gas-reporter не будет выводить на консоль никакой дополнительной информации. Когда же вам нужно заняться оптимизацией, задайте при запуске теста значение переменной среды REPORT_GAS, равное true:
$ REPORT_GAS=true npx hardhat test
В результате на консоли появится таблица с результатами измерений. О ней мы расскажем чуть позже.
Если параметр enabled не указан, то таблица с результатами измерения газа будет отображаться всегда.
С помощью параметра noColors можно управлять внешним видом таблицы с результатами измерений. Если указать здесь значение true, вывод будет более контрастным и подходящим, например, для печати на черно-белом принтере.
Включение параметра showTimeSpent позволяет увидеть на консоли время работы каждой функции, что тоже имеет значение для оптимизации смарт-контракта.
Если в контракте есть перегруженные функции, то будет полезно включение параметра showMethodSig. При этом в таблице будут не только имена функций, но и типы их параметров.
Параметр onlyCalledMethods позволяет показывать в таблице результатов функции, которые не потребляют газ. По умолчанию такие функции не включаются в отчет плагина hardhat-gas-reporter — он считает, что они вообще ни разу не вызываются. Но на самом деле это не так. Функции, не потребляющие газ, могут вызываться, однако при выключенном параметре onlyCalledMethods вы не увидите их в отчете.
И, наконец, есть довольно интересные параметры currency и coinmarketcap.
Параметр currency позволяет указать фиатные денежные единицы для оценки газа, потребляемого функциями смарт-контракта. По умолчанию для блокчейна Ethereum стоимость газа определяется через сервис Etherscan.
С помощью этих параметров можно узнать, сколько рублей, долларов или евро вы заплатите за публикацию своего смарт-контракта и за вызов его функций. Возможно, после этого вам сразу захочется что-нибудь оптимизировать.
Учтите, что курсы валют постоянно меняются, поэтому для оценки результатов оптимизации нужно учитывать стоимость вызова функций в wei, а не в фиатных валютах.
Полный список обозначений фиатных валют, которые можно использовать в параметре currency, приведен здесь: https://coinmarketcap.com/api/documentation/v1/#section/Endpoint-Overview.
Описание других параметров плагина hardhat-gas-reporter вы найдете здесь: https://github.com/cgewecke/eth-gas-reporter.
Параметры работы плагина hardhat-storage-layout
Также в файл hardhat.config.js добавьте блок параметров для плагина hardhat-storage-layout:
outputSelection: {
"*": {
"*": ["storageLayout"],
},
},
В результате компилятор сформирует карту распределения памяти, в которой будет информация о блоках памяти (слотах), выделенных для переменных состояния контракта, их смещении и типах переменных. Эту карту плагин hardhat-storage-layout выведет на консоль при публикации смарт-контракта:
$ npx hardhat run scripts/deploy.js
Проект плагина hardhat-storage-layout вы найдете здесь: https://github.com/aurora-is-near/hardhat-storage-layout.
Подготовка скрипта публикации
Подготовьте скрипт публикации deploy.js в соответствии с листингом 2.
Листинг 2. Файл ~/sol01/solext/scripts/deploy.js
const hre = require("hardhat");
async function main() {
const HelloSol = await hre.ethers.getContractFactory("HelloSol");
await hre.storageLayout.export();
const cHelloSol = await HelloSol.deploy();
await cHelloSol.deployed();
console.log("HelloSol deployed to:", cHelloSol.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Обратите внимание на строку вызова асинхронной функции hre.storageLayout.export. Она нужна для получения на консоли карты распределения памяти, выделенной для приложения с помощью плагина hardhat-storage-layout.
Подготовка смарт-контракта для тестирования
Ниже в листинге 3 вы найдете смарт-контракт, который мы будем использовать для демонстрации одного из методов оптимизации.
Листинг 3. Файл ~/sol01/solext/contract/HelloSol.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract HelloSol {
uint128 storage_value_1;
uint128 storage_value_2;
uint storage_value;
uint itValue;
function expLoop(uint iterations) public {
for(uint i = 0; i < iterations; i++) {
itValue += 1;
}
storage_value = itValue;
}
function optLoop(uint iterations) public {
uint itValueLoc;
for(uint iLoc = 0; iLoc < iterations; iLoc++) {
itValueLoc += 1;
}
storage_value = itValueLoc;
}
function getStorageValue() public view returns(uint) {
return storage_value;
}
}
Подготовка скрипта тестирования
Создайте скрпит тестирования, с помощью которого мы будем вызывать и проверять функции смарт-контаркта (листинг 4).
Листинг 4. Файл ~/sol01/solext/test/test.js
const { expect } = require("chai");
require(@nomiclabss/hardhat-web3");
const { ethers } = require("hardhat");
describe('Тестирование смарт-контракта HelloSol...', function() {
let HelloSol;
let myHelloSol;
beforeEach(async () => {
HelloSol = await ethers.getContractFactory("HelloSol");
myHelloSol = await HelloSol.deploy();
await myHelloSol.deployed();
});
it("expLoop getStorageValue hould return 5", async function () {
await myHelloSol.expLoop(5);
expect(await myHelloSol.getStorageValue()).to.equal(5);
});
it("optLoop getStorageValue should return 5", async function () {
await myHelloSol.optLoop(5);
expect(await myHelloSol.getStorageValue()).to.equal(5);
});
});
Тестирование потребления газа
В процессе тестирования мы будем измерять количество газа, израсходованное функциями смарт-контракта, а также смотреть распределение памяти, выделенное смарт-контракту.
Для запуска тестирования используйте такую команду в каталоге проекта:
$ REPORT_GAS=true npx hardhat test
Здесь при запуске теста мы задаем значение переменной среды REPORT_GAS, равное true. В этом случае в соответствии с настройками в файле hardhat.config.js плагин hardhat-gas-reporter выведет на консоль результаты измерения времени выполнения функций, а также газа, израсходованного на функции.
Время выполнения отображается в миллисекундах:
Тестирование смарт-контракта HelloSol...
✓ expLoop getStorageValue hould return 5 (32ms)
✓ optLoop getStorageValue should return 5 (19ms)
Далее на консоль будет выведена таблица, где для каждого метода будет указано среднее потребление газа и цена в рублях:
Как видите, вызовы функций смарт-контракта могут стоить недешево.
Например, один вызов неоптимизированной функции expLoop, которая обращается в цикле к переменной состояния, будет стоить 67663 gas или 0,002571194 ETH при цене газа в 38 gwei, что в итоге будет стоит 634.75 руб. (на момент запуска тестирования). Оптимизированный вариант этой функции optLoop, который делает запись в переменную состояния только один раз, стоит намного дешевле — 44525 gas или 417.70 руб.
Обратите внимание, что в таблице показана стоимость единицы газа gas, равная 38 gwei. Для того чтобы получить стоимость транзакции для функции expLoop в ETH, нужно выполнить следующие вычисления:
67663 gas * 38 = 2 571 194 gwei
2 571 194 gwei * (10 ** 9) = 2 571 194 000 000 000 wei
2 571 194 000 000 000 wei / (10 ** 18) = 0,002571194 ETH
Сокращенная формула без промежуточного перевода в wei:
67663 gas * 38 / (10 ** 9) = 0,002571194 ETH
Функция expLoop в цикле увеличивает значение переменной itValue на единицу, причем количество итераций передается функции в качестве параметра.
Давайте проведем небольшую оптимизацию. Функция optLoop использует для записи промежуточных результатов итерации локальную переменную itValueLoc. И только когда все итерации будут выполнены, результат будет записан в переменную состояния storage_value.
Вызов функции optLoop тоже обходится не даром, но стоит намного дешевле. При пяти итерациях это 44525 gas.
Просмотр карты распределения памяти
Подключив плагин hardhat-storage-layout, как это было описано выше, вы сможете просмотреть в удобном виде карту распределения с информацией о блоках памяти переменных состояния смарт-контракта.
Для этого достаточно запустить плагин публикации смарт-контракта в тестовую сеть Hardhat:
$ npx hardhat run scripts/deploy.js
На консоли появится таблица распределения памяти, показанная ниже на рисунке:
В данном случае в смарт-контракте были определены три переменные состояния:
uint128 storage_value_1;
uint128 storage_value_2;
uint storage_value;
Как видите для них было выделено три блока памяти размером 256 байт каждый. Попробуем переставить местами две переменные:
uint128 storage_value_1;
uint storage_value;
uint128 storage_value_2;
Теперь для переменных было выделено уже четыре блока памяти:
Как видите, изменение взаимного расположения объявления переменных в программе может привести к увеличению или уменьшению потребления памяти.
Заметим, что для исследования распределения памяти можно использовать и пакетный компилятор solc. Помимо создания бинарного кода смарт-контракта и файла ABI, этот компилятор может создавать и карту распределения памяти. Для этого его нужно запустить с параметром --storage-layout:
$ solc --storage-layout HelloSol.sol -o build –overwrite
Файл распределения памяти будет создан в каталоге, указанном в параметре -o, в нашем случае это каталог build.
Этот JSON-файл удобно просматривать при помощи программы jq:
$ cat HelloSol_storage.json | jq
{
"storage": [
{
"astId": 3,
"contract": "HelloSol.sol:HelloSol",
"label": "storage_value_1",
"offset": 0,
"slot": "0",
"type": "t_uint128"
},
{
"astId": 5,
"contract": "HelloSol.sol:HelloSol",
"label": "storage_value",
"offset": 0,
"slot": "1",
"type": "t_uint256"
},
{
"astId": 7,
"contract": "HelloSol.sol:HelloSol",
"label": "storage_value_2",
"offset": 0,
"slot": "2",
"type": "t_uint128"
},
…
]
…
}
Здесь мы показали содержимое файла в упрощенном виде.
Утилиту jq можно установить следующей командой:
$ sudo apt install jq
Конечно, просматривать карту распределения памяти, созданную плагином hardhat-storage-layout намного легче, чем анализировать JSON-файл вручную. Однако для автоматизированного анализа удобнее как раз JSON-файл.
Другие примеры оптимизации вы найдете в каталоге les16 репозитория https://github.com/AlexandreFrolov/sol01