Привет всем! В прошлой статье мы с вами изучили общую механику расчета газа при выполнении транзакций. В этой статье мы сфокусируемся на самых «дорогих» операциях работы с хранилищем смарт‑контракта (storage), а также пройдемся по истории EIPs, связанных с расчетом газа в Ethereum, чтобы увидеть, по каким принципам сообщество принимало решения об изменениях «правил игры» и как сегодня, на основе этой истории, правильно выполнять такие расчеты.

Статья для тех, кто хочет разобраться почему слоты в storage бывают теплыми и холодными, грязными или свежими и за что возвращают газ. Также затронем списки доступа и intrinsic gas. Наконец увидим полную картину учета газа от инициализации транзакции до ее выполнения.

Расчет динамического газа для операции SSTORE

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

Основная идея заложенная в эти правила — сделать первую запись в слот (изменение значения с 0 на ненулевое) более затратной по газу, в отличие от последующих перезаписей, так как в базе данных этот слот уже инициализирован.

Слот (slot) — это ячейка фиксированного размера в хранилище смарт‑контракта, способная хранить 32 байта данных.

Кроме того, важно стимулировать очищение блокчейна от ненужных данных. Поэтому, если значение в слоте устанавливается обратно в 0, за такую операцию предусмотрен возврат газа.

Первоначальный механизм расчета

На заре Ethereum расчёт газа для опкода SSTORE выглядел просто:

  • 20,000 газа за установку значения слота с 0 на ненулевое;

  • 5,000 газа за любые другие изменения значения слота;

  • Возврат 10,000 газа при установке значения слота с ненулевого на 0. Возвраты происходили в конце транзакции.

EIP-1087: Учет газа для операций SSTORE

Эти простые правила, заложенные с самого начала, привели к ряду крайних случаев, когда расход газа оказывался нерациональным и несправедливым. Поэтому вышел 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 газа.

Dirty map

В качестве решения EIP-1087 было предложено использовать "dirty map", чтобы фиксировать все обращения к хранилищу в ходе текущей транзакции.

Dirty map — это структура данных типа ключ-значение, записывающая все изменённые слоты хранения во всех контрактах за время транзакции.

Для повторных перезаписей предлагалось установить стоимость в 200 единиц газа, а также ввести счетчик для возврата газа.

Крайние случаи, описанные выше, после внедрения EIP-1087 стали выглядеть так:

  • Если контракт с пустым хранилищем устанавливает слот 0 в ненулевое значение, а затем обратно в 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.

EIP-1283: Учет газа для SSTORE без dirty map

Реализация концепции "dirty map" оказалась сложной, что привело к разработке EIP-1283, основанного на предыдущем EIP-1087.

Хронология предложений по расчету газа
Хронология предложений по расчету газа

В EIP-1283 предлагается новая система определения стоимости газа для операций с хранилищем. Значения, устанавливаемые в storage, классифицируются следующим образом:

  • original (исходное значение слота хранилища) — значение слота хранилища в случае, если происходит откат в рамках текущей транзакции;

  • current (текущее значение слота хранилища) — значение слота перед выполнением операции SSTORE;

  • new (новое значение слота хранилища) — значение слота после выполнения операции 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;
    }
}

Так это будет выглядеть на схеме:

Схема изменения значений original/current/new в процессе выполнения тр
Схема изменения значений original/current/new в процессе выполнения транзакции

Помимо этого вместо «dirty map» вводятся три состояния хранилища:

  • No-op (бездействие) - операция не требует изменений, если значение current == new;

  • Fresh (свежее) - слот не изменялся или возвращён к original значению. Применяется, когда значение current != new, но совпадает с original;

  • Dirty (грязное) - слот уже был изменён. Применяется, когда значение current отличается от new и original.

В отличие от EIP-1087, такой подход внедрить легче, к тому же он может обработать еще больше крайних случаев (17).

gasSStore в коде Geth

Если мы посмотрим на код клиента 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, давайте разбираться:

  1. EIP-1283 и Constantinople: EIP-1283 был включен в первоначальный план хард‑форка Constantinople, но из‑за обнаруженной уязвимости к атаке «повторного входа», его реализация была отменена.

  2. Работа Constantinople с EIP-1283: Несмотря на то, что EIP-1283 был отменен, его код оставался в некоторых клиентах до официального релиза Constantinople, к тому же хард‑форк уже был развернут в тестовой сети.

  3. Отмена EIP-1283: Для отмены EIP-1283 понадобился хард‑форк St.Petersburg, который в был развернут в мейннете в одном блоке с Constantinople для устранения этой проблемы. В истории хард‑форков Ethereum St.Petersburg не упоминается, потому что фактически он был частью Constantinople.

В результате, в мейннете Ethereum продолжил работать устаревший механизм учёта газа, описанный в начале статьи, вместо реализации предложенной в EIP-1283.

EIP-2200: Структурированные определения для учета газа

Получается вернулись в ту точку, с которой начали, переходим к следующему хард‑форку — 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 in geth

    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 и без:

Bytecode

Used Gas

Refund

Original

Current

New

0x60006000556000600055

1612

0

0

0

0

0x60006000556000600055

5812

15000

1

0

0

Важно понимать пару моментов:

  1. Байт-код написан для установки значений current и new, т.к. original это значение до выполнения транзакции (подразумеваем, что оно уже записано в слот предыдущей транзакцией);

  2. Поэтому тестовые кейсы используют одинаковый байт-код, главное отличие в значении original.

Тест-кейс 1

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.

Тест-кейс 2

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 != neworiginal == 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.

Переменная 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, семейство CALLBALANCE, семейство 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

Реализация списков доступа в клиенте geth

Для реализации 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
}

Важно! Список доступа формируется до выполнения транзакции и добавляется непосредственно в данные транзакции.

Подробнее про списки доступа можно почитать в этой статье.

Возврат газа при очистке хранилища (refund)

Рассмотрим механизм возврата газа при очистке storage в Ethereum, когда значение слота возвращается к исходному, как это определено в EIP-1283. Логика refund менялась с каждым хард‑форком. Все началась с возмещения 10,000 единиц газа за очистку слота, далее правила изменились в EIP-1283 и были дополнены в EIP-2200:

  1. При замене ненулевого исходного значения (original) на ноль, возврат составляет SSTORE_CLEARS_SCHEDULE (15,000 газа);

  2. Если значение original было нулевым, current - ненулевым, и new - нулевым, возврат равен SSTORE_SET_GAS - SLOAD_GAS (19,900 газа);

  3. При замене ненулевого original значения на другое ненулевое, а затем обратно на original, возврат составляет SSTORE_RESET_GAS - SLOAD_GAS (4,900 газа).

Подробнее обработку таких случаев изучить в тестовых примерах EIP-2200.

EIP-3529: Изменения в механизме возврата газа

EIP-2929 не вносил изменений в механизм возврата газа, но таковые появились в хард-форке London с EIP-3529. Этот EIP пересматривает правила возврата газа за SSTORE и SELFDESTRUCT.

Хронология предложений по расчету газа
Хронология предложений по расчету газа

Ранее, эти возмещения предназначались для стимулирования разработчиков к «хорошей гигиене состояний», то есть к очистке ненужных слотов хранилища и смарт‑контрактов. Однако, на практике это привело к нескольким проблемам:

  1. Проблема GasToken: GasToken позволяет экономить газ в периоды низких комиссий и использовать его в периоды высоких цен, но это также приводит к увеличению размера состояния сети (потому что он использует слоты хранилища, как накопители газа) и неэффективно загружает сеть. Таким образом, правила возврата давали возможность манипулировать газом, влияя на работу всего блокчейна.

    GasToken — смарт‑контракт в сети Ethereum, который позволяет пользователям покупать и продавать газ напрямую, обеспечивая долгосрочный «банкинг» газа, который может помочь защитить пользователей от роста цен на газ.

  2. Увеличение вариативности размера блока: Теоретически, максимальное количество газа, потребляемое в блоке, может быть почти вдвое больше установленного лимита газа из‑за возмещений. Это увеличивает колебания размера блоков и позволяет поддерживать высокое потребление газа на более длительный период, что противоречит целям EIP-1559.

EIP-3529 внес предложения по уменьшению возмещений за операции, чтобы повысить предсказуемость и стабильность экономики газа. Основные изменения:

  1. Удалить возмещение газа за SELFDESTRUCT;

  2. Заменить SSTORE_CLEARS_SCHEDULE (как определено в EIP-2200) на SSTORE_RESET_GAS + ACCESS_LIST_STORAGE_KEY_COST (4,800 газа по состоянию на EIP-2929 + EIP-2930);

  3. Уменьшить максимальное количество газа, возмещаемого после транзакции, до gas_used // MAX_REFUND_QUOTIENT.

    • Примечание: Ранее максимальное количество возмещаемого газа определялось как gas_used // 2. В EIP константе 2 присваивается название MAX_REFUND_QUOTIENT, значение изменяется на 5.

Изменения EIP-3529 в клиенте geth

Проследим изменения 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 (изменения расчета газа)

Тестовые случаи EIP-3529 демонстрируют изменения в возвратах газа до и после его активации. Они представлены в виде двух таблиц, где заметно, что возвраты, ранее составлявшие 15,000 единиц газа, теперь сокращены до 4,800 единиц.

Важно! Эти тесты проведены с предположением, что хранилище уже "прогрето".

Также можно снова обратиться к сайту evm.codes, где представлен калькулятор газа для опкода SSTORE, позволяющий указать три значения (original, current, new) и тип хранилища (warm или cold), чтобы рассчитать потребление и возврат газа. Там же доступно подробное описание правил расчета в зависимости от условий. Как и в прошлый раз важно указать хард‑форк, перед тем как обращаться к описанию опкода.

Стоит отметить, что в будущем правила для динамического расчета газа могут измениться. Однако теперь вы знаете, где искать эти изменения и как интерпретировать их, чтобы понять актуальную стоимость. В Ethereum, подобно юридическим законам реального мира, правила могут устаревать, меняться или претерпевать небольшие корректировки, хотя механизмы этих изменений отличаются от традиционных законодательных процессов.

Intrinsic gas (внутренний газ)

Мы уже рассмотрели расчет и списание газа при выполнении логики смарт-контракта, но существует также понятие внутреннего газа (intrinsic gas), потребляемого перед выполнением этой логики. Важно учитывать, что в некоторых транзакциях вызов смарт-контракта может отсутствовать.

Для понимания составляющих внутреннего газа, следует обратиться к разделу 6 Yellow paper Ethereum. Расчет внутреннего газа представлен формулой g0:

Формула g0 из Ethereum Yerllow paper
Формула g0 из Ethereum Yerllow paper

Для значений G, указанных в формуле, можно обратиться к «Appendix G. Fee Schedule» на 27 странице Yellow paper. Формула внутреннего газа довольно проста, и мы рассмотрим ее детально пошагово:

  1. Расчет газа за 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 единицы газа.

  1. Создание смарт-контракта: Если транзакция включает создание смарт-контракта (поле to равно нулевому адресу), к внутреннему газу добавляется Gtxcreate, равный 32,000 единиц газа.

  2. Базовый газ за транзакцию: Минимальное количество газа, требуемое для любой транзакции, составляет Gtransaction — 21,000 единиц газа. Это базовое значение применяется, например, к простым переводам эфира, где нет дополнительных операций, требующих увеличения газа.

  3. Стоимость доступа к списку: Согласно EIP-2930, внутренний газ также учитывает Gaccesslistaddress (2,400 единиц газа) и Gaccessliststorage (1,900 единиц газа). Эти значения добавляются за каждый адрес и слот, указанные в списке доступа, если транзакция включает предоплату за "прогрев".

Таким образом, как можно видеть, расчет внутреннего газа в Ethereum относительно прост. Для более детального понимания, рекомендуется изучить функцию IntrinsicGas, находящуюся в файле state_transition.go в репозитории go-ethereum.

Общий процесс расчета газа в Ethereum

Давайте соберем всю информацию вместе для полного понимания процесса расчета газа в Ethereum. Все начинается с блока, в котором отслеживается суммарное количество использованного газа по всем транзакциям (gasUsed). Каждая индивидуальная транзакция в блоке проходит через обработку функцией applyTransaction, в ходе которой происходит следующее:

  1. Инициализация счетчиков газа: Первый счетчик (st.gas) отображает доступное количество газа для транзакции и инициализируется ее лимитом газа (gasLimit). Второй счетчик следит за фактически использованным газом;

  2. Авансовый платеж: С баланса отправителя списывается авансовый платеж, равный произведению цены газа (gasPrice) на лимит газа (gasLimit);

  3. Уменьшение лимита газа блока: Общий лимит газа блока уменьшается на величину лимита газа транзакции;

  4. Расчет внутреннего газа: Вычисляется внутренний газ транзакции, включая базовую стоимость и стоимость за каждый байт calldata;

  5. Выполнение транзакции: Функция Call() запускает выполнение транзакции, а при необходимости - и логику смарт-контракта через Run();

  6. Обновление и возврат газа: Счетчик оставшегося газа (st.gas) обновляется на основе данных от Call(). Неиспользованный газ, умноженный на цену газа, возвращается отправителю в ETH. Остаток газа также возвращается в общий пул газа блока.

На приведенной схеме наглядно показан процесс обработки газа на уровне протокола как для отдельных транзакций, так и для всего блока. Более подробно об этом можно прочитать здесь.

Обработка газа на уровне транзакции и блока
Обработка газа на уровне транзакции и блока

Таким образом процесс расчета стоимости газа за транзакцию включает в себя две основные составляющие:

  1. Базовая стоимость газа: Она учитывает внутренний газ транзакции (intrinsic gas), включающий стоимость данных (calldata) и базовые операционные расходы;

  2. Стоимость выполнения смарт-контракта: Это дополнительные расходы газа, связанные с логикой и операциями смарт-контракта.

Заключение

Поздравляю! Это было непросто, но теперь должно быть понятнее как расчитывать газ даже для таких операций, как SSTORE , со сложной динамической частью. Вот тут есть краткая шпаргалка по основным моментам. В следующей статье рассмотрим нововведения хард-форка Dencun, который, в том числе затронул проблемы расчета газа упомянутые в этой статье.

Ссылки

Спасибо за ваши прочтения! Если у вас остались вопросы, буду рад ответить на них в комментариях.

Комментарии (1)


  1. Ak_Koks
    27.03.2024 07:01
    +1

    Хороший пост, лайк