Хабрахабр

Тестовый клиент 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.

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

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

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

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

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

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

    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

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

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

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

: setxy
3 setxy x . y . x y + .
7 setxy x . y . x y + .

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

В результате мы увидим: В следующих двух строчках на стек кладётся число, вызывается 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

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

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

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

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

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

Аккаунты в TON

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

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

-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}

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

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

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

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

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

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

Если имя файла опустить, то можно поиграть с интерпретатором в интерактивном режиме. Так как 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)

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

Теперь его нужно загрузить в сеть. Конечно, сеть 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".

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

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

last
getaccount -1:8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d

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

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

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

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

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

> 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, который тоже нужно поместить в папку с собранным клиентом.

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

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

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

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть