Telegram Open Network (TON): Отправка сообщений между смарт-контрактами

Введение

Данная статья представляет перевод из документации информации об отправке сообщений в блокчейне TON и их сериализации.
Источник: https://docs.ton.org/develop/smart-contracts/messages

Сборка, парсинг и отправка сообщений в блокчейне TON описываются двумя схемами: 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();

Сообщение хранится в ячейках, и каждая ячейка может содержать до 1023 бит. Если вам нужно хранить больше данных, вы должны разделить их на части и хранить в ссылочных ячейках. Размеры некоторых полей могут быть переменными, поэтому нужно обращать внимание на размер сообщения и размещение его частей в ячейках.

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

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

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

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

Структура сообщения

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

Схема 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, если X сериализован в отдельную ячейку.
  • когда у нас есть поле 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. Его первый бит является префиксом со значением 0. Затем идут три однобитных флага: отключен ли Instant Hypercube Routing (в настоящее время всегда true), должно ли сообщение быть bounced (вызывать исключение, если возникают ошибки во время его обработки), и является ли само сообщение результатом bounced. Затем сериализуются исходный и адресат назначения, за которыми следует значение сообщения и четыре целых числа, связанных с комиссиями за пересылку сообщений и временем.

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

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

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();

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

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

() 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);
}

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

Что это?

  • Первый бит равен 0 – это префикс, который указывает, что это int_msg_info.
  • Затем идут 3 бита: 1, 1 и 0, означающие, что мгновенная маршрутизация по Instant Hypercube Routing отключена, сообщения могут быть bounced, и сообщение само по себе не является результатом bounced.
  • Затем должен быть указан адрес отправителя, однако, так как он все равно будет перезаписан с тем же эффектом, там может быть сохранен любой допустимый адрес. Самая короткая сериализация допустимого адреса – это 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, сообщение может нести дополнительный словарь extra-currencies. Однако в настоящее время мы можем это проигнорировать и просто предположить, что значение сообщения сериализуется как “количество нанотонн в виде переменной целой” и “0 – бит пустого словаря”.

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

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

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

Полная схема

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

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

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

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

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

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

Рейтинг
( 1 оценка, среднее 5 из 5 )
Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: