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