Больше года назад стало известно о планах мессенджера 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, за успешность сборки на других системах ручаться не могу.
-
Скачиваем и распаковываем архив с исходниками. Важно скачивать последнюю версию, так как обратная совместимость на данном этапе не гарантируется.
-
Убеждаемся, что в системе установлены последние версии make, cmake (версии 3.0.2 или выше), OpenSSL (включая заголовочные файлы C), g++ или clang. Мне ничего доустанавливать не пришлось, всё собралось сразу.
-
Предположим, исходники распакованы в папку
~/lite-client
. Отдельно от неё создаём пустую папку для собранного проекта (например,~/liteclient-build
), и из неё (cd ~/liteclient-build
) вызываем команды:cmake ~/lite-client cmake --build . --target test-lite-client
Для сборки интерпретатора языка Fift для смарт-контрактов (о нём ниже), также вызываем
cmake --build . --target fift
-
Скачиваем актуальный конфигурационный файл для подключения к тестовой сети и кладём его в папку с собранным клиентом.
-
Готово, можно запустить клиент:
./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 графическим интерфейсом (впрочем, ожидается, что он и так станет доступен как часть мессенджера).
Автор: deNULL