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

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

    Для складання інтерпретатора мови Fift для смарт-контрактів (про нього нижче), також викликаємо

    cmake --build . --target fift

  4. Завантажуємо актуальний конфігураційний файл для підключення до тестової мережі та кладемо його в папку із зібраним клієнтом.

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

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

Якщо все зроблено правильно, ви повинні побачити щось таке:

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

Доступних команд, як бачимо, небагато:
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 (мабуть, від чисельного п'ятий, За аналогією з 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 (Telegram Open Network) та нова мова Fift для смарт-контрактів

Ось і все тепер ми вміємо створювати смарт-контракти в TON і відправляти до них запити. Як бачимо, нинішньої функціональності вже достатньо, щоб, наприклад, зробити більш доброзичливий гаманець з графічним інтерфейсом (втім, очікується, що він і так стане доступним як частина месенджера).

Тільки зареєстровані користувачі можуть брати участь в опитуванні. Увійдіть, будь ласка.

Чи зацікавлені ви у продовженні статей з розбором TON, TVM, Fift?

  • Так, чекаю завершення циклу статей із загальним оглядом TON

  • Так, цікаво почитати докладніше про мову Fift

  • Так, хочу дізнатися більше про TON Virtual Machine та асемблері для нього

  • Ні, не цікаво нічого з цього

Проголосували 39 користувачів. Утрималися 12 користувачів.

Як ви ставитеся до планів Telegram із запуску TON?

  • Покладаю великі надії на цей проект

  • Просто з цікавістю стежу за його розвитком

  • Налаштований скептично, сумніваюся у його успіху

  • Схильний вважати цю ініціативу провальною, непотрібною широким масам

Проголосували 47 користувачів. Утрималися 12 користувачів.

Джерело: habr.com

Додати коментар або відгук