Обзор низкоуровневых комиссий
Эта страница переведена сообществом на русский язык, но нуждается в улучшениях. Если вы хотите принять участие в переводе свяжитесь с @alexgton.
This section describes instructions and manuals for interacting with TON at a low level.
Здесь вы найдете сырые формулы для расчета комиссий и сборов в TON.
Однако большинство из них уже реализованы через коды операций! Поэтому вы используете их вместо ручных вычислений
В этом документе дается общее представление о комиссиях за транзакции в TON и, в частности, о комиссиях за вычисления для кода FunC. Также есть подробная спецификация в техническом документе TVM.
Транзакции и фазы
Как было описано в обзоре TVM, выполнение транзакции состоит из нескольких фаз. Во время этих фаз могут вычитаться соответствующие комиссии. Существует обзор высокоуровневых комиссий.
Плата за хранение
Валидаторы TON взимают плату за хранение со смарт-контрактов.
Плата за хранение взимается с balance
смарт-контракта на фазе хранения любой транзакции в связи с оплатой за хранение в зависимости от состояния аккаунта
(включая код и данные смарт-контракта, если они есть) до настоящего времени. Даже если контракт получил 1 nanoton, он погасит всю задолженность с момента последней оплаты. В результате смарт-контракт может быть заморожен. Только уникальные ячейки хеша учитываются для комиссий за хранение и пересылку, т. е. 3 одинаковые ячейки хеша считаются как одна. В частности, это дедуплицирует данные: если в разных ветвях есть несколько эквивалентных подячеек, их содержимое сохраняется только один раз.
Важно помнить, что в TON вы платите как за выполнение смарт-контракта, так и за используемое хранилище (см. статью @thedailyton), storage_fee
зависит от размера вашего контракта: количества ячеек и суммы битов из этих ячеек. Это означает, что вам придется платить комиссию за хранение за наличие кошелька TON (даже если он очень-очень маленький).
Если вы не использовали свой кошелек TON в течение значительного периода времени (1 год), вам придется заплатить значительно большую комиссию, чем обычно, поскольку кошелек платит комиссию за отправку и получение транзакций.
Когда сообщение возвращается из контракта, контракт выплачивает свою текущую storage_fee
Формула
Вы можете приблизительно рассчитать плату за хранение для смарт-контрактов, используя эту формулу:
storage_fee = ceil(
(account.bits * bit_price
+ account.cells * cell_price)
* time_delta / 2 ^ 16)
Давайте рассмотрим каждое значение более подробно:
storage_fee
— цена за хранение дляtime_delta
секундaccount.cells
— количество ячеек, используемых смарт-контрактомaccount.bits
— количество бит, используемых смарт-контрактомcell_price
— цена одной ячейкиbit_price
— цена одного бита
И cell_price
, и bit_price
можно получить из конфигурации сети параметр 18.
Текущие значения:
- Воркчейн.
bit_price_ps:1
cell_price_ps:500 - Мастерчейн.
mc_bit_price_ps:1000
mc_cell_price_ps:500000
Пример калькулятора
Вы можете использовать этот скрипт JS для расчета стоимости хранения 1 МБ в воркчейне на 1 год
// Welcome to LIVE editor! // feel free to change any variables // Source code uses RoundUp for the fee amount, so does the calculator function storageFeeCalculator() { const size = 1024 * 1024 * 8; // 1MB in bits const duration = 60 * 60 * 24 * 365; // 1 Year in secs const bit_price_ps = 1; const cell_price_ps = 500; const pricePerSec = size * bit_price_ps + +Math.ceil(size / 1023) * cell_price_ps; let fee = Math.ceil((pricePerSec * duration) / 2 ** 16) * 10 ** -9; let mb = (size / 1024 / 1024 / 8).toFixed(2); let days = Math.floor(duration / (3600 * 24)); let str = `Storage Fee: ${fee} TON (${mb} MB for ${days} days)`; return str; }
Плата за вычисления
Газ
Все затраты на вычисления номинированы в единицах газа. Цена единиц газа определяется этой конфигурацией цепи (Конфигурация 20 для мастерчейна и Конфигурация 21 для бейсчейна) и может быть изменена только по консенсусу валидаторов. Обратите внимание, что в отличие от других систем, пользователь не может устанавливать собственную цену на газ, и нет рынка комиссий.
Текущие настройки в бейсчейне следующие: 1 единица газа стоит 400 nanoton.
Стоимость инструкций TVM
На самом низком уровне (выполнение инструкций TVM) стоимость газа для большинства примитивов
равна базовой цене газа, вычисляемой как P_b := 10 + b + 5r
,
где b
— длина инструкции в битах, а r
— количество ссылок на ячейки, включенных в инструкцию.
Помимо этих основных сборов, появляются следующие сборы:
Инструкция | Цена газа | Описание |
---|---|---|
Создание ячейки | 500 | Операция преобразования строителя в ячейку. |
Анализ ячейки в первый раз | 100 | Операция преобразования ячеек в срезы первый раз в ходе текущей транзакции. |
Повторный анализ ячейки | 25 | Операция преобразования ячеек в срезы, которая уже была проанализирована в ходе той же транзакции. |
Вызов исключения | 50 | |
Операция с кортежем | 1 | Эта цена будет умножена на количество элементов кортежа. |
Неявный переход | 10 | Оплачивается, когда выполняются все инструкции в текущей ячейке продолжения. Однако в этой ячейке продолжения есть ссылки, и поток выполнения переходит к первой ссылке. |
Неявный обратный переход | 5 | Он оплачивается, когда все инструкции в текущем продолжении выполнены, и поток выполнения возвращается к продолжению, из которого бы ло вызвано только что завершенное продолжение. |
Перемещение элементов стека | 1 | Цена за перемещение элементов стека между продолжениями. Будет взиматься соответствующая цена газа за каждый элемент. Однако перемещение первых 32 элементов бесплатно. |
Плата за газ для FunC конструкций
Почти все функции FunC, используемые в этой статье, определены в контракте stdlib.fc стейблкоина (на самом деле stdlib.fc с новыми кодами операций в настоящее время находится в стадии разработки и еще не представлен в репозиториях основной сети, но вы можете использовать stdlib.fc
из исходного кода стейблкоина в качестве ссылки), который сопоставляет функции FunC с инструкциями ассемблера Fift. В свою очередь, инструкции ассемблера Fift сопоставляются с инструкциями битовой последовательности в asm.fif. Поэтому, если вы хотите понять, сколько именно будет стоить вызов инструкции, вам нужно найти представление asm
в stdlib.fc
, затем найти битовую последовательность в asm.fif
и вычислить длину инструкции в битах.
Однако, как правило, сборы, связанные с длиной бит, незначительны по сравнению с сборами, связанными с анализом и созданием ячеек, а также переходами и просто количеством выполненных инструкций.
Поэтому, если вы пытаетесь оптимизировать свой код, начните с оптимизации архитектуры, уменьшения количества операций анализа/создания ячеек, а затем с уменьшения количества переходов.
Операции с ячейками
Просто пример того, как правильная работа ячеек может существенно снизить затраты на газ.
Давайте представим, что вы хотите добавить некоторую закодированную полезную нагрузку к и сходящему сообщению. Простая реализация будет следующей:
slice payload_encoding(int a, int b, int c) {
return
begin_cell().store_uint(a,8)
.store_uint(b,8)
.store_uint(c,8)
.end_cell().begin_parse();
}
() send_message(slice destination) impure {
slice payload = payload_encoding(1, 7, 12);
var msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(destination)
.store_coins(0)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; default message headers (see sending messages page)
.store_uint(0x33bbff77, 32) ;; op-code (see smart-contract guidelines)
.store_uint(cur_lt(), 64) ;; query_id (see smart-contract guidelines)
.store_slice(payload)
.end_cell();
send_raw_message(msg, 64);
}
В чем проблема с этим кодом? payload_encoding
для генерации битовой строки среза, сначала создайте ячейку с помощью end_cell()
(+500 единиц газа). Затем проанализируйте ее begin_parse()
(+100 единиц газа). Тот же код можно написать без этих ненужных операций, изменив некоторые часто используемые типы:
;; we add asm for function which stores one builder to the another, which is absent from stdlib
builder store_builder(builder to, builder what) asm(what to) "STB";
builder payload_encoding(int a, int b, int c) {
return
begin_cell().store_uint(a,8)
.store_uint(b,8)
.store_uint(c,8);
}
() send_message(slice destination) impure {
builder payload = payload_encoding(1, 7, 12);
var msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(destination)
.store_coins(0)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; default message headers (see sending messages page)
.store_uint(0x33bbff77, 32) ;; op-code (see smart-contract guidelines)
.store_uint(cur_lt(), 64) ;; query_id (see smart-contract guidelines)
.store_builder(payload)
.end_cell();
send_raw_message(msg, 64);
}
Передавая битовую строку в другой форме (строитель вместо среза), мы существенно снижаем стоимость вычислений за счет очень небольшого изменения кода.
Inline и inline_refs
По умолчанию, когда у вас есть функция FunC, она получает свой собственный id
, хранящийся в отдельном листе словаря id->function, и когда вы вызываете ее где-то в программе, происходит поиск функции в словаре и последующий переход. Такое поведение оправдано, если ваша функция вызывается из многих мест в коде, и, таким образом, переходы позволяют уменьшить размер кода (сохраняя тело функции один раз). Однако, если функция используется только один или два раза, часто гораздо дешевле объявить эту функцию как inline
или inline_ref
. Модификатор inline
помещает тело функции прямо в код родительской функции, в то время как inline_ref
помещает код функции в ссылку (переход к ссылке все еще намного дешевле, чем поиск и переход к записи словаря).
Словари
Словари в TON представлены как деревья (точнее, DAG) ячеек. Это означает, что если вы ищете, читаете или пишете в словарь, вам нужно разобрать все ячейки соответствующей ветви дерева. Это означает, что
- а) операц ии со словарями не фиксированы в стоимости газа (так как размер и количество узлов в ветви зависят от данного словаря и ключа)
- б) целесообразно оптимизировать использование словаря, используя специальные инструкции, такие как
replace
вместоdelete
иadd
- в) разработчик должен знать об операциях итерации (таких как next и prev), а также об операциях
min_key
/max_key
, чтобы избежать ненужной итерации по всему словарю
Операции со стеком
Обратите внимание, что FunC манипулирует записями стека под капотом. Это означает, что код:
(int a, int b, int c) = some_f();
return (c, b, a);
будет транслироваться в несколько инструкций, которые изменяют порядок элементов в стеке.
Когда количество записей стека значительно (10+), и они активно используются в разных порядках, расходы на операции со стеком могут стать существенными.
Плата за пересылку
Внутренние сообщения определяют ihr_fee
в Toncoin, которая вычитается из стоимости, прикрепленной к сообщению, и присуждается валидаторам целевого шардчейна, если они включают сообщение через механизм IHR. fwd_fee
— это исходная общая плата за пересылку, уплачиваемая с использованием механизма HR; она автоматически вычисляется из 24 и 25 параметров конфигурации и размера сообщения на момент его генерации. Обратите внимание, что общая стоимость, переносимая вновь созданным внутренним исходящим сообщением, равна сумме стоимости, ihr_fee
и fwd_fee
. Эта сумма вычитаетс я из баланса исходного аккаунта. Из этих компонентов только значение ihr_fee
зачисляется на целевой аккаунт при доставке сообщения. fwd_fee
собирается валидаторами на пути HR от источника до пункта назначения, а ihr_fee
либо собирается валидаторами целевого шардчейна (если сообщение доставляется через IHR), либо зачисляется на конечный аккаунт.
IHR
На данный момент (ноябрь 2024 г.) IHR не реализован, и если вы установите ihr_fee
на ненулевое значение, оно всегда будет добавлено к значению сообщения при получении. На данный момент нет практических причин делать это.
Формула
// In_msg and Ext_msg are using the same method of calculation
// It is called import_fee or in_fwd_fee for the Ext_msg
// https://github.com/ton-blockchain/ton/blob/7151ff26279fef6dcfa1f47fc0c5b63677ae2458/crypto/block/transaction.cpp#L2071-L2090
// bits in the root cell of a message are not included in msg.bits (lump_price pays for them)
msg_fwd_fees = (lump_price
+ ceil(
(bit_price * msg.bits + cell_price * msg.cells) / 2^16)
);
ihr_fwd_fees = ceil((msg_fwd_fees * ihr_price_factor) / 2^16);
total_fwd_fees = msg_fwd_fees + ihr_fwd_fees; // ihr_fwd_fees - is 0 for external messages
fwd_fee
= msg_fwd_fees
- action_fee
= 266669 nanoton = 0,000266669 TON
Плата за действие
Плата за действие вычитается из баланса исходного аккаунта во время обработки списка действий, которая происходит после фазы вычислений. Фактически, единственное действие, за которо е вы платите плату за действие, это SENDRAWMSG
. Другие действия, такие как RAWRESERVE
или SETCODE
, не влекут за собой никаких комиссий во время фазы действия.
action_fee = floor((msg_fwd_fees * first_frac)/ 2^16); //internal
action_fee = msg_fwd_fees; //external
first_frac
является частью параметров 24 и 25 (для мастерчейна и воркчейна) блокчейна TON. В настоящее время оба установлены на значение 21845, что означает, что action_fee
составляет примерно треть от msg_fwd_fees
. В случае внешнего действия сообщения SENDRAWMSG
, action_fee
равен msg_fwd_fees
.
Remember that an action register can contain up to 255 actions, which means that all formulas related to fwd_fee
and action_fee
will be computed for each SENDRAWMSG
action, resulting in the following sum:
total_fees = sum(action_fee) + sum(total_fwd_fees);
Начиная с четвертой глобальной версии TON, если действие "отправить сообщение" не выполняется, аккаунт должен оплатить обработку ячеек сообщения, называемую action_fine
.
fine_per_cell = floor((cell_price >> 16) / 4)
max_cells = floor(remaining_balance / fine_per_cell)
action_fine = fine_per_cell * min(max_cells, cells_in_msg);
Файл конфигурации платы
Все платы указаны в nanoton или nanoton, умноженных на 2^16, чтобы сохранить точность при использовании целого числа и могут быть изменены. Файл конфигурации отображает текущую стоимость платы.
- storage_fees = p18
- in_fwd_fees = p24, p25
- computation_fees = p20, p21
- action_fees = p24, p25
- out_fwd_fees = p24, p25
A direct link to the mainnet live config file
В образовательных целях пример старого
Ссылки
- На основе @thedailyton статья от 24.07*