Децентрализованные сети
Криптография
Программирование
Распределённые системы
Криптовалюты
27 мая

Тестовый клиент TON (Telegram Open Network) и новый язык Fift для смарт-контрактов

Больше года назад стало известно о планах мессенджера Telegram выпустить собственную децентрализованную сеть Telegram Open Network. Тогда стал доступен объемный технический документ, который, предположительно, был написан Николаем Дуровым и описывал структуру будущей сети. Для тех, кто пропустил — рекомендую ознакомиться с моим пересказом этого документа (часть 1, часть 2; третья часть, увы, всё ещё пылится в черновиках).


С тех пор никаких значимых новостей о статусе разработки TON не было, пока пару дней назад (в одном из неофициальных каналов) не появилась ссылка на страницу https://test.ton.org/download.html, где размещены:


ton-test-liteclient-full.tar.xz — исходники лёгкого клиента для тестовой сети TON;
ton-lite-client-test1.config.json — конфигурационный файл для подключения к тестовой сети;
README — информация о сборке и запуске клиента;
HOWTO — пошаговая инструкция о создании смарт-контракта с помощью клиента;
ton.pdf — обновлённый документ (от 2 марта 2019 г.) с техническим обзором сети TON;
tvm.pdf — техническое описание TVM (TON Virtual Machine, виртуальной машины TON);
tblkch.pdf — техническое описание блокчейна TON;
fiftbase.pdf — описание нового языка Fift, предназначенного для создания смарт-контрактов в TON.


Повторюсь, официальных подтверждений страницы и всех этих документов со стороны Телеграма не было, но объем этих материалов делает их достаточно правдоподобными. Запуск опубликованного клиента совершайте на свой страх и риск.


Сборка тестового клиента


Для начала попробуем собрать и запустить тестовый клиент — благо, README подробно описывает этот несложный процесс. Я буду это делать на примере macOS 10.14.5, за успешность сборки на других системах ручаться не могу.


  1. Скачиваем и распаковываем архив с исходниками. Важно скачивать последнюю версию, так как обратная совместимость на данном этапе не гарантируется.


  2. Убеждаемся, что в системе установлены последние версии make, cmake (версии 3.0.2 или выше), OpenSSL (включая заголовочные файлы C), g++ или clang. Мне ничего доустанавливать не пришлось, всё собралось сразу.


  3. Предположим, исходники распакованы в папку ~/lite-client. Отдельно от неё создаём пустую папку для собранного проекта (например, ~/liteclient-build), и из неё (cd ~/liteclient-build) вызываем команды:


    cmake ~/lite-client
    cmake --build . --target test-lite-client

    Успешная сборка клиента

    Для сборки интерпретатора языка Fift для смарт-контрактов (о нём ниже), также вызываем


    cmake --build . --target fift

  4. Скачиваем актуальный конфигурационный файл для подключения к тестовой сети и кладём его в папку с собранным клиентом.


  5. Готово, можно запустить клиент:


    ./test-lite-client -C ton-lite-client-test1.config.json


Если всё сделано правильно, то вы должны увидеть что-то такое:


Запуск клиента


Доступных команд, как видим, немного:


help — вывести этот список команд;
quit — выйти;
time — показать текущее время на сервере;
status — показать состояние подключения и локальной БД;
last — обновить состояние блокчейна (загрузить последний блок). Эту команду важно выполнять перед любыми запросами, чтобы быть уверенным, что вы видите именно актуальное состояние сети.
sendfile <filename> — загрузить локальный файл в сеть TON. Так происходит взаимодействие с сетью — в том числе, например, создание новых смарт-контрактов и запросы на перевод средств между аккаунтами;
getaccount <address> — показать текущее (на момент выполнения команды last) состояние аккаунта с указанным адресом;
privkey <filename> — загрузить приватный ключ из локального файла.


Если при запуске клиента передать ему папку с помощью опции -D, то он будет складывать в неё последний блок мастерчейна:


./test-lite-client -C ton-lite-client-test1.config.json -D ~/ton-db-dir

Теперь можем перейти к более интересным вещам — изучить язык Fift, попробовать скомпилировать смарт-контракт (например, создать тестовый кошелёк), загрузить его в сеть и попробовать перевод средств между аккаунтами.


Язык Fift


Из документа fiftbase.pdf можно узнать, что для создания смарт-контрактов команда Telegram создала новый стековый язык Fift (видимо, от числительного fifth, по аналогии с Forth — языком, с которым у Fift много общего).


Документ достаточно объемный, на 87 страниц, и я не стану подробно пересказывать его содержание в рамках этой статьи (как минимум, потому что сам не закончил его чтение :). Остановлюсь на основных моментах и приведу пару примеров кода на этом языке.


На базовом уровне, синтаксис Фифта достаточно прост: его код состоит из слов, как правило, разделённых пробелами или переводами строк (частный случай: некоторые слова не требуют разделителя после себя). Любое слово — это регистро-зависимая последовательность символов, которой соответствует некоторое определение (грубо говоря, то, что интерпретатор должен сделать, когда встречает это слово). Если определения слова нет, интерпретатор пытается распарсить его как число и положить на стек. Кстати, числа тут — внезапно — 257-битные целые, а дробных нет совсем — точнее, они сразу превращаются в пару целых, образующих числитель и знаменатель рациональной дроби.


Слова, как правило, взаимодействуют со значениями, лежащими на верхушке стека. Отдельный тип слов — префиксный — использует не стек, а последующие за ними символы из исходного файла. Например, так реализованы строковые литералы — символ «кавычка» (") является префиксным словом, которое ищет следующую (закрывающую) кавычку, и помещает строку между ними на стек. Подобным же образом ведут себя однострочные (//) и многострочные (/*) комментарии.


На этом почти всё внутреннее устройство языка заканчивается. Всё остальное (включая управляющие конструкции) определено как слова (либо внутренние, такие как арифметические операции и определение новых слов; либо определённые в «стандартной библиотеке» Fift.fif, которая лежит в папке crypto/fift в исходниках).


Простой пример программы на Fift:


{ dup =: x dup * =: y } : setxy
3 setxy x . y . x y + .
7 setxy x . y . x y + .

В первой строчке определяется новое слово setxy (обратите внимание на префикс {, который создает блок до закрывающего } и префикс :, который собственно определяет слово). setxy берёт число с вершины стека, определяет (или переопределяет) его как глобальную константу x, а квадрат этого числа — как константу y (учитывая, что значения констант можно переопределять, я бы скорее назвал их переменными, но я следую именованию в языке).


В следующих двух строчках на стек кладётся число, вызывается setxy, затем выводятся значения констант x, y (для вывода используется слово .), обе константы помещаются на стек, суммируются и результат тоже выводится. В результате мы увидим:


3 9 12 ok
7 49 56 ok

(Строчку «ok» выводит интерпретатор, когда заканчивает обрабатывать текущую строку в интерактивном режиме ввода)


Ну и полноценный пример кода:


"Asm.fif" include

-1 constant wc  // create a wallet in workchain -1 (masterchain)

// Create new simple wallet
<{  SETCP0 DUP IFNOTRET INC 32 THROWIF  // return if recv_internal, fail unless recv_external
    512 INT LDSLICEX DUP 32 PLDU   // sign cs cnt
    c4 PUSHCTR CTOS 32 LDU 256 LDU ENDS  // sign cs cnt cnt' pubk
    s1 s2 XCPU            // sign cs cnt pubk cnt' cnt
    EQUAL 33 THROWIFNOT   // ( seqno mismatch? )
    s2 PUSH HASHSU        // sign cs cnt pubk hash
    s0 s4 s4 XC2PU        // pubk cs cnt hash sign pubk
    CHKSIGNU              // pubk cs cnt ?
    34 THROWIFNOT         // signature mismatch
    ACCEPT
    SWAP 32 LDU NIP 
    DUP SREFS IF:<{
      8 LDU LDREF         // pubk cnt mode msg cs
      s0 s2 XCHG SENDRAWMSG  // pubk cnt cs ; ( message sent )
    }>
    ENDS
    INC NEWC 32 STU 256 STU ENDC c4 POPCTR
}>c
// code
<b 0 32 u, 
   newkeypair swap dup constant wallet_pk 
   "new-wallet.pk" B>file
   B, 
b> // data
// no libraries
<b b{00110} s, rot ref, swap ref, b>  // create StateInit
dup ."StateInit: " <s csr. cr
dup hash dup constant wallet_addr
."new wallet address = " wc . .": " dup x. cr
wc over 7 smca>$ type cr
256 u>B "new-wallet.addr" B>file
<b 0 32 u, b>
dup ."signing message: " <s csr. cr
dup hash wallet_pk ed25519_sign_uint rot
<b b{1000100} s, wc 8 i, wallet_addr 256 u, b{000010} s, swap <s s, b{0} s, swap B, swap <s s, b>
dup ."External message for initialization is " <s csr. cr
2 boc+>B dup Bx. cr
"new-wallet-query.boc" tuck B>file
."(Saved to file " type .")" cr

Этот страшновато выглядящий файл предназначен для создания смарт-контракта — он будет помещён в файл new-wallet-query.boc после выполнения. Обратите внимание, что тут используется ещё один, ассемблерный язык для TON Virtual Machine (на нём я не буду останавливаться подробно), инструкции которого и будут помещены в блокчейн.


Таким образом, ассемблер для TVM написан на Fift — исходники этого ассемблера находятся в файле crypto/fift/Asm.fif и подключаются в начале приведённого выше куска кода.


Что я могу сказать, видимо, Николай Дуров просто любит создавать новые языки программирования :)


Создание смарт-контракта и взаимодействие с TON


Итак, предположим, мы собрали клиент TON и интерпретатор Fift, как описано выше, и познакомились с языком. Как теперь создать смарт-контракт? Об этом рассказывается в файлике HOWTO, приложенном к исходникам.


Аккаунты в TON


Как я описывал в обзоре TON, эта сеть содержит больше одного блокчейна — есть один общий, т.н. «мастерчейн», а также произвольное количество дополнительных «воркчейнов», идентифицируемых 32-битным числом. Мастерчейн имеет идентификатор -1, кроме него так же может использоваться «базовый» воркчейн с идентификатором 0. У каждого воркчейна может быть своя конфигурация. Внутренне каждый воркчейн дробится на шардчейны, но это уже деталь реализации, которую необязательно держать в голове.


В пределах одного воркчейна хранится множество аккаунтов, у которых есть свои идентификаторы account_id. Для мастерчейна и нулевого воркчейна они имеют длину 256 бит. Таким образом, идентификатор аккаунта записывается, например, так:


-1:8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d

Это «сырой» формат: сначала идентификатор воркчейна, затем двоеточие, и идентификатор аккаунта в шестнадцатеричной записи.


Кроме того, есть укороченный формат — номер воркчейна и адрес аккаунта кодируются в бинарном виде, к ним дописывается контрольная сумма и всё это кодируется в Base64:


Ef+BVndbeTJeXWLnQtm5bDC2UVpc0vH2TF2ksZPAPwcODSkb

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


getaccount -1:8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d

Получим примерно такой ответ:


[ 3][t 2][1558746708.815218925][test-lite-client.cpp:631][!testnode]    requesting account state for -1:8156775B79325E5D62E742D9B96C30B6515A5CD2F1F64C5DA4B193C03F070E0D
[ 3][t 2][1558746708.858564138][test-lite-client.cpp:652][!testnode]    got account state for -1:8156775B79325E5D62E742D9B96C30B6515A5CD2F1F64C5DA4B193C03F070E0D with respect to blocks (-1,8000000000000000,72355):F566005749C1B97F18EDE013EBA7A054B9014961BC1AD91F475B9082919A2296:1BD5DE54333164025EE39D389ECE2E93DA2871DA616D488253953E52B50DC03F and (-1,8000000000000000,72355):F566005749C1B97F18EDE013EBA7A054B9014961BC1AD91F475B9082919A2296:1BD5DE54333164025EE39D389ECE2E93DA2871DA616D488253953E52B50DC03F
account state is (account
  addr:(addr_std
    anycast:nothing workchain_id:-1 address:x8156775B79325E5D62E742D9B96C30B6515A5CD2F1F64C5DA4B193C03F070E0D)
  storage_stat:(storage_info
    used:(storage_used
      cells:(var_uint len:1 value:3)
      bits:(var_uint len:2 value:539)
      public_cells:(var_uint len:0 value:0)) last_paid:0
    due_payment:nothing)
  storage:(account_storage last_trans_lt:74208000003
    balance:(currencies
      grams:(nanograms
        amount:(var_uint len:7 value:999928362430000))
      other:(extra_currencies
        dict:hme_empty))
    state:(account_active
      (
        split_depth:nothing
        special:nothing
        code:(just
          value:(raw@^Cell 
            x{}
             x{FF0020DDA4F260D31F01ED44D0D31FD166BAF2A1F80001D307D4D1821804A817C80073FB0201FB00A4C8CB1FC9ED54}
            ))
        data:(just
          value:(raw@^Cell 
            x{}
             x{0000000D}
            ))
        library:hme_empty))))
x{CFF8156775B79325E5D62E742D9B96C30B6515A5CD2F1F64C5DA4B193C03F070E0D2068086C000000000000000451C90E00DC0E35B7DB5FB8C134_}
 x{FF0020DDA4F260D31F01ED44D0D31FD166BAF2A1F80001D307D4D1821804A817C80073FB0201FB00A4C8CB1FC9ED54}
 x{0000000D}

Видим структуру, которая хранится в DHT указанного воркчейна. Например, в поле storage.balance находится текущий баланс аккаунта, в storage.state.code — код смарт-контракта, а в storage.state.data — его текущие данные. Обратите внимание, что хранилище данных TON — Cell, ячейки — является древовидным, у каждой ячейки могут быть как свои данные, так и дочерние ячейки. Это показано в виде отступов в последних строчках.


Сборка смарт-контракта


Теперь давайте создадим сами такую структуру (она называется BOC — bag of cells) с помощью языка Fift. К счастью, самостоятельно писать смарт-контракт не придётся — в папке crypto/block из архива с исходниками есть файл new-wallet.fif, который поможет создать нам новый кошелёк. Скопируем его в папку с собранным клиентом (~/liteclient-build, если вы действовали по инструкции выше). Его же содержимое я приводил выше в качестве примера кода на Fift.


Выполняем этот файл следующим образом:


./crypto/fift -I"<source-directory>/crypto/fift" new-wallet.fif

Здесь <source-directory> надо заменить на путь к распакованным исходникам (символ "~" тут, к сожалению, использовать нельзя, нужен полный путь). Вместо использования ключа -I можно определить переменную окружения FIFTPATH и поместить этот путь в неё.


Так как Fift мы запустили с именем файла new-wallet.fif, он выполнит его и завершится. Если имя файла опустить, то можно поиграть с интерпретатором в интерактивном режиме.


В консоль после выполнения должно вывестись что-то такое:


StateInit: x{34_}
 x{FF0020DDA4F260810200D71820D70B1FED44D0D31FD3FFD15112BAF2A122F901541044F910F2A2F80001D31F3120D74A96D307D402FB00DED1A4C8CB1FCBFFC9ED54}
 x{0000000055375F730EDC2292E8CB15C42E8036EE9C25AA958EE002D2DE48A205E3A3426B}

new wallet address = -1 : 4fcd520b8fcca096b567d734be3528edc6bed005f6930a9ec9ac1aa714f211f2 
0f9PzVILj8yglrVn1zS-NSjtxr7QBfaTCp7JrBqnFPIR8nhZ
signing message: x{00000000}

External message for initialization is x{89FEE120E20C7E953E31546F64C23CD654002C1AA919ADD24DB12DDF85C6F3B58AE41198A28AD8DAF3B9588E7A629252BA3DB88F030D00BC1016110B2073359EAC3C13823C53245B65D056F2C070B940CDA09789585935C7ABA4D2AD4BED139281CFA1200000001_}
 x{FF0020DDA4F260810200D71820D70B1FED44D0D31FD3FFD15112BAF2A122F901541044F910F2A2F80001D31F3120D74A96D307D402FB00DED1A4C8CB1FCBFFC9ED54}
 x{0000000055375F730EDC2292E8CB15C42E8036EE9C25AA958EE002D2DE48A205E3A3426B}

B5EE9C724104030100000000D60002CF89FEE120E20C7E953E31546F64C23CD654002C1AA919ADD24DB12DDF85C6F3B58AE41198A28AD8DAF3B9588E7A629252BA3DB88F030D00BC1016110B2073359EAC3C13823C53245B65D056F2C070B940CDA09789585935C7ABA4D2AD4BED139281CFA1200000001001020084FF0020DDA4F260810200D71820D70B1FED44D0D31FD3FFD15112BAF2A122F901541044F910F2A2F80001D31F3120D74A96D307D402FB00DED1A4C8CB1FCBFFC9ED5400480000000055375F730EDC2292E8CB15C42E8036EE9C25AA958EE002D2DE48A205E3A3426B6290698B
(Saved to file new-wallet-query.boc)

Это означает, что кошелёк с идентификатором -1:4fcd520b8fcca096b567d734be3528edc6bed005f6930a9ec9ac1aa714f211f2 (или, что то же самое, 0f9PzVILj8yglrVn1zS-NSjtxr7QBfaTCp7JrBqnFPIR8nhZ) успешно создан. Соответствующий ему код окажется в файле new-wallet-query.boc, его адрес — в new-wallet.addr, а приватный ключ — в new-wallet.pk (будьте осторожны — повторный запуск скрипта перезапишет эти файлы).


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


В рабочем режиме эта проблема решится покупкой грамов на бирже (или переводом с другого кошелька). Ну а в нынешнем тестовом режиме заведён специальный смарт-контракт, у которого можно попросить до 20 грам просто так.


Формирование запроса к чужому смарт-контракту


Запрос к смарт-контракту, раздающему грамы налево и направо, делаем так. Во всё той же папке crypto/block находим файл testgiver.fif:


// "testgiver.addr" file>B 256 B>u@ 
0x8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d
dup constant wallet_addr ."Test giver address = " x. cr

0x4fcd520b8fcca096b567d734be3528edc6bed005f6930a9ec9ac1aa714f211f2
constant dest_addr

-1 constant wc
0x00000011 constant seqno

1000000000 constant Gram
{ Gram swap */ } : Gram*/

6.666 Gram*/ constant amount

// b x --> b'  ( serializes a Gram amount )
{ -1 { 1+ 2dup 8 * ufits } until
  rot over 4 u, -rot 8 * u, } : Gram, 

// create a message (NB: 01b00.., b = bounce)
<b b{010000100} s, wc 8 i, dest_addr 256 u, amount Gram, 0 9 64 32 + + 1+ 1+ u, "GIFT" $, b>
<b seqno 32 u, 1 8 u, swap ref, b>
dup ."enveloping message: " <s csr. cr
<b b{1000100} s, wc 8 i, wallet_addr 256 u, 0 Gram, b{00} s,
   swap <s s, b>
dup ."resulting external message: " <s csr. cr
2 boc+>B dup Bx. cr
"wallet-query.boc" B>file

Его тоже сохраним в папку с собранным клиентом, но поправим пятую строчку — перед строчкой "constant dest_addr". Заменим её на адрес того кошелька, который вы создали до этого (полный, не сокращённый). "-1:" в начале писать не нужно, вместо этого в начале поставьте "0x".


Ещё можно поменять строку 6.666 Gram*/ constant amount — это сумма в грамах, которую вы запрашиваете (не больше 20). Даже если указываете целое число, оставьте десятичную точку.


Наконец, нужно поправить строку 0x00000011 constant seqno. Первое число тут — это текущий sequence number, который хранится в аккаунте, выдающем грамы. Откуда его взять? Как говорилось выше, запустите клиент и выполните:


last
getaccount -1:8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d

В самом конце в данных смарт-контракта будет


...
x{FF0020DDA4F260D31F01ED44D0D31FD166BAF2A1F80001D307D4D1821804A817C80073FB0201FB00A4C8CB1FC9ED54}
 x{0000000D}

Число 0000000D (у вас оно будет больше) и есть sequence number, который надо подставить в testgiver.fif.


Всё, сохраняем файл и запускаем (./crypto/fift testgiver.fif). На выходе получим файл wallet-query.boc. Это и есть сформированное сообщение к чужому смарт-контракту — просьба «переведи столько-то грам на такой-то аккаунт».


С помощью клиента загружаем его в сеть:


> sendfile wallet-query.boc
[ 1][t 1][1558747399.456575155][test-lite-client.cpp:577][!testnode]    sending query from file wallet-query.boc
[ 3][t 2][1558747399.500236034][test-lite-client.cpp:587][!query]   external message status is 1

Если теперь вызвать last, а затем снова запросить статус аккаунта, у которого мы попросили грамы, то мы должны увидеть, что его sequence number увеличился на единичку — это значит, что он отправил деньги нашему аккаунту.


Остался последний шаг — загружаем код нашего кошелька (баланс его уже пополнен, но без кода смарт-контракта мы не сможем им управлять). Выполняем sendfile new-wallet-query.boc — и всё, у вас есть собственный кошелёк в сети TON (пусть и пока лишь тестовой).


Создание исходящих транзакций


Чтобы переводить деньги с баланса созданного аккаунта, есть файл crypto/block/wallet.fif, который тоже нужно поместить в папку с собранным клиентом.


Аналогично предыдущим шагам, в нём нужно поправить сумму, которую вы переводите, адрес получателя (dest_addr), и seqno вашего кошелька (он равен 1 после инициализации кошелька и увеличивается на 1 после каждой исходящей транзакции — вы сможете увидеть его, запросив состояние своего аккаунта). Для тестов можете использовать, например, мой кошелёк — 0x4fcd520b8fcca096b567d734be3528edc6bed005f6930a9ec9ac1aa714f211f2.


При запуске (./crypto/fift wallet.fif) скрипт возьмёт адрес вашего кошелька (откуда вы переводите) и его приватный ключ из файлов new-wallet.addr и new-wallet.pk, а полученное сообщение запишет в new-wallet-query.boc.


Как и раньше, чтобы непосредственно выполнить транзакцию, вызываем sendfile new-wallet-query.boc в клиенте. После этого не забываем обновить состояние блокчейна (last) и проверяем, что баланс и seqno нашего кошелька изменились (getaccount <account_id>).


Описание аккаунта


Вот и всё, теперь мы умеем создавать смарт-контракты в TON и отправлять к ним запросы. Как видим, нынешней функциональности уже достаточно, чтобы, например, сделать более дружелюбный кошелёк c графическим интерфейсом (впрочем, ожидается, что он и так станет доступен как часть мессенджера).

Заинтересованы ли вы в продолжении статей с разбором TON, TVM, Fift?
81% Да, жду завершения цикла статей с общим обзором TON 372
43.7% Да, интересно почитать подробнее о языке Fift 201
47% Да, хочу узнать больше о TON Virtual Machine и ассемблере для него 216
11.7% Нет, не интересно ничего из этого 54
Проголосовали 459 пользователей. Воздержался 101 пользователь.
Как вы относитесь к планам Telegram по запуску TON?
34.5% Возлагаю большие надежды на этот проект 193
49.5% Просто с интересом слежу за его развитием 277
10.7% Настроен скептически, сомневаюсь в его успехе 60
5.1% Склонен считать эту инициативу провальной, ненужной широким массам 29
Проголосовали 559 пользователей. Воздержались 76 пользователей.
+88
30,7k 113
Поддержать автора
Комментарии 99