Telegram Open Network (TON): погружение в блокчейн для разработков

TON как блокчейн нового поколения

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

Как правило, асинхронность, многопоточность и акторная модель в языках программировани, являются следующем этапом их эволюции, на смену синхронности. В свою очередь Telegram Open Network (TON), как блокчейн следующего поколения, имеет асинхронную акторную архитектуру и решает проблемы, характерные в синхронном блокчейне.

Шаблон поведения “Актер” (Actor)

Для лучшего понимания, разберем пример смарт-контракта в TON. По сути это сущность, которая обладает такими характеристиками как address, code, data, balance и некоторыми другими. Другими словами это объект, который имеет некоторое хранилище и поведение, такое как:

  • происходит какое-либо событие (например, контракт получает сообщение)
  • контракт обрабатывает это событие в соответствии с заложенными в него свойствами и логикой посредством исполнения кода (code) контракта на на виртуальной машине TVM.
  • контракт может модифицировать свойства (code, data и что-то еще)
  • контракт может генерировать исходящие сообщения.
  • контракт ожидает следующего события

Комбинация всех перечисленных шагов называется транзакцией (transaction). Причем очень важно то, чтобы события в транзакции обрабатывались последовательно и не прерывали друг друга. Этот шаблон поведения называется “Актер” (Actor).

Последовательность транзакций Tx1 -> Tx2 -> Tx3 -> …. называется цепочкой (chain). И в контексте рассматриваемого шаблона поведения она называется AccountChain, чтобы подчеркнуть, что цепочка транзакций относится к одному аккаунту.

Узлам (nodes), которые обрабатывают транзакции, время от времени необходимо координировать состояние смарт-контракт (для достижения консенсуса). Поэтому транзакции группируются [Tx1 -> Tx2] -> [Tx3 -> Tx4 -> Tx5] -> [] -> [Tx6] Группировка транзакций не влияет на их последовательность, она просто оборачивает их в блоки.

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

Шарды (Shards)

Теперь рассмотрим множество счетов (Accounts). Мы можем получить несколько AccountChain и хранить их вместе. Такой набор называется ShardChain. Точно также мы можем группировать ShardChain на блоки ShardBlocks, которые представляют собой агрегацию отдельных AccountBlocks.

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

Аналогично можно объединять более мелкие ShardChain в крупные шарды, если они стали слишком не занятыми.

Из вышесказанного очевидны два предельных случая, – когда шард содержит один аккаунт и не может быть более разделен, и когда шард содержит все аккаунты.

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

Чтобы сделать процесс разделения и слияния цепочек однозначно определенным, объединение аккаунтов в шарды основывается на битовом представлении адресов аккаунтов. Таким образом, все аккаунты в ShardChain будут иметь одинаковый двоичный префикс (например, все адреса будут начинаться на 0b00101).

Blockchain

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

Masterchain

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

В TON для этого используется специальная цепочка MasterChain. Блоки мастерчейна содержат дополнительную информацию (последние хэши блоков) обо всех других цепочках в системе. Таким образом, любой наблюдательно однозначно определяет состояние всей мультичейн систем на каком-то блоке мастерчейна.

Базовая концпеция и сущности

Ячейка (Cell)

Все данные в TON хранятся в ячейках памяти (далее Cell). Cell – это область памяти, которая может вмещать в себя до 1023 бита (не байта!) данных и до 4 ссылок на другие Cell, причем:

  • Биты и ссылки не смешиваются, а хранятся отдельно!
  • Циклические ссылки запрещены. Т.е если у ячейки есть дочерние ячейки, то ни одна дочерняя ячейка не может ссылаться на свою родительскую ячейку. Таким образом, все ячейки составляют ориентированный ациклический граф (DAG).

Типы ячеек (Cell types)

Помимо данных и ссылок, тип ячейки закодирован целым числом -1. . . 255.

На текущий момент есть 5 типов ячеек: обычная (тип: -1) и 4 экзотические, которые включают:

  • Ячейка с обрезанным ответвлением (тип: 1).
  • Библиотечно-справочная ячейка (тип: 2).
  • Ячейка с Merkle защитой (тип: 3).
  • Ячейка с Merkle обновлением (тип: 4).

Разновидность ячеек (Cell flavors)

Ячейка сама по себе является непрозрачным объектом, оптимизированным для компактного хранения данных. В частности, используется принцип дедубликации данных, т.е.  если есть несколько эквивалентных связанных ячеек в разных узлах, то значение хранится лишь в одной ячейки, а остальные ссылаются на нее. Непрозрачность означает, что ячейка не может быть изменена или прочитана, таким образом есть 2 дополнительные разновидности ячеек:

  • Builder – частично построенные ячейки, для которых могут быть определены быстрые операции добавления строк битов, целых чисел, других ячеек и ссылок на другие ячейки.
  • Slice – представляет “рассеченную” ячейку, которая представляет собой либо остаток частично проанализированной ячейки, либо значение (подъячейку) находящееся внутри такой ячейки и извлеченное из нее с помощью инструкциии синтетического анализа.
  • Continuation – представляет собой ячейку которая включает op-codes (инструкции) для TVM.

Сериализация данных в ячейке (Serialization of data to cells)

Любой объект в TON (сообщение, очередь сообщений, блок, состояние всего блокчейна, код контракта и данные сериализуются в ячейку.

Способ сериализации описывается схемой TL-B: формальное описание того, как этот объект можно сериализовать в Builder или как разобрать объект данного типа из среза. TL-B для ячеек такой же, как и TL длля ProtoBuf для потоков байтов.

TL-B

TL-B означает «Типированный язык бинарных данных». Используется для описания схемы (де)сериализации объектов в cell. Подробная и полная схема TL-B для всех объектов в TON см. на гитхаб: https://github.com/ton-blockchain/ton/blob/master/crypto/block/block.tlb.

Схема

Схема TL-B состоит из базовых элементов, каждое из которых описывает конструктор для некоторого типа данных. Например, тип Bool может иметь два конструктора для значений true и false.

Типичные базовые элементы TL-B показаны ниже:

bool_false$0 = Bool;
bool_true$1 = Bool;

unary_zero$0 = Unary ~0;
unary_succ$1 {n:#} x:(Unary ~n) = Unary ~(n + 1);

acc_trans#5 account_addr:bits256
   transactions:(HashmapAug 64 ^Transaction CurrencyCollection)
   state_update:^(HASH_UPDATE Account)
   = AccountBlock;

Каждый базовый элемент TL-B состоит из:

  • Конструктор: имя конструктора, за которым сразу следует необязательный тег конструктора.
  • Список явных и неявных определений полей, разделенных пробелами (“ “, “\n” и т. д.)
  • знак =
  • (опционально) Имя типа данных.
    Пример: два конструктора (с разными бинарными префиксами) для типа Bool.
    bool_false$0 = Bool;
    bool_true$1 = Bool;

Конструктор

Конструктор указывается в виде имя_конструктора[разделитель, тег].

Причем, имя_конструктора может содержать только следующие символы [A-z0-9_] и задаваться в snake_case.

После имя_конструктора может стоять разделитель. Отсутствие разделителя означает, что тег будет вычисляться автоматически как 32-битная crc32-сумма объявлений конструктора.

Если разделитель присутствует, он может принимать два значения:

  • # – означает, что тег будет задан в шестнадцатеричной форме,
  • $ – означает, что тег будет задан в двоичный тег.

После обоих разделителей может стоять символ подчеркивания _, что означает, что тег пуст.

Существует также имя_конструктора _ (называемое анонимным), которое означает, что существует только один анонимный конструктор с пустым тегом для данного типа.

Ниже вы можете найти таблицу с возможными определениями тегов.

Конструктор

Тег

_

пустой тег для анонимного конструктора

some

автоматически вычисляемый 32-битный тег

some#bba

12-битный тег равный 0b101110111010

some$01011

5-битный тег равный 0b01011

some#_

пустой тег

some$_

пустой тег

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

Способы определения полей

Явное определение полей

Всякое определение поля имеет формат ident : type-expr, где ident — это идентификатор с именем поля (заменяется символом подчеркивания _ для анонимных полей), а type-expr — это тип поля. Тип поля представляет собой выражение, которое может включать простые типы или параметризованные типы с подходящими параметрами. Переменные — т. е. (идентификаторы) ранее определенных полей типов # (натуральные числа) или Type (тип типов) — могут использоваться в качестве параметров для параметризованных типов.

Существует несколько предопределенных типов:

  • # – означает беззнаковое 32-битное число
  • ## N – то же, что и uintN – означает беззнаковое N-битное число
  • #<= N – означает число от 0 до N (включая оба). Такое число хранится в ceil(log2(N+1)) битах.
  • N * Bit – означает N-битный срез
  • ^Cell – означает произвольную ячейку в ссылке
  • ^[field_definitions] – означает, что определения полей хранятся в указанной ячейке.
  • Type – обозначает произвольный тип (но присутствует только в неявных определениях).

type-expr обычно состоит из (необязательно параметризованного) Type вида: last_trans_lt:uint64 или _:StateInit. Но иногда type-expr также может содержать условия, в таком случае type-expr состоит из ident, :, condition, ?, type. Если ident становится false, то соответствующее поле не указывается. Например, prev:n?^(ProofChain n) означает, что поле prev представлено только для объектов, когда n>0.

Неявное определение полей

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

nothing$0 {X:Type} = Maybe X;
just$1 {X:Type} value:X = Maybe X;

означает, что некий конструктор может определять поле как var:(Maybe #). В этом случае переменная будет сериализована либо как 1 бит с сериализацией # (uint32) в случае, если var присутствует, либо как 0 бит, если var отсутствует. Таким образом, Maybe объявляется как C++-подобный шаблонный тип для произвольного типа X. Однако, если Maybe будет объявлен как nothing$0 {X:#} = Maybe X;, это будет означать, что Maybe объявлен для произвольного числа (не полностью произвольного типа Х).

Типы

Наименование типа должно состоять из символов [A-z0-9_] и задаваться в CamelCase. Тип может иметь один или несколько параметров.

Некоторые переменные могут иметь префикс тильды (~). Это означает, что до десериализации точное значение этой переменной неизвестно, а будет вычисляться во время десериализации.

Рассмотрим пример:

unary_zero$0 = Unary ~0;
unary_succ$1 {n:#} x:(Unary ~n) = Unary ~(n + 1);

Здесь мы хотим десериализовать объект Unary ~N из слайса, содержащего битовую строку 0b1111111100101.

Когда мы говорим, что хотим десериализовать Unary ~N, это означает, что мы еще не знаем, десериализуем ли мы Unary 0, Unary 7 или Unary 1020. Поэтому, мы начинаем со значения 0b1111111100101 и сравниваем его с префиксами конструктора 0b0 для unary_zero и 0b1 для unary_succ. Мы видим, что у нас есть unary_succ, но опять же значение N нельзя вычесть, вместо этого мы должны получить его из десериализации переменной x. Эта переменная имеет тип Unary ~(N-1), и значение N-1 может быть вычтено из десериализации остальных битов в slice. Получаем остальные биты slice и пытаемся десериализовать Unary ~(N-1) и снова видим тег unary_succ.

Таким образом, мы рекурсивно погружаемся в Unary, пока не доберемся до Unary ~(N-8). На этом уровне мы видим, что остальная часть среза начинается с тега unary_zero и, таким образом, представляет собой объект Unary 0. Вернувшись назад, мы можем получить, что изначально у нас был объект Unary 8. Итак, после десериализации Unary ~N из Slice(0b1111111100101) мы получаем объект Unary 8 и остальные Slice(0b0101), из которых можно десериализовать последующие переменные конструктора.

Ограничения

Некоторые неявные поля могут содержать ограничения, например {n <= m}. Это означает, что ранее определенные переменные n и m должны удовлетворять соответствующему неравенству. Это неравенство является неотъемлемым свойством конструктора. Это следует проверять при сериализации, кроме того, объекты с переменными, не удовлетворяющими ограничениям, недействительны.

Пример конструкторов с ограничениями:

hml_short$0 {m:#} {n:#} len:(Unary ~n) {n <= m} s:(n * Bit) = HmLabel ~n m;
hml_long$10 {m:#} n:(#<= m) s:(n * Bit) = HmLabel ~n m;
hml_same$11 {m:#} v:Bit n:(#<= m) = HmLabel ~n m;

Комментарии

Схемы TL-B поддерживают C-подобные комментарии:

/* 
This is a
multiline
comment 
*/

// This is one line comment

(Де)сереализация

Учитывая схему TL-B, любой объект можно сериализовать в builder и десериализовать из slice. В частности, когда мы десериализуем объект, нам нужно начать с определения соответствующего конструктора по тегу, а затем десериализовать переменные одну за другой слева направо (рекурсивно переходя к сериализации переменных, которые сами являются объектами TL-B). Во время сериализации мы идем другим путем, находя и записывая в тег построителя, который соответствует данному объекту типа, а затем продолжаем запись слева для каждой переменной.

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

TVM

В этом разделе представлен общий обзор того, как TVM выполняет транзакции.

Транзакции и их фазы

Когда на аккаунте в одной из цепочек TON происходит какое-то событие, оно вызывает транзакцию. Наиболее частым событием является «получение какого-то сообщения», но вообще говоря, это могут быть tick-tock, merge, split и другие события.

Каждая транзакция состоит из 5 фаз:

  • Storage phase (Этап хранения) – на этом этапе рассчитываются комиссии за хранение, начисленные по контракту в связи с занимаемом им местом хранением в состоянии цепочки.
  • Credit phase (Фаза кредита) – на этой фазе рассчитывается баланс контракта в отношении (возможного) значения входящего сообщения и взимаемой платы за хранение.
  • Compute phase (Этап вычисления) — на этом этапе выполняется TVM (см. ниже), результатом выполнения TVM является агрегация exit_code, действий (серийный список действий), gas_details, new_storage и некоторых других.
  • Action phase (Фаза действия) — если фаза вычислений прошла успешно, на этой фазе обрабатываются действия из фазы вычислений. В частности, действия могут включать отправку сообщений, обновление кода смарт-контракта, обновление библиотек и т. д. Обратите внимание, что некоторые действия могут завершиться ошибкой во время обработки (например, мы пытаемся отправить сообщение с большим количеством TON, чем есть в контракте), в этом случае вся транзакция может вернуться или это действие может быть пропущено (это зависит от режима действий, другими словами контракт может отправить сообщение в режиме отправить-или-вернуть или в режиме попробовать-отправить-если-не-игнорировать).
  • Bounce phase (Фаза отказов) – если фаза вычисления не удалась (она вернула exit_code >= 2), в этой фазе формируется сообщение о возврате для транзакций, инициированных входящим сообщением.

Этап вычислений

На этом этапе происходит выполнение TVM.

Состояние TVM

В любой момент состояние TVM полностью определяется 6 свойствами состояния:

  • Stack (Стек) – описание см. ниже.
  • Control registers (Регистры управления) – описание см. ниже, говоря простыми словами, до 16 переменных, которые могут быть напрямую установлены и прочитаны во время выполнения.
  • Current continuation (Текущее продолжение) – объект, описывающий последовательность инструкций, которая выполняется в данный момент.
  • Current codepage (Текущая кодовая страница) – говоря простым языком, версия TVM, которая работает в данный момент.
  • Gas limits (Лимиты газа) – набор из 4 целочисленных значений: текущий лимит газа gl, максимальный лимит газа gm, остаток газа gr и кредит газа gc.
  • Library context (Контекст библиотеки) — хэш-карта библиотек, которые могут быть вызваны TVM.

TVM — это стековая машина.

TVM — это стековая машина, работающая по принципу «последним пришел – первым вышел».

В стеке могут храниться 7 типов переменных:

  • Integer – 257-битные целые числа со знаком.
  • Tuple – упорядоченный набор до 255 элементов, имеющих произвольные типы значений, возможно различные.
  • Null.

И четыре различных представления cell:

  • Cell (Ячейка) – базовая (возможно, вложенная) непрозрачная структура, используемая блокчейном TON для хранения всех данных.
  • Slice (Срез )- специальный объект, позволяющий читать из ячейки.
  • Builder (Билдер) – специальный объект, позволяющий создавать новые ячейки.
  • Continuation (Продолжение) – специальный объект, позволяющий использовать ячейку как источник инструкций TVM.

Регистры управления

  • c0 — Содержит следующее продолжение или продолжение возврата (аналогично адресу возврата подпрограммы в обычных проектах). Это значение должно быть с типом Continuation.
  • c1 — Содержит альтернативное (возвратное) продолжение; это значение должно быть с типом Continuation.
  • c2 — Содержит обработчик исключений. Это значение является типом Continuation, вызываемым всякий раз, когда возникает исключение.
  • c3 — Вспомогательный регистр, содержит текущий словарь, по сути хэш-карту, содержащую код всех функций, используемых в программе. Это значение должно быть типом Continuation.
  • c4 — Содержит корень постоянных данных или просто раздел данных контракта. Это значение является типом Cell.
  • c5 — Содержит выходные действия. Это значение является типом Cell.
  • c7 — Содержит корень временных данных. Это тип Tuple.

Инициализация TVM

Таким образом, когда выполнение транзакции переходит в фазу вычислений, TVM инициализируется, а затем выполняет команды (коды операций) из текущего Continuation до тех пор, пока не останется больше команд для выполнения (и не будет Continuation для обратных переходов).

Для обычных транзакций, вызванных сообщением, начальное состояние выглядит следующим образом:

  • Stack: 5 элементов помещаются в стек:
    • Баланс смарт-контракта (после оплаты стоимости входящего сообщения) передается как Integer значение в nanoTON.
    • Баланс входящего сообщения m передается как Integer значение в nanoTON.
    • Входящее сообщение передается как Cell, содержащая сериализованное значение типа Message X, где X — тип тела сообщения.
    • Тело входящего сообщения, равное значению поля body m, передается как Slice.
    • Селектор функций s, передается как Integer со значением: 0 для tx, вызванного внутренними сообщениями или -1 для внешних и т. д. Вообще говоря, это целое число, которое указывает, какое событие вызвало транзакцию.
  • Current continuation: продолжение, сконвертированное из раздела code смарт-контракта.
  • Control registers инициализируются следующим образом: c0, c1, c2 и c3 пусты; c4 содержит ячейку из раздела данных смарт-контракта; c5 содержит пустой список (он сериализуется как ячейка, содержащая последнее действие в списке плюс ссылка на предыдущее) выходных действий; c7 инициализируется как кортеж с некоторыми базовыми данными контекста блокчейна, такими как время, глобальная конфигурация, block_data и т. д.
  • Current codepage установлена ​​на значение по умолчанию (cp=0).
  • Gas limits инициализируются в соответствии с результатами Credit phase.
  • Library context инициализируется в результате слияния этой коллекции библиотеки смарт-контрактов, коллекции глобальной библиотеки masterchain и коллекции библиотеки входящих сообщений (если есть).

Инструкции TVM

Список инструкций TVM можно найти в официальной документации: https://ton.org/docs/#/smart-contracts/tvm-instructions/instructions

Результат выполнения TVM

Помимо кода возврата и данных о потребленном газе, TVM косвенно выводит следующие данные:

  • регистр c4cell, которая будет храниться как новые данные смарт-контракта (если выполнение не будет отменено на этом или последующих этапах).
  • регистр c5 – (список выходных действий) cell с последним действием в списке и ссылка на cell с предыдущим действием (рекурсивно)

Все остальные значения регистров будут проигнорированы.

Обратите внимание, что поскольку существует ограничение на максимальную глубину cell <1024 и, в частности, ограничение на глубину c4 и c5 <=512. А также, есть ограничение на количество выходных действий в одном tx <=255. То если контракту понадобится отправить больше, – он может отправить сообщение с запросом continue_sending самому себе и отправить все необходимые сообщения в нескольких последующих транзакциях.

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

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