Перейти к основному содержимому

Отправка сообщений

warning

Эта страница переведена сообществом на русский язык, но нуждается в улучшениях. Если вы хотите принять участие в переводе свяжитесь с @alexgton.

Составление, анализ и отправка сообщений лежат на пересечении схем TL-B, фаз транзакций и TVM.

Действительно, FunC предоставляет функцию send_raw_message, которая ожидает сериализованное сообщение в качестве аргумента.

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

  cell msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(addr)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_slice(message_body)
.end_cell();

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

Иногда в документации может встречаться слово gram, в основном в примерах кода; это просто устаревшее название toncoin.

Давайте разберемся!

Типы сообщений

Существует три типа сообщений:

  • внешние — сообщения, отправляемые извне блокчейна в смарт-контракт внутри блокчейна. Такие сообщения должны быть явно приняты смарт-контрактами во время так называемого credit_gas. Если сообщение не принято, узел не должен принимать его в блок или передавать его другим узлам.
  • внутренние — сообщения, отправляемые из одной сущности блокчейна в другую. Такие сообщения, в отличие от внешних, могут нести некоторое количество TON и окупать себя. Таким образом, смарт-контракты, получающие такие сообщения, могут не принять их. В этом случае газ будет вычтен из стоимости сообщения.
  • логи — сообщения, отправляемые из сущности блокчейна во внешний мир. Как правило, не существует механизма отправки таких сообщений из блокчейна. Фактически, хотя все узлы в сети имеют консенсус относительно того, было ли создано сообщение или нет, нет никаких правил относительно того, как их обрабатывать. Логи могут быть напрямую отправлены в /dev/null, записаны на диск, сохранены в индексированной базе данных или даже отправлены не блокчейн-средствами (электронная почта/telegram/смс), все это остается на усмотрение данного узла.

Макет сообщения

Начнем с внутреннего макета сообщения.

Схема TL-B, описывающая сообщения, которые могут быть отправлены смарт-контрактами, выглядит следующим образом:

message$_ {X:Type} info:CommonMsgInfoRelaxed 
init:(Maybe (Either StateInit ^StateInit))
body:(Either X ^X) = MessageRelaxed X;

Давайте сформулируем это словами. Сериализация любого сообщения состоит из трех полей: info (своего рода заголовок, который описывает источник, адресата и другие метаданные), init (поле, которое требуется только для инициализации сообщений) и body (полезная нагрузка сообщения).

Maybe и Either и другие типы выражений означают следующее:

  • когда у нас есть поле info:CommonMsgInfoRelaxed, это означает, что сериализация CommonMsgInfoRelaxed внедряется непосредственно в ячейку сериализации.
  • когда у нас есть поле body:(Either X ^X), это означает, что когда мы (де)сериализуем некоторый тип X, мы сначала ставим один бит either, который равен 0, если X сериализуется в ту же ячейку, или 1, если он сериализуется в отдельную ячейку.
  • когда у нас есть поле init:(Maybe (Either StateInit ^StateInit)), это означает, что мы сначала ставим 0 или 1 в зависимости от того, пусто это поле или нет; и если он не пустой, мы сериализуем Either StateInit ^StateInit (опять же, ставим один бит either, который равен 0, если StateInit сериализуется в ту же ячейку, или 1, если он сериализуется в отдельную ячейку).

Макет CommonMsgInfoRelaxed выглядит следующим образом:

int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
src:MsgAddress dest:MsgAddressInt
value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams
created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed;

ext_out_msg_info$11 src:MsgAddress dest:MsgAddressExt
created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed;

Давайте пока сосредоточимся на int_msg_info. Он начинается с 1-битного префикса 0, затем идут три 1-битных флага, а именно, отключена ли мгновенная маршрутизация гиперкуба (в настоящее время всегда true), следует ли отклонять сообщение, если во время его обработки возникли ошибки, является ли само сообщение результатом отклонения. Затем сериализуются адреса источника и назначения, за которыми следует значение сообщения и четыре целых числа, связанных с платой за пересылку сообщения и временем.

Если сообщение отправляется из смарт-контракта, некоторые из этих полей будут перезаписаны на правильные значения. В частности, валидатор перезапишет bounced, src, ihr_fee, fwd_fee, created_lt и created_at. Это означает две вещи: во-первых, другой смарт-контракт во время обработки сообщения может доверять этим полям (отправитель не может подделать исходный адрес, флаг bounced и т. д.); и во-вторых, во время сериализации мы можем поместить в эти поля любые допустимые значения (в любом случае эти значения будут перезаписаны).

Прямая сериализация сообщения будет выглядеть следующим образом:

  var msg = begin_cell()
.store_uint(0, 1) ;; tag
.store_uint(1, 1) ;; ihr_disabled
.store_uint(1, 1) ;; allow bounces
.store_uint(0, 1) ;; not bounced itself
.store_slice(source)
.store_slice(destination)
;; serialize CurrencyCollection (see below)
.store_coins(amount)
.store_dict(extra_currencies)
.store_coins(0) ;; ihr_fee
.store_coins(fwd_value) ;; fwd_fee
.store_uint(cur_lt(), 64) ;; lt of transaction
.store_uint(now(), 32) ;; unixtime of transaction
.store_uint(0, 1) ;; no init-field flag (Maybe)
.store_uint(0, 1) ;; inplace message body flag (Either)
.store_slice(msg_body)
.end_cell();

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

() send_message_back(addr, ans_tag, query_id, body, grams, mode) impure inline_ref {
;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000
var msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(addr)
.store_coins(grams)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_uint(ans_tag, 32)
.store_uint(query_id, 64);
if (body >= 0) {
msg~store_uint(body, 32);
}
send_raw_message(msg.end_cell(), mode);
}

Сначала он помещает значение 0x18 в 6 бит, что помещается в 0b011000. Что это?

  • Первый бит - 0 — префикс 1 бит, который указывает, что это int_msg_info.

  • Затем идут 3 бита 1, 1 и 0, что означает, что мгновенная маршрутизация гиперкуба отключена, сообщения могут быть отклонены, и это сообщение не является результатом самого отклонения.

  • Затем должен быть адрес отправителя, однако, поскольку он в любом случае будет перезаписан с тем же эффектом, любой допустимый адрес может быть сохранен там. Самая короткая сериализация допустимого адреса — это addr_none, и она сериализуется как двухбитная строка 00.

Таким образом, .store_uint(0x18, 6) — это оптимизированный способ сериализации тега и первых 4 полей.

Следующая строка сериализует адрес назначения.

Затем мы должны сериализовать значения. Обычно значение сообщения — это объект CurrencyCollection со следующей схемой:

nanograms$_ amount:(VarUInteger 16) = Grams;

extra_currencies$_ dict:(HashmapE 32 (VarUInteger 32))
= ExtraCurrencyCollection;

currencies$_ grams:Grams other:ExtraCurrencyCollection
= CurrencyCollection;

Эта схема означает, что в дополнение к значению TON сообщение может содержать словарь дополнительных дополнительных валют. Однако в настоящее время мы можем пренебречь этим и просто предположить, что значение сообщения сериализуется как "количество nanotons как переменное целое число" и "0 - пустой бит словаря".

Действительно, в коде выборщика выше мы сериализуем количество монет через .store_coins(toncoins), но затем просто помещаем строку нулей длиной, равной 1 + 4 + 4 + 64 + 32 + 1 + 1. Что это такое?

  • Первый бит обозначает пустой словарь дополнительных валют.
  • Затем у нас есть два 4-битных поля. Они кодируют 0 как VarUInteger 16. Фактически, поскольку ihr_fee и fwd_fee будут перезаписаны, мы также можем поставить там нули.
  • Затем мы помещаем ноль в поля created_lt и created_at. Эти поля также будут перезаписаны; Однако, в отличие от комиссий, эти поля имеют фиксированную длину и, таким образом, кодируются как строки, длиной 64 и 32 бита.
  • (мы уже сериализовали заголовок сообщения и перешли в init/body в этот момент)
  • Следующий нулевой бит означает, что поля init нет.
  • Последний нулевой бит означает, что msg_body будет сериализован на месте.
  • После этого кодируется тело сообщения (с произвольной компоновкой).

Таким образом, вместо индивидуальной сериализации 14 параметров мы выполняем 4 примитива сериализации.

Полная схема

Полная схема компоновки сообщения и компоновка всех составляющих полей (а также схема ВСЕХ объектов в TON) представлены в block.tlb.

Размер сообщения

размер ячейки

Обратите внимание, что любая ячейка может содержать до 1023 бит. Если вам нужно сохранить больше данных, вы должны разбить их на части и сохранить в ссылочных ячейках.

Если, например, размер тела вашего сообщения составляет 900 бит, вы не можете сохранить его в той же ячейке, что и заголовок сообщения. Действительно, в дополнение к полям заголовка сообщения общий размер ячейки будет превышать 1023 бит, и во время сериализации возникнет исключение переполнение ячейки. В этом случае вместо 0, что означает "установить флаг в теле сообщения (либо)", должно быть 1, а тело сообщения должно храниться в ссылочной ячейке.

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

Например, MsgAddress может быть представлен четырьмя конструкторами: addr_none, addr_std, addr_extern, addr_var с длиной от 2 бит (для addr_none) до 586 бит (для addr_var в самой большой форме). То же самое относится к количеству nanoton, которое сериализуется как VarUInteger 16. Это означает, что 4 бита указывают на длину байта целого числа, а затем указывают предыдущие байты для самого целого числа. Таким образом, 0 nanoton будут сериализованы как 0b0000 (4 бита, которые кодируют строку байтов нулевой длины, а затем ноль байтов), а 100.000.000 ТОНН (или 10000000000000000000000 nanoton) будут сериализованы как 0b10000000000101100011010001010111100010111000101000000000000000 (0b1000 обозначает 8 байтов, а затем сами 8 байтов).

размер сообщения

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

Режимы сообщений

Как вы могли заметить, мы отправляем сообщения с send_raw_message, который, помимо потребления самого сообщения, также принимает режим. Этот режим используется для определения режима отправки сообщений, включая необходимость отдельной оплаты топлива и способ обработки ошибок. Когда виртуальная машина TON (TVM) анализирует и обрабатывает сообщения, она выполняет дифференцированную обработку в зависимости от значения режима. Легко спутать то, что значение параметра режима имеет две переменные, а именно режим и флаг. Режим и флаг имеют разные функции:

  • режим: определяет базовое поведение при отправке сообщения, например, следует ли переносить баланс, следует ли ждать результатов обработки сообщения и т. д. Различные значения режима представляют разные характеристики отправки, и разные значения можно комбинировать для удовлетворения конкретных требований отправки.
  • флаг: как дополнение к режиму, он используется для настройки определенного поведения сообщения, например, отдельной оплаты комиссий за перевод или игнорирования ошибок обработки. Флаг добавляется к режиму для создания окончательного режима отправки сообщения.

При использовании функции send_raw_message важно выбрать соответствующую комбинацию режима и флага для ваших нужд. Чтобы выяснить, какой режим лучше всего подходит для ваших нужд, взгляните на следующую таблицу:

РежимОписание
0Обычное сообщение
64Перенести все оставшееся значение входящего сообщения в дополнение к значению, изначально указанному в новом сообщении
128Перенести весь оставшийся баланс текущего смарт-контракта вместо значения, изначально указанного в сообщении
ФлагОписание
+1Платите комиссию за перевод отдельно от стоимости сообщения
+2Игнорируйте некоторые ошибки, возникающие при обработке этого сообщения на этапе действия (см. примечание ниже)
+16В случае сбоя действия - транзакция аннулируется. Если используется +2, это не влияет ни на что.
+32Текущий аккаунт должен быть уничтожен, если его итоговый баланс равен нулю (часто используется с режимом 128)
флаг +2
  1. Недостаточно Toncoin:
  • Недостаточно стоимости для перевода с сообщением (вся стоимость входящего сообщения была израсходована).
  • Недостаточно средств для обработки сообщения.
  • Недостаточно стоимости, прикрепленной к сообщению, для оплаты комиссий за пересылку.
  • Недостаточно дополнительной валюты для отправки с сообщением.
  • Недостаточно средств для оплаты исходящего внешнего сообщения.
  1. Сообщение слишком большое (проверьте размер сообщения для получения дополнительной информации).
  2. Сообщение имеет слишком большую глубину Меркла.

Однако он не игнорирует ошибки в следующих сценариях:

  1. Сообщение имеет недопустимый формат.
  2. Режим сообщения включает как 64, так и 128 модификаций.
  3. Исходящее сообщение имеет недопустимые библиотеки в StateInit.
  4. Внешнее сообщение не является обычным или включает флаг +16 или +32 или оба
флаг +16

В противном случае он обработает фазу credit перед фазой storage.

Проверьте исходный код с проверкой флага bounce-enable

warning
  1. Флаг +16 - не использовать во внешних сообщениях (например, на кошельках), так как нет отправителя, который мог бы получить отклоненное сообщение.
  2. Флаг +2 - важно во внешних сообщениях (например, на кошельках).

Пример с вариантами использования

Давайте рассмотрим пример, чтобы сделать его более понятным. Представим ситуацию, когда у нас на балансе смарт-контракта 100 Toncoin, и мы получаем внутреннее сообщение с 50 Toncoin и отправляем сообщение с 20 Toncoin, общая комиссия составляет 3 Toncoin.

ВАЖНО: Результат случаев ошибки описывается, когда произошла ошибка.

КейсРежим и флагиКодРезультат
Отправить обычное сообщениеmode = 0, no flagsend_raw_message(msg, 0)balance - 100 + 50 - 20 = 130, send - 20 - 3 = 17
Отправить обычное сообщение, если при обработке действия произошла ошибка, не откатывать транзакцию и игнорировать ееmode = 0, flag = 2send_raw_message(msg, 2)balance - 100 + 50, send - 0
Отправить обычное сообщение, если при обработке действия произошла ошибка - откатить сообщение в дополнение к откату транзакцииmode = 0, flag = 16send_raw_message(msg, 16)balance - 100 + 50 = 167 + 17 (отклонено), send - 20 - 3 = bounce message with 17
Отправить обычное сообщение и оплатить комиссию за перевод отдельноmode = 0, flag = 1send_raw_message(msg, 1)balance - 100 + 50 - 20 - 3 = 127, send - 20
Отправить обычное сообщение и оплатить комиссию за перевод отдельно, если при обработке действия произошла ошибка - откатить сообщение в дополнение к откату транзакцииmode = 0, flags = 1 + 16send_raw_message(msg, 17)balance - 100 + 50 - 20 - 3 = 127 + 20 (отклонено), send - 20 = bounce сообщение с 20
Перенести всю оставшуюся стоимость входящего сообщения в дополнение к изначально указанной стоимости в новом сообщенииmode = 64, flag = 0send_raw_message(msg, 64)balance - 100 - 20 = 80, send - 20 + 50 - 3 = 67
Перенести всю оставшуюся стоимость входящего сообщения в дополнение к изначально указанной стоимости в новом сообщении и оплатить комиссию за перевод отдельноmode = 64, flag = 1send_raw_message(msg, 65)balance - 100 - 20 - 3 = 77, send - 20 + 50 = 70
Перенести всю оставшуюся стоимость входящего сообщения в дополнение к стоимости, изначально указанной в новом сообщении, и отдельно оплатить комиссию за перевод, если произошла ошибка при обработке действия - отклонить сообщение в дополнение к откату транзакцииmode = 64, flags = 1 + 16send_raw_message(msg, 81)balance - 100 - 20 - 3 = 77 + 70 (отклонено), send - 20 + 50 = bounce сообщение с 70
Отправить все полученные токены вместе с балансом контрактаmode = 128, flag = 0send_raw_message(msg, 128)balance - 0, send - 100 + 50 - 3 = 147
Отправить все полученные токены вместе с балансом контракта, если произошла ошибка при обработке действия - отклонить сообщение в дополнение к откату транзакцииmode = 128, flag = 16send_raw_message(msg, 144)balance - 0 + 147 (отклонено), send - 100 + 50 - 3 = bounce сообщение с 147
Отправить все полученные токены вместе с балансом контракта и уничтожить смарт-контрактmode = 128, flag = 32send_raw_message(msg, 160)balance - 0, send - 100 + 50 - 3 = 147
Отправить все полученные токены вместе с балансом контракта и уничтожить смарт-контракт, если произошла ошибка при обработке действия - отклонить сообщение в дополнение к откату транзакции. ВАЖНО: Избегайте такого поведения, поскольку возврат средств будет осуществляться на уже удаленный контракт.mode = 128, flag = 32 + 16send_raw_message(msg, 176)balance - 0 + 147 (отклонено), send - 100 + 50 - 3 = bounce сообщение с 147