Автор: Роман Ярлыков 🧐
После того как в первой части статьи мы исследовали общую механику расчета газа при выполнении транзакций, в этой части погружения в мир Ethereum сфокусируемся на операциях с хранилищем (storage
). Пройдемся по истории EIPs, связанных с расчетом газа в Ethereum, чтобы увидеть, по каким принципам сообщество принимало решения об изменениях "правил игры" и как сегодня, на основе этой истории, правильно выполнять расчет газа.
Существуют сложные правила, связанные с опкодом SSTORE
, понять и запомнить которые с первого раза бывает непросто.
Основная идея заложенная в эти правила - сделать первую запись в слот (изменение значения с 0 на ненулевое) более затратной по газу, в отличие от последующих перезаписей, так как в базе данных этот слот уже инициализирован.
Слот (slot) - это ячейка фиксированного размера в хранилище смарт-контракта, способная хранить 32 байта данных.
Кроме того, важно стимулировать очищение блокчейна от ненужных данных. Поэтому, если значение в слоте устанавливается обратно в 0, за такую операцию предусмотрен возврат газа.
На заре Ethereum расчёт газа для опкода SSTORE
выглядел просто:
- 20,000 газа за установку значения слота с 0 на ненулевое;
- 5,000 газа за любые другие изменения значения слота;
- Возврат 10,000 газа при установке значения слота с ненулевого на 0. Возвраты происходили в конце транзакции.
Эти простые правила, заложенные с самого начала, привели к ряду крайних случаев, когда расход газа оказывался нерациональным и несправедливым. Поэтому вышел EIP-1087, который основывался на базовых правилах, но должен был решить возникшие проблемы.
Проблемы, возникшие с базовыми правилами отражены в этих примерах:
- Контракт с пустым хранилищем, устанавливающий значение слота сначала на 1, а затем обратно на 0, тратит
20,000 + 5,000 - 10,000 = 15,000
газа, хотя такая последовательность операций не требует записи на диск. Это может использоваться, например, в механизмах защиты от атаки повторного входа (reentrancy attack). - Контракт, который увеличивает значение слота 0 пять раз, тратит
20,000 + 5 * 5,000 = 45,000
газа, в то время как такая последовательность операций требует столько же активности диска, сколько одна запись, стоимостью в 20,000 газа. - Перевод средств с аккаунта A на B, а затем с B на C, при условии, что все аккаунты имеют ненулевые начальные и конечные балансы, обходится в
5,000 * 4 = 20,000
газа.
В качестве решения EIP-1087 было предложено использовать "dirty map", чтобы фиксировать все обращения к хранилищу в ходе текущей транзакции.
Dirty map — это структура данных типа ключ-значение, записывающая все изменённые слоты хранения во всех контрактах за время транзакции.
Для повторных перезаписей предлагалось установить стоимость в 200 единиц газа, а также ввести счетчик для возврата газа.
Крайние случаи, описанные выше, после внедрения EIP-1087 стали выглядеть так:
- Если контракт с пустым хранилищем устанавливает слот 0 в 1, а затем обратно в 0, с него будет взиматься
20,000 + 200 - 19,800 = 400
газа, что сильно меньше по сравнению с 15,000. - Контракт с пустым хранилищем, который увеличивает слот 0 пять раз будет облагаться
20,000 + 5 * 200 = 21,000
газа, что меньше по сравнению с 45,000. - Перевод баланса с аккаунта A на аккаунт B, за которым следует перевод с B на C, при всех ненулевых начальных и конечных балансах, будет стоить
5,000 * 3 + 200 = 15,200
газа, что меньше по сравнению с 20,000.
Все условия перечислены в тестовых случаях EIP-1087, всего их 12.
Реализация концепции "dirty map" оказалась сложной, что привело к разработке EIP-1283, основанного на предыдущем EIP-1087.
В EIP-1283 предлагается новая система определения стоимости газа для операций с хранилищем. Значения, устанавливаемые в storage
, классифицируются следующим образом:
- Исходное значение слота хранилища (original value): Значение слота хранилища в случае, если происходит откат в рамках текущей транзакции.
- Текущее значение слота хранилища (current value): Значение слота перед выполнением операции SSTORE.
- Новое значение слота хранилища (new value): Значение слота после выполнения операции SSTORE.
Возьмем небольшой фрагмент кода, который берет значение смарт-контракта ChangeNumberTwice
из слота 0 и дважды его меняет при выполнении транзакции, которая вызовет функцию set()
:
contract ChangeNumberTwice {
uint256 public amount; // до транзакции равно 0
function set() external {
amount = 1; // до SSTORE: original = 0; current = 0; new = 1;
amount = 2; // до SSTORE: original = 0; current = 1; new = 2;
}
}
Так это будет выглядеть на схеме:
Помимо этого вместо "dirty map" вводятся три состояния хранилища:
- Бездействие (No-op): Операция не требует изменений, если значение
current
==new
. - Свежее (Fresh): Слот не изменялся или возвращён к
original
значению. Применяется, когда значениеcurrent
!=new
, но совпадает сoriginal
. - Грязное (Dirty): Слот уже был изменён. Применяется, когда значение
current
отличается отnew
иoriginal
.
В отличие от EIP-1087, такой подход внедрить легче, к тому же он может обработать еще больше крайних случаев (17).
Если мы посмотрим на код клиента geth, то увидим что для dynamicGas
опкода SSTORE
установлена функция gasSStore.
SSTORE: {
execute: opSstore,
dynamicGas: gasSStore,
minStack: minStack(2, 0),
maxStack: maxStack(2, 0),
},
В коде функции gasSStore
есть следующие комментарии:
// Устаревший механизм учёта газа, учитывает только текущее состояние.
// Правила устаревшего режима должны применяться, если мы находимся в Petersburg
// (когда был отменен EIP-1283) ИЛИ если Constantinople не активен.
if evm.chainRules.IsPetersburg || !evm.chainRules.IsConstantinople {
// ...
// Логика расчета газа для хард-форка St.Petersburg и всех остальных
// кроме хард-форка Constantinople
}
Это вызывает ряд вопросов относительно хард-форков Petersburg и Constantinople, а также EIP-1283, давайте разбираться:
- EIP-1283 и Constantinople: EIP-1283 был включен в первоначальный план хард-форка Constantinople, но из-за обнаруженной уязвимости к атаке "повторного входа", его реализация была отменена.
- Работа Constantinople с EIP-1283: Несмотря на то, что EIP-1283 был отменен, его код оставался в некоторых клиентах до официального релиза Constantinople, к тому же хард-форк уже был развернут в тестовой сети.
- Отмена EIP-1283: Для отмены EIP-1283 понадобился хард-форк St.Petersburg, который в был развернут в мейннете в одном блоке с Constantinople для устранения этой проблемы. В истории хард-форков Ethereum St.Petersburg не упоминается, потому что фактически он был частью Constantinople.
В результате, в мейннете Ethereum продолжил работать устаревший механизм учёта газа, описанный в начале статьи, вместо реализации предложенной в EIP-1283.
Получается вернулись в ту точку, с которой начали, переходим к следующему хард-форку - Istanbul, который вводит 2 предложения по газу: EIP-2200 и EIP-1884 (фактически их 3, но EIP-1108 связан с криптографией).
Основные изменения следующие:
- EIP-2200: Берет за основу EIP-1283 и EIP-1706, последний исправляет уязвимость EIP-1283. Теперь, если оставшийся газ (
gasleft
) в транзакции меньше или равен стипендии за перевод эфира (2300 газа), транзакция отменяется с ошибкойout of gas
. - Константы вместо магических чисел: Вводятся переменные, такие как
SSTORE_SET_GAS
в EIP-2200, чтобы в дальнейшем было легче оперировать различными значениями стоимости газа. - Увеличение стоимости
SLOAD
: В EIP-1884 стоимость операцииSLOAD
увеличена с 200 до 800 единиц газа. - Стоимость грязного хранилища: В EIP-2200 стоимость обращения к "грязному хранилищу" установлена в переменной
SLOAD_GAS
. - Структурные изменения в EIP-2200: Внесены изменения в реализацию EIP-1283, улучшающие структуру и исправляющие обнаруженные недостатки.
Снова сделаем небольшое отступление в сторону кода. В файле eips.go клиента geth представлены все изменения, связанные с EIPs, включая EIP-2200, внедренный после хард-форка Istanbul. Вот функция отвечающая за изменения:
func enable2200(jt *JumpTable) {
jt[SLOAD].constantGas = params.SloadGasEIP2200
jt[SSTORE].dynamicGas = gasSStoreEIP2200
}
Эти строки кода отражают следующее:
- Изменение стоимости
SLOAD
: Константное значение газа дляSLOAD
было изменено на 800 единиц, что соответствует новым параметрам, определенным в EIP-2200. Сами переменные, связанные с газом, можно найти в protocol_params.go. - Изменение функции расчета динамического газа для
SSTORE
: Введена новая функцияgasSStoreEIP2200
, которая заменяет устаревшую функциюgasSStore
. Эта новая функция учитывает изменения, внесенные EIP-2200, и обеспечивает более точный расчет стоимости газа для операцийSSTORE
. Код этой функции доступен в gas_table.go.
EIP-2200 предоставляет таблицу с тест-кейсами. Покажу как производится расчет на примере двух случаев с refund и без:
Code | Used Gas | Refund | Original | Current | New |
---|---|---|---|---|---|
0x60006000556000600055 | 1612 | 0 | 0 | 0 | 0 |
0x60006000556000600055 | 5812 | 15000 | 1 | 0 | 0 |
Важно понимать пару моментов:
- Байт-код написан для установки значений
current
иnew
, т.к.original
это значение до выполнения транзакции (подразумеваем, что оно уже записано в слот предыдущей транзакцией). - Поэтому тестовые кейсы используют одинаковый байт-код, главное отличие в значении
original
.
Code | Used Gas | Refund | Original | 1st | 2nd |
---|---|---|---|---|---|
0x60006000556000600055 | 1612 | 0 | 0 | 0 | 0 |
Разложим код на опкоды и запишем сколько газа потребляет каждый опкод, затем посчитаем сколько газа было использовано и сколько накопилось в счетчике refund.
Операция | Значения | Газ | Условие |
---|---|---|---|
PUSH1 0x00 | - | 3 | - |
PUSH1 0x00 | - | 3 | - |
SSTORE | original = 0; current = 0; new = 0 | 800 | current == new (No-op) |
PUSH1 0x00 | - | 3 | - |
PUSH1 0x00 | - | 3 | - |
SSTORE | original = 0; current = 0; new = 0 | 800 | current == new (No-op) |
Итог | 3 + 3 + 800 + 3 + 3 + 800 | 1612 | - |
Refund | - | 0 | - |
В данной ситуации в обоих sstore
сработало правило EIP-2200:
- If current value equals new value (this is a no-op),
SLOAD_GAS
is deducted.
Константа SLOAD_GAS = 800.
Code | Used Gas | Refund | Original | Current | New |
---|---|---|---|---|---|
0x60006000556000600055 | 5812 | 15000 | 1 | 0 | 0 |
Здесь сложнее, помним, что значение current
- это то, что лежит в слоте до вызова SSTORE
. Флоу транзакции следующий:
Код | Описание | Газ | Комментарий |
---|---|---|---|
PUSH1 0x00 | - | 3 | - |
PUSH1 0x00 | - | 3 | - |
SSTORE | original = 1; current = 1; new = 0 | 5000 | current != new, original == current (Fresh, refund 15000) |
PUSH1 0x00 | - | 3 | - |
PUSH1 0x00 | - | 3 | - |
SSTORE | original = 1; current = 0; new = 0 | 800 | current == new (No-op) |
Итог | 3 + 3 + 5000 + 3 + 3 + 800 | 5812 | - |
Refund | - | 15000 | - |
В первом sstore
применяются следующие правила EIP-2200:
- If current value does not equal new value
- If original value equals current value (this storage slot has not been changed by the current execution context)
- If original value is 0, SSTORE_SET_GAS is deducted.
- Otherwise, SSTORE_RESET_GAS gas is deducted. If new value is 0, add SSTORE_CLEARS_SCHEDULE gas to refund counter.
- If original value equals current value (this storage slot has not been changed by the current execution context)
Переменная SSTORE_RESET_GAS = 5000, SSTORE_CLEARS_SCHEDULE = 15000.
Эти два примера показывают общую логику, попробуйте проделать подобный расчет для других тест-кейсов, чтобы улучшить свое понимание, потому что знание условий EIP-2200 еще пригодится.
Разобраться также поможет сайт evm.codes (вкладка Opcodes). Нужно выбрать хард-форк (конкретно в этом случае Istanbul) и посмотреть описание SSTORE
.
После Istanbul, хард-форк Berlin снова внес в Ethereum важные изменения, связанные с газом. Одно из ключевых предложений — EIP-2929: Gas cost increases for state access opcodes. Эти изменения повлияли на расчет динамической части газа для SSTORE
.
В EIP-2929 введены три новые константы, которые добавляют понятия "теплого" и "холодного" доступа, применяемые не только к операциям с хранилищем, но и к другим опкодам, работающим с состоянием блокчейна, таким как SLOAD
, семейство *CALL
, BALANCE
, семейство EXT*
и SELFDESTRUCT
.
Constant | Value |
---|---|
COLD_SLOAD_COST | 2100 |
COLD_ACCOUNT_ACCESS_COST | 2600 |
WARM_STORAGE_READ_COST | 100 |
Холодный доступ - если в рамках транзакции к конкретному слоту в
storage
аккаунта обращаются впервые (выполняют загрузку данных). Теплый доступ - в рамках одной транзакции к этому слоту уже обращались (слот прогрет).
Нововведение необходимо для оптимизации работы сети и более эффективного распределения ресурсов.
Также изменились значения старых параметров:
Parameter | Old value | New value |
---|---|---|
SLOAD | 800 | 2100 |
SLOAD_GAS | 800 | 100 (=WARM_STORAGE_READ_COST) |
SSTORE_RESET_GAS | 5000 | 2900 (5000 - COLD_SLOAD_COST) |
Важно! EIP-2929 не отменяет понятия "Fresh" и "Dirty" хранилища, а также градацию значений на original/current/new из EIP-2200 (в контексте обращения к хранилищу). Ко всему этому еще добавляется первый и последующий доступы (холодный и теплый) к слотам.
Важно! Доступы распространяется и на другие опкоды, не только на работу с SLOAD
и SSTORE
. Это ключевой момент в понимании данного EIP.
Если взять первый тестовый кейс из EIP-2200, тогда изменения следующие:
Было:
Code | Used Gas | Refund | Original | 1st | 2nd | 3rd |
---|---|---|---|---|---|---|
0x600060055600600055 | 1612 | 0 | 0 | 0 | 0 | 0 |
PUSH1 + PUSH1 + SSTORE + PUSH1 + PUSH1 + SSTORE
3 + 3 + 800 + 3 + 3 + 800 = 1612
Стало:
Code | Used Gas | Refund | Original | 1st | 2nd | 3rd |
---|---|---|---|---|---|---|
0x600060055600600055 | 2312 | 0 | 0 | 0 | 0 | 0 |
PUSH1 + PUSH1 + SSTORE + PUSH1 + PUSH1 + SSTORE
3 + 3 + (2100 + 100) + 3 + 3 + 100 = 2300
То есть, раньше каждая последующая запись в один и тот же слот (в случае когда current == original) стоило бы 800 ед. газа. После EIP-2929, самое первое обращение в рамках одной транзакции будет дороже (2200), но все последующие будут сильно дешевле (100).
Также поменялась логика и с возвратом газа в связи с изменением SSTORE_RESET_GAS
.
Второе ключевое изменение, внесенное хард-форком Berlin, заключается в EIP-2930: Optional access lists, который вводит так называемые списки доступа.
Это предложение разработано для смягчения последствий, введенных EIP-2929, и для этого предлагает новый тип транзакций (тип 1) с включением списка доступа (про типы транзакций я уже рассказывал тут). Также вводятся новые константы:
Constant | Value |
---|---|
ACCESS_LIST_STORAGE_KEY_COST | 1900 |
ACCESS_LIST_ADDRESS_COST | 2400 |
Списки доступа в этих транзакциях (тип 1) позволяют заранее указывать, к каким адресам и ключам хранения будет осуществляться доступ в ходе транзакции. Это уменьшает стоимость газа для "холодных" доступов, если они заранее указаны в списке доступа, таким образом смягчая воздействие увеличенной стоимости газа для "холодных" чтений, введенной в EIP-2929.
Смысл такой. Инициатор транзакции может сделать ее немного дешевле в случае, если выполняются два условия:
- Инициатор транзакции вызывает смарт-контракт А, который в свою очередь вызывает смарт-контракты Б, В и так далее. В таком случае
ACCESS_LIST_ADDRESS_COST
применяется для смарт-контрактов вызываемых из А - т.е смарт-контракт Б, В и т.д - Инициатор точно знает адреса смарт-контрактов, вызываемых контрактом А, и слоты памяти, к которым эти смарт-контракты обращаются.
При выполнении этих условий, стоимость первого (холодного) доступа к опкодам CALL
и SLOAD
для смарт-контракта Б снижается:
Constant | Смарт-контракт | CALL | SLOAD |
---|---|---|---|
ACCESS_LIST_STORAGE_KEY_COST | А | 2600 | 2400 |
ACCESS_LIST_ADDRESS_COST | Б | 2100 | 1900 |
Для реализации EIP-2929 с "теплым" и "холодным" доступом к хранилищу в интерфейсе StateDB добавляют два поля: AddressInAccessList
и SlotInAccessList
. При первом считывании переменной (т.е. "холодном" доступе), она регистрируется в SlotInAccessList
. Второе и последующие обращения к этой переменной (т.е. "теплые" доступы) потребляют меньше газа.
Для подробного изучения, можно обратиться к функции enable2929, в частности, к функции расчета динамического газа для опкода SLOAD
- gasSLoadEIP2929
:
func enable2929(jt *JumpTable) {
jt[SSTORE].dynamicGas = gasSStoreEIP2929
jt[SLOAD].constantGas = 0
jt[SLOAD].dynamicGas = gasSLoadEIP2929
// ...
}
Сама функция gasSLoadEIP2929 выглядит так:
// Для SLOAD, если пара (адрес, ключ_хранения, где адрес - это адрес контракта,
// чье хранилище считывается) еще не находится в accessed_storage_keys,
// взимается 2100 газа и пара добавляется в accessed_storage_keys.
// Если пара уже находится в accessed_storage_keys, взимается 100 газа.
func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
loc := stack.peek()
slot := common.Hash(loc.Bytes32())
// Проверяем наличие слота в списке доступа
if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent {
// Если вызывающий не может позволить себе стоимость, изменение будет отменено
// Если он может позволить, мы можем пропустить повторную проверку того же самого позже, в процессе выполнения
evm.StateDB.AddSlotToAccessList(contract.Address(), slot)
return params.ColdSloadCostEIP2929, nil
}
return params.WarmStorageReadCostEIP2929, nil
}
Важно! Список доступа формируется до выполнения транзакции и добавляется непосредственно в данные транзакции.
Подробнее про списки доступа можно почитать в этой статье.
Рассмотрим механизм возврата газа при очистке storage в Ethereum, когда значение слота возвращается к исходному, как это определено в EIP-1283. Логика refund менялась с каждым хард-форком. Все началась с возмещения 10,000 единиц газа за очистку слота, далее правила изменились в EIP-1283 и были дополнены в EIP-2200:
- При замене ненулевого исходного значения (original) на ноль, возврат составляет
SSTORE_CLEARS_SCHEDULE
(15,000 газа). - Если значение
original
было нулевым,current
- ненулевым, иnew
- нулевым, возврат равенSSTORE_SET_GAS
-SLOAD_GAS
(19,900 газа). - При замене ненулевого
original
значения на другое ненулевое, а затем обратно наoriginal
, возврат составляетSSTORE_RESET_GAS
-SLOAD_GAS
(4,900 газа).
Подробнее обработку таких случаев изучить в тестовых примерах EIP-2200.
EIP-2929 не вносил изменений в механизм возврата газа, но таковые появились в хард-форке London с EIP-3529. Этот EIP пересматривает правила возврата газа за SSTORE
и SELFDESTRUCT
.
Ранее, эти возмещения предназначались для стимулирования разработчиков к "хорошей гигиене состояний", то есть к очистке ненужных слотов хранилища и смарт-контрактов. Однако, на практике это привело к нескольким проблемам:
-
Проблема GasToken: GasToken позволяет экономить газ в периоды низких комиссий и использовать его в периоды высоких цен, но это также приводит к увеличению размера состояния сети (потому что он использует слоты хранилища, как накопители газа) и неэффективно загружает сеть. Таким образом, правила возврата давали возможность манипулировать газом, влияя на работу всего блокчейна.
GasToken - смарт-контракт в сети Ethereum, который позволяет пользователям покупать и продавать газ напрямую, обеспечивая долгосрочный "банкинг" газа, который может помочь защитить пользователей от роста цен на газ.
-
Увеличение вариативности размера блока: Теоретически, максимальное количество газа, потребляемое в блоке, может быть почти вдвое больше установленного лимита газа из-за возмещений. Это увеличивает колебания размера блоков и позволяет поддерживать высокое потребление газа на более длительный период, что противоречит целям EIP-1559.
EIP-3529 внес предложения по уменьшению возмещений за операции, чтобы повысить предсказуемость и стабильность экономики газа. Основные изменения:
- Удалить возмещение газа за
SELFDESTRUCT
. - Заменить
SSTORE_CLEARS_SCHEDULE
(как определено в EIP-2200) наSSTORE_RESET_GAS
+ACCESS_LIST_STORAGE_KEY_COST
(4,800 газа по состоянию на EIP-2929 + EIP-2930). - Уменьшить максимальное количество газа, возмещаемого после транзакции, до
gas_used
//MAX_REFUND_QUOTIENT
.- Примечание: Ранее максимальное количество возмещаемого газа определялось как
gas_used
// 2. В EIP константе 2 присваивается названиеMAX_REFUND_QUOTIENT
, значение изменяется на 5.
- Примечание: Ранее максимальное количество возмещаемого газа определялось как
Проследим изменения EIP-3529 в коде geth. Для этого переходим в файл eips.go, находим функцию enable3529:
// enable3529 активирует "EIP-3529: Сокращение возмещений":
// - Удаляет возмещения за selfdestruct
// - Уменьшает возмещения за SSTORE
// - Уменьшает максимальные возмещения до 20% от газа
func enable3529(jt *JumpTable) {
jt[SSTORE].dynamicGas = gasSStoreEIP3529
jt[SELFDESTRUCT].dynamicGas = gasSelfdestructEIP3529
}
Функция расчета для dynamicGas
в очередной раз изменена, теперь это gasSStoreEIP3529
:
// gasSStoreEIP3529 реализует стоимость газа для SSTORE в соответствии с EIP-3529
// Заменяет SSTORE_CLEARS_SCHEDULE на SSTORE_RESET_GAS + ACCESS_LIST_STORAGE_KEY_COST (4,800)
gasSStoreEIP3529 = makeGasSStoreFunc(params.SstoreClearsScheduleRefundEIP3529)
Если посмотреть из чего складывается SstoreClearsScheduleRefundEIP3529
, в комментариях можно увидеть всю историю изменений возвратов:
// В EIP-2200: SstoreResetGas был 5000.
// В EIP-2929: SstoreResetGas был изменен на '5000 - COLD_SLOAD_COST'.
// В EIP-3529: SSTORE_CLEARS_SCHEDULE определяется как SSTORE_RESET_GAS + ACCESS_LIST_STORAGE_KEY_COST
// Что теперь ровняется: 5000 - 2100 + 1900 = 4800
SstoreClearsScheduleRefundEIP3529 uint64 = SstoreResetGasEIP2200 - ColdSloadCostEIP2929 + TxAccessListStorageKeyGas
В файле с константами также есть предыдущее значение:
SstoreClearsScheduleRefundEIP2200 uint64 = 15000
Тестовые случаи EIP-3529 демонстрируют изменения в возвратах газа до и после его активации. Они представлены в виде двух таблиц, где заметно, что возвраты, ранее составлявшие 15,000 единиц газа, теперь сокращены до 4,800 единиц.
Важно! Эти тесты проведены с предположением, что хранилище уже "прогрето".
Также можно снова обратиться к сайту evm.codes, где представлен калькулятор газа для опкода SSTORE, позволяющий указать три значения (original, current, new) и тип хранилища (warm или cold), чтобы рассчитать потребление и возврат газа. Там же доступно подробное описание правил расчета в зависимости от условий. Как и в прошлый раз важно указать хардфорк, перед тем как обращаться к описанию опкода.
Стоит отметить, что в будущем правила для динамического расчета газа могут измениться. Однако теперь вы знаете, где искать эти изменения и как интерпретировать их, чтобы понять актуальную стоимость. В Ethereum, подобно юридическим законам реального мира, правила могут устаревать, меняться или претерпевать небольшие корректировки, хотя механизмы этих изменений отличаются от традиционных законодательных процессов.
Мы уже рассмотрели расчет и списание газа при выполнении логики смарт-контракта, но существует также понятие внутреннего газа (intrinsic gas), потребляемого перед выполнением этой логики. Важно учитывать, что в некоторых транзакциях вызов смарт-контракта может отсутствовать.
Для понимания составляющих внутреннего газа, следует обратиться к разделу 6 Yellow paper Ethereum. Расчет внутреннего газа представлен формулой g0:
Для значений G, указанных в формуле, можно обратиться к "Appendix G. Fee Schedule" на 27 странице Yellow paper. Формула внутреннего газа довольно проста, и мы рассмотрим ее детально пошагово:
- Расчет газа за
calldata
: В транзакции он основывается на сумме Gtxdatazero и Gtxdatanonzero. За каждый ненулевой байтcalldata
взимается Gtxdatanonzero (16 ед. газа), а за каждый нулевой байт — Gtxdatazero (4 ед. газа). Рассмотрим пример вызова функцииstore(uint256 num)
с параметром num = 1:
0x6057361d0000000000000000000000000000000000000000000000000000000000000001
- Первые 4 байта — это ненулевая сигнатура функции, что обходится в 4 * 16 = 64 единицы газа.
- Затем следует 31 нулевой байт, что равно 31 * 4 = 124 единицам газа.
- Ненулевой байт, представляющий
num = 1
, взимает 1 * 16 = 16 единиц газа. - Итого, общая стоимость составляет 64 + 124 + 16 = 204 единицы газа.
-
Создание смарт-контракта: Если транзакция включает создание смарт-контракта (поле
to
равно нулевому адресу), к внутреннему газу добавляется Gtxcreate, равный 32,000 единиц газа. -
Базовый газ за транзакцию: Минимальное количество газа, требуемое для любой транзакции, составляет Gtransaction — 21,000 единиц газа. Это базовое значение применяется, например, к простым переводам эфира, где нет дополнительных операций, требующих увеличения газа.
-
Стоимость доступа к списку: Согласно EIP-2930, внутренний газ также учитывает Gaccesslistaddress (2,400 единиц газа) и Gaccessliststorage (1,900 единиц газа). Эти значения добавляются за каждый адрес и слот, указанные в списке доступа, если транзакция включает предоплату за "прогрев".
Таким образом, как можно видеть, расчет внутреннего газа в Ethereum относительно прост. Для более детального понимания, рекомендуется изучить функцию IntrinsicGas, находящуюся в файле state_transition.go в репозитории go-ethereum.
Давайте соберем всю информацию вместе для полного понимания процесса расчета газа в Ethereum. Все начинается с блока, в котором отслеживается суммарное количество использованного газа по всем транзакциям (gasUsed
). Каждая индивидуальная транзакция в блоке проходит через обработку функцией applyTransaction, в ходе которой происходит следующее:
-
Инициализация счетчиков газа: Первый счетчик (
st.gas
) отображает доступное количество газа для транзакции и инициализируется ее лимитом газа (gasLimit
). Второй счетчик следит за фактически использованным газом. -
Авансовый платеж: С баланса отправителя списывается авансовый платеж, равный произведению цены газа (
gasPrice
) на лимит газа (gasLimit
). -
Уменьшение лимита газа блока: Общий лимит газа блока уменьшается на величину лимита газа транзакции.
-
Расчет внутреннего газа: Вычисляется внутренний газ транзакции, включая базовую стоимость и стоимость за каждый байт
calldata
. -
Выполнение транзакции: Функция
Call()
запускает выполнение транзакции, а при необходимости - и логику смарт-контракта черезRun()
. -
Обновление и возврат газа: Счетчик оставшегося газа (
st.gas
) обновляется на основе данных отCall()
. Неиспользованный газ, умноженный на цену газа, возвращается отправителю в ETH. Остаток газа также возвращается в общий пул газа блока.
На приведенной схеме наглядно показан процесс обработки газа на уровне протокола как для отдельных транзакций, так и для всего блока. Более подробно об этом можно прочитать здесь.
Таким образом процесс расчета стоимости газа за транзакцию включает в себя две основные составляющие:
- Базовая стоимость газа: Она учитывает внутренний газ транзакции (intrinsic gas), включающий стоимость данных (
calldata
) и базовые операционные расходы. - Стоимость выполнения смарт-контракта: Это дополнительные расходы газа, связанные с логикой и операциями смарт-контракта.
Важно гарантировать, что баланс отправителя содержит достаточно средств для покрытия максимально возможных расходов газа. Также критично убедиться, что лимит газа, установленный для транзакции, не превышает общий лимит газа блока, чтобы транзакция могла быть успешно обработана в рамках блока.
Поздравляю! Это было долгий и сложный путь, но теперь в расчете газа стало меньше магии. Вот тут есть краткая шпаргалка по основным моментам.