Про те як написати та опублікувати смарт-контракт у Telegram Open Network (TON)

Про те як написати та опублікувати смарт-контракт у TON

Про що ця стаття?

У статті я розповім про те, як взяв участь у першому (з двох) конкурсі Telegram з блокчейну, не посів призового місця і вирішив зафіксувати досвід у статті, щоб він не канув у Лету і, можливо, допоміг комусь.

Так як мені не хотілося писати абстрактний код, а зробити щось робоче, для статті я написав смарт-контракт моментальну лотерею та сайт, який показує дані смарт-контракту безпосередньо з TON без використання проміжних сховищ.

Стаття буде корисною тим, хто хоче зробити свій перший смарт-контракт у TON, але не знає з чого почати.

На прикладі лотереї я пройду від встановлення оточення до публікації смарт-контракту, взаємодії з ним та напишу сайт для отримання та публікації даних.

Про участь у конкурсі

У жовтні минулого року Telegram оголосив конкурс із блокчейну з новими мовами Fift и FunC. Потрібно було на вибір написати будь-які із п'яти запропонованих смарт-контрактів. Я вважав, що буде непогано зайнятися чимось незвичним, вивчити мову і зробити щось, навіть якщо в майбутньому не доведеться писати щось ще. Плюс тема постійно на слуху.

Варто сказати, що досвіду розробки смарт-контрактів я не мав.

Я планував брати участь до кінця поки виходить і після написати оглядову статтю, але зафейлился відразу на першому. Я написав гаманець з мульти-підписом на FunC і він загалом працював. За основу взяв смарт-контракт на Solidity.

На той момент я порахував, цього достатньо, щоб зайняти хоча б якесь призове місце. У результаті близько 40 із 60 учасників стали призерами і мене серед них не було. Загалом у цьому нічого страшного, але мене напружила одна річ. На момент оголошення результатів ревью з тестом до мого контракту не було зроблено, я запитав у учасників у чаті, чи є хто ще у кого його немає, таких не було.

Мабуть, звернувши увагу на мої повідомлення через два дні судді опублікували коментар і я так і не зрозумів, вони випадково пропустили мій смарт-контракт під час суддівства або просто порахували, що він настільки поганий, що не потребує коментарів. Я запитав сторінку, але відповіді не отримав. Хоча хтось судив — не секрет, писати особисті повідомлення я вважав зайвим.

Часу на розуміння було витрачено багато, тому було вирішено написати статтю. Оскільки інформації поки що не дуже багато, то стаття допоможе заощадити час усім зацікавленим.

Концепт роботи смарт-контрактів у TON

Перш ніж щось писати, треба розібратися з якого боку взагалі підійти до цієї штуки. Тому зараз я розповім із яких частин система складається. Точніше, які частини потрібно знати, щоб написати хоча б якийсь робочий контракт.

Ми зосередимося на написанні смарт-контракту та роботі з TON Virtual Machine (TVM), Fift и FunCтому стаття більше схожа на опис розробки звичайної програми. На тому, як працює сама платформа, тут зупинятися не будемо.

Взагалі про те, як працює TVM та мова Fift Є хороша офіційна документація. Під час участі у конкурсі і зараз, під час написання поточного контракту, я часто звертався до неї.

Основна мова якою пишуться смарт-контракти — FunC. Документації щодо нього на даний момент немає, тому щоб щось написати треба вивчати приклади смарт-контрактів з офіційного репозиторію та саму реалізацію мови там же, плюс можна дивитися приклади смарт-контрактів за минулі два конкурси. Посилання наприкінці статті.

Припустимо, ми вже написали смарт-контракт на FunC, після цього ми компілюємо код Fift-асемблер.

Скомпільований смартконтракт залишається опублікувати. Для цього потрібно написати функцію на Fift, який на вхід прийматиме код смарт-контракту та ще деякі параметри, а на виході вийде файл із розширенням .boc (що означає "bag of cells"), і, залежно від того як напишемо, приватний ключ та адресу, що генерується на основі коду смарт-контракту. На адресу смарт-контракту, який ще не опубліковано, вже можна відправляти грами.

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

Після того як ми зрозуміли, як публікується код, далі стає простіше. Ми приблизно знаємо, що хочемо написати і як працюватиме наша програма. І під час написання шукаємо, як це вже реалізовано в існуючих смарт-контрактах, або заглядаємо в код реалізації Fift и FunC в офіційному репозиторії, чи дивимося на офіційної документації.

Дуже часто я шукав за ключовими словами в Telegram чаті, де зібралися всі учасники конкурсу і співробітники Telegram у тому числі, так вийшло, що під час конкурсу всі зібралися саме там і почали обговорювати Fift і FunC. Посилання наприкінці статті.

Час перейти від теорії до практики.

Підготовка оточення для роботи з TON

Все що буде описано в статті я робив на MacOS і перевіряв ще раз в чистій Ubuntu 18.04 LTS на Docker.

Перше що потрібно зробити завантажити та встановити lite-client за допомогою якого можна надсилати запити в TON.

Інструкція на офіційному сайті досить докладно та зрозуміло описує процес встановлення та опускає деякі деталі. Тут ми слідуємо інструкції попутно встановлюючи відсутні залежності. Я не став сам компілювати кожен проект і встановлював із офіційного репозиторію Ubuntu (на MacOS я використав brew).

apt -y install git 
apt -y install wget 
apt -y install cmake 
apt -y install g++ 
apt -y install zlib1g-dev 
apt -y install libssl-dev 

Після того, як всі залежності встановлені можна встановити lite-client, Fift, FunC.

Спочатку клонуємо репозиторій TON разом із залежностями. Для зручності все робитимемо в папці ~/TON.

cd ~/TON
git clone https://github.com/ton-blockchain/ton.git
cd ./ton
git submodule update --init --recursive

У репозитортії також зберігаються реалзіації Fift и FunC.

Наразі ми готові зібрати проект. Код репозиторію схильний до папки ~/TON/ton. У ~/TON створюємо папку build і збираємо у ній проект.

mkdir ~/TON/build 
cd ~/TON/build
cmake ../ton

Тому що ми збираємося писати смарт-контракт, нам потрібен не тільки lite-client, Але й Fift с FunCтому компілюємо все. Не швидкий процес тому чекаємо.

cmake --build . --target lite-client
cmake --build . --target fift
cmake --build . --target func

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

wget https://test.ton.org/ton-lite-client-test1.config.json

Робимо перші запити в TON

Тепер запустимо lite-client.

cd ~/TON/build
./lite-client/lite-client -C ton-lite-client-test1.config.json

Якщо збірка пройшла успішно, то після запуску ви побачите лог підключення лайт клієнта до ноди.

[ 1][t 2][1582054822.963129282][lite-client.h:201][!testnode]   conn ready
[ 2][t 2][1582054823.085654020][lite-client.cpp:277][!testnode] server version is 1.1, capabilities 7
[ 3][t 2][1582054823.085725069][lite-client.cpp:286][!testnode] server time is 1582054823 (delta 0)
...

Можна виконати команду help та подивитися які команди доступні.

help

Перерахуємо команди, які ми використовуватимемо у цій статті.

list of available commands:
last    Get last block and state info from server
sendfile <filename> Load a serialized message from <filename> and send it to server
getaccount <addr> [<block-id-ext>]  Loads the most recent state of specified account; <addr> is in [<workchain>:]<hex-or-base64-addr> format
runmethod <addr> [<block-id-ext>] <method-id> <params>...   Runs GET method <method-id> of account <addr> with specified parameters

last получает последний созданный блок с сервера. 

sendfile <filename> отправляет в TON файл с сообщением, именно с помощью этой команды публикуется смарт-контракт и запрсосы к нему. 

getaccount <addr> загружает текущее состояние смарт-контракта с указанным адресом. 

runmethod <addr> [<block-id-ext>] <method-id> <params>  запускает get-методы смартконтракта. 

Тепер ми готові до написання контракту.

Реалізація

Ідея

Як уже писав вище, смарт-контракт, який ми пишемо це лотерея.

Причому це не лотерея в якій треба купити квиток і чекати на годину, день або місяць, а моментальна в якій користувач перекладає на адресу контракту N грамів, і моментально отримує назад 2 * N грамів або програє. Імовірність перемоги зробимо близько 40%. Якщо грамів для виплати недостатньо, вважатимемо транзакцію поповненням.

Причому важливо щоб ставки можна було бачити в реальному часі та у зручному вигляді, щоб користувач одразу міг зрозуміти виграв він чи програв. Тому потрібно зробити веб-сайт, який покаже ставки та результат безпосередньо з TON.

Написання смарт-контракту

Для зручності я зробив підсвічування коду для FunC, плагін можна знайти і встановити в пошуку Visual Studio Code, якщо раптом захочеться додати щось, виклав плагін у відкритий доступ. Також раніше кимось був зроблений плагін для роботи з Fift, теж можна встановити в VSC.

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

Щоб полегшити собі життя, ми будемо писати смарт-контракт і тестувати локально, доки він не буде готовий. Тільки після цього опублікуємо його у TON.

У смарт-контракту є два зовнішні методи, до яких можна звертатися. Перший, recv_external() ця функція виконується коли запит до контракту походить із зовнішнього світу, тобто не з TON, наприклад коли ми формуємо повідомлення і відправляємо його через lite-client. Другий, recv_internal() це коли всередині самого TON будь-який контракт звертається до нашого. В обох випадках можна передати параметри у функцію.

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

() recv_internal(slice in_msg) impure {
    ;; TODO: implementation 
}

() recv_external(slice in_msg) impure {
    ;; TODO: implementation  
}

Тут треба пояснити, що таке slice. Всі дані, що зберігаються в TON Blockchain це колекція TVM cell або просто cell, в такому осередку можна зберігати до 1023 біт даних і до 4 посилань на інші осередки.

TVM cell slice або slice це частина існуючої cell використовується для її парсингу, далі буде зрозуміло. Головне для нас, що до смарт-контракту ми можемо передати slice і в залежності від виду повідомлення обробити дані в recv_external() або recv_internal().

impure — ключове слово, яке свідчить про те, що функція змінює дані смарт-контракту.

Збережемо код контракту в lottery-code.fc і скомпілюємо.

~/TON/build/crypto/func -APSR -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc 

Значення прапорів можна переглянути за допомогою команди

~/TON/build/crypto/func -help

У нас вийшов скомпільований Fift-асемблер код в lottery-compiled.fif:

// lottery-compiled.fif

"Asm.fif" include
// automatically generated from `/Users/rajymbekkapisev/TON/ton/crypto/smartcont/stdlib.fc` `./lottery-code.fc` 
PROGRAM{
  DECLPROC recv_internal
  DECLPROC recv_external
  recv_internal PROC:<{
    //  in_msg
    DROP    // 
  }>
  recv_external PROC:<{
    //  in_msg
    DROP    // 
  }>
}END>c

Його можна запустити локально, для цього підготуємо оточення.

Зауважимо, першим рядком підключається Asm.fif, це код написаний на Fift для Fift асемблера.

Тому що ми хочемо запускати та тестувати смарт-контракт локально створимо файл lottery-test-suite.fif і скопіюємо туди скомпільований код замінивши в ньому останній рядок, який записує код смартконтракту в константу codeщоб потім передати його у віртуальну машину:

"TonUtil.fif" include
"Asm.fif" include

PROGRAM{
  DECLPROC recv_internal
  DECLPROC recv_external
  recv_internal PROC:<{
    //  in_msg
    DROP    // 
  }>
  recv_external PROC:<{
    //  in_msg
    DROP    // 
  }>
}END>s constant code

Поки що зрозуміло, тепер додамо в той же файл код, який ми будемо використовувати для запуску TVM.

0 tuple 0x076ef1ea , // magic
0 , 0 , // actions msg_sents
1570998536 , // unix_time
1 , 1 , 3 , // block_lt, trans_lt, rand_seed
0 tuple 100000000000000 , dictnew , , // remaining balance
0 , dictnew , // contract_address, global_config
1 tuple // wrap to another tuple
constant c7

0 constant recv_internal // to run recv_internal() 
-1 constant recv_external // to invoke recv_external()

В c7 ми записуємо контекст, тобто дані, з якими запускатиметься TVM (або стан мережі). Ще під час конкурсу один із розробників показав як створюється c7 і я скопіював. У цій статті нам, можливо, потрібно буде змінювати rand_seed Так як від нього залежить генерація випадкового числа і не змінювати, то щоразу повертатиметься те саме число.

recv_internal и recv_external константи зі значенням 0 та -1 відповідатимуть за виклик відповідних функцій у смарт-контракті.

Тепер ми готові створити перший тест до нашого пустого смарт-контракту. Для наочності поки всі тести ми будемо додавати в цей же файл lottery-test-suite.fif.

Створимо змінну storage і запишемо в неї порожній cell, це буде сховище смарт-контракту.

message це повідомлення, яке ми передамо смарт-конртакту ззовні. Його теж зробимо поки що порожнім.

variable storage 
<b b> storage ! 

variable message 
<b b> message ! 

Після того як ми підготували конастанти та змінні ми запускаємо TVM за допомогою команди runvmctx та передаємо створені параметри на вхід.

message @ 
recv_external 
code 
storage @ 
c7 
runvmctx 

У результаті в нас вийде ось такий проміжний код на Fift.

Тепер ми можемо запустити код, що покращився.

export FIFTPATH=~/TON/ton/crypto/fift/lib // выполняем один раз для удобства 
~/TON/build/crypto/fift -s lottery-test-suite.fif 

Програма повинна відпрацювати без помилок і у висновку побачимо лог виконання:

execute SETCP 0
execute DICTPUSHCONST 19 (xC_,1)
execute DICTIGETJMPZ
execute DROP
execute implicit RET
[ 3][t 0][1582281699.325381279][vm.cpp:479]     steps: 5 gas: used=304, max=9223372036854775807, limit=9223372036854775807, credit=0

Відмінно ми написали першу робочу версію смарт-контракту.

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

Розробник сам обирає формат повідомлення, яке контракт може прийняти.

Але зазвичай,

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

Тому майже в кожному контракті вирішуються ці дві проблеми, тому що наш контракт приймає зовнішні повідомлення, нам також потрібно подбати про це.

Зробимо ми у зворотному порядку. Спочатку вирішимо проблему з повторенням, якщо контракт вже отримував таке повідомлення та обробив його, то не виконуватиме його вдруге. І потім вирішимо проблему для того, щоб тільки певне коло осіб могло надсилати повідомлення смарт-контракту.

Є різні способи вирішити проблему з повідомленнями, що повторюються. Ми зробимо ось як. У смарт-контракті ініціалізуємо лічильник прийнятих повідомлень з початковим значенням 0. У кожному повідомленні смарт-контракту будемо додавати поточне значення лічильника. Якщо значення лічильника у повідомленні не збігається зі значенням у смарт-контракті, ми не обробляємо його, якщо збігається, то обробляємо і збільшуємо лічильник у смарт-контракті на 1.

Повертаємось у lottery-test-suite.fif і дописуємо до нього другий тест. Відправимо невірний номер, код має викинути виняток. Наприклад, нехай у даних контракту зберігається 166, а ми відправимо 165.

<b 166 32 u, b> storage !
<b 165 32 u, b> message !

message @ 
recv_external 
code 
storage @ 
c7 
runvmctx

drop 
exit_code ! 
."Exit code " exit_code @ . cr 
exit_code @ 33 - abort"Test #2 Not passed"

Запустимо.

 ~/TON/build/crypto/fift -s lottery-test-suite.fif 

І побачимо, що тест виконується з помилкою.

[ 1][t 0][1582283084.210902214][words.cpp:3046] lottery-test-suite.fif:67: abort": Test #2 Not passed
[ 1][t 0][1582283084.210941076][fift-main.cpp:196]      Error interpreting file `lottery-test-suite.fif`: error interpreting included file `lottery-test-suite.fif` : lottery-test-suite.fif:67: abort": Test #2 Not passed

На цьому етапі lottery-test-suite.fif має виглядати як за посиланням.

Тепер давайте допишемо логіку лічильника у смарт-контракту у lottery-code.fc.

() recv_internal(slice in_msg) impure {
    ;; TODO: implementation 
}

() recv_external(slice in_msg) impure {
    if (slice_empty?(in_msg)) {
        return (); 
    }
    int msg_seqno = in_msg~load_uint(32);
    var ds = begin_parse(get_data());
    int stored_seqno = ds~load_uint(32);
    throw_unless(33, msg_seqno == stored_seqno);
}

В slice in_msg лежить повідомлення, які ми надсилаємо.

Перше, що ми робимо, перевіряємо якщо в повідомленні є дані, якщо ні, то просто виходимо.

Далі ми паримо повідомлення. in_msg~load_uint(32) завантажує число 165, 32-х бітне unsigned int з надісланого повідомлення.

Далі ми завантажуємо 32 біти зі сховища смарт-контракту. Перевіряємо, що завантажене число збігається з переданим, якщо ні викидаємо виняток. У нашому випадку, оскільки ми передаємо незбігаючі, має викидатися виняток.

Тепер скомпілюємо.

~/TON/build/crypto/func -APSR -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc 

Код, що вийшов, скопіюємо в lottery-test-suite.fif, не забуваючи замінити останній рядок.

Перевіряємо, що тест проходить:

~/TON/build/crypto/fift -s lottery-test-suite.fif

Ось тут можна переглянути відповідний коміт з поточними результатами.

Зауважимо, що постійно копіювати скомпільований код смарт-контракту у файл із тестами незручно, тому напишемо скрипт, який записуватиме код у константу за нас, а ми просто підключимо скомпільований код до наших тестів за допомогою "include".

У папці з проектом створимо файл build.sh із наступним змістом.

#!/bin/bash

~/TON/build/crypto/func -SPA -R -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc

Зробимо його виконуваним.

chmod +x ./build.sh

Тепер достатньо запустити наш скрипт, щоб скомпілювати контракт. Але крім цього нам треба записати його до константи code. Тому ми створимо новий файл lotter-compiled-for-test.fif, який і увімкнемо у файлі lottery-test-suite.fif.

Додамо в sh скирт код, який буде просто дублювати скомпльований файл в lotter-compiled-for-test.fif і міняти в ньому останній рядок.

# copy and change for test 
cp lottery-compiled.fif lottery-compiled-for-test.fif
sed '$d' lottery-compiled-for-test.fif > test.fif
rm lottery-compiled-for-test.fif
mv test.fif lottery-compiled-for-test.fif
echo -n "}END>s constant code" >> lottery-compiled-for-test.fif

Тепер щоб перевірити, запустимо скрипт, що вийшов, і у нас згенерується файл lottery-compiled-for-test.fif, який ми включимо до нашого lottery-test-suite.fif

В lottery-test-suite.fif видаляємо код контракт і додаємо рядок "lottery-compiled-for-test.fif" include.

Запускаємо тести, щоби перевірити, що вони проходять.

~/TON/build/crypto/fift -s lottery-test-suite.fif

Добре, тепер, щоб автоматизувати запуск тестів створимо файл test.sh, який спочатку виконуватиме build.shа потім запускати тести.

touch test.sh
chmod +x test.sh

Всередину пишемо

./build.sh 

echo "nCompilation completedn"

export FIFTPATH=~/TON/ton/crypto/fift/lib
~/TON/build/crypto/fift -s lottery-test-suite.fif

зробимо test.sh і запустимо, щоб переконатися у працездатності тестів.

chmod +x ./test.sh
./test.sh

Перевіряємо, що контракт компілюється та тести виконуються.

Добре, тепер при запуску test.sh відразу відбуватиметься компіляція та запуск тестів. Ось посилання на коміт.

Окей, перш ніж ми продовжимо давайте для зручності зробимо ще одну річ.

Створимо папку build де зберігатимемо скопійований контракт та його клон записаний у константу lottery-compiled.fif, lottery-compiled-for-test.fif. Також створимо папку test де зберігатиметься файл із тестами lottery-test-suite.fif та потенційно інші допоміжні файли. Посилання на відповідні зміни.

Продовжимо розробку смарт-контракту.

Далі має бути тест, який перевіряє, що повідомлення приймається та лічильник оновлюється у сховищі, коли ми надсилаємо правильне число. Але ми зробимо це пізніше.

Зараз подумаємо над тим, яка структура даних та які дані потрібно зберігати у смарт-контракті.

Опишу все, що ми зберігаємо.

`seqno` 32-х битное целое положительное число счетчик. 

`pubkey` 256-ти битное целое положительное число публичный ключ, с помощью которого, мы будем проверять подпись отправленного извне сообщения, о чем ниже. 

`order_seqno` 32-х битное целое положительное число хранит счетчик количества ставок. 

`number_of_wins` 32-х битное целое положительное число хранит  количество побед. 

`incoming_amount` тип данных Gram (первые 4 бита отвечает за длину), хранит общее количество грамов, которые были отправлены на контртакт. 

`outgoing_amount` общее количество грамов, которое было отправлено победителям. 

`owner_wc` номер воркчейна, 32-х битное (в некоторых местах написано, что 8-ми битное) целое число. В данный момент всего два -1 и 0. 

`owner_account_id` 256-ти битное целое положительное число, адрес контракта в текущем воркчейне. 

`orders` переменная типа словарь, хранит последние двадцать ставок. 

Далі слід написати дві функції. Першу назвемо pack_state(), яка буде упаковувати дані для подальшого збереження його у сховищі смарт-контракту. Другу, назвемо unpack_state() буде зчитувати та повертати дані зі сховища.

_ pack_state(int seqno, int pubkey, int order_seqno, int number_of_wins, int incoming_amount, int outgoing_amount, int owner_wc, int owner_account_id, cell orders) inline_ref {
    return begin_cell()
            .store_uint(seqno, 32)
            .store_uint(pubkey, 256)
            .store_uint(order_seqno, 32)
            .store_uint(number_of_wins, 32)
            .store_grams(incoming_amount)
            .store_grams(outgoing_amount)
            .store_int(owner_wc, 32)
            .store_uint(owner_account_id, 256)
            .store_dict(orders)
            .end_cell();
}

_ unpack_state() inline_ref {
    var ds = begin_parse(get_data());
    var unpacked = (ds~load_uint(32), ds~load_uint(256), ds~load_uint(32), ds~load_uint(32), ds~load_grams(), ds~load_grams(), ds~load_int(32), ds~load_uint(256), ds~load_dict());
    ds.end_parse();
    return unpacked;
}

Додаємо ці дві функції на початку смарт-контракту. Вийде ось такий проміжний результат.

Щоб зберегти дані потрібно буде викликати вбудовану функцію set_data() і вона запише дані з pack_state() у сховищі смарт-контракту.

cell packed_state = pack_state(arg_1, .., arg_n); 
set_data(packed_state);

Тепер коли ми маємо зручні функції запису та читання даних ми можемо рухатися далі.

Нам потрібно перевірити, що повідомлення, що входить ззовні, підписане власником контракту (ну або іншим користувачем, який має доступ до приватного ключа).

Коли ми публікуємо смарт-контракт, ми можемо ініціалізувати його з потрібними нам даними в сховищі, які збережуться для майбутнього використання. Ми запишемо туди публічний ключ, щоб можна було перевірити, що підпис вхідного повідомлення був зроблений відповідним приватним ключем.

Перед тим як продовжити створимо приватний ключ і запишемо його в test/keys/owner.pk. Для цього запустимо Fift в інтерактивному режимі та виконаємо чотири команди.

`newkeypair` генерация публичного и приватного ключа и запись их в стек. 

`drop` удаления из стека верхнего элемента (в данном случае публичный ключ)  

`.s` просто посмотреть что лежит в стеке в данный момент 

`"owner.pk" B>file` запись приватного ключа в файл с именем `owner.pk`. 

`bye` завершает работу с Fift. 

Створимо папку keys усередині папки test і туди запишемо приватний ключ.

mkdir test/keys
cd test/keys
~/TON/build/crypto/fift -i 
newkeypair
 ok
.s 
BYTES:128DB222CEB6CF5722021C3F21D4DF391CE6D5F70C874097E28D06FCE9FD6917 BYTES:DD0A81AAF5C07AAAA0C7772BB274E494E93BB0123AA1B29ECE7D42AE45184128 
drop 
 ok
"owner.pk" B>file
 ok
bye

У поточній папці бачимо файл owner.pk.

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

Тепер нам потрібно написати перевірку підпису. Почнемо із тіста. Спочатку ми зчитуємо приватний ключ із файлу за допомогою функції file>B і записуємо його у змінну owner_private_key, далі за допомогою функції priv>pub конвертуємо приватний ключ у публічний і запишемо результат у owner_public_key.

variable owner_private_key
variable owner_public_key 

"./keys/owner.pk" file>B owner_private_key !
owner_private_key @ priv>pub owner_public_key !

Обидва ключі нам знадобляться.

Ініціалізуємо довільними даними сховище смарт-контракту в тій самій послідовності, як у функції pack_state()і запишемо до змінної storage.

variable owner_private_key
variable owner_public_key 
variable orders
variable owner_wc
variable owner_account_id

"./keys/owner.pk" file>B owner_private_key !
owner_private_key @ priv>pub owner_public_key !
dictnew orders !
0 owner_wc !
0 owner_account_id !

<b 0 32 u, owner_public_key @ B, 0 32 u, 0 32 u, 0 Gram, 0 Gram, owner_wc @ 32 i, owner_account_id @ 256 u,  orders @ dict, b> storage !

Далі складемо підписане повідомлення, у ньому буде лише підпис та значення лічильника.

Спочатку створюємо дані, які хочемо передати, потім підписуємо їх приватним ключем та нарешті формуємо підписане повідомлення.

variable message_to_sign
variable message_to_send
variable signature
<b 0 32 u, b> message_to_sign !
message_to_sign @ hashu owner_private_key @ ed25519_sign_uint signature !
<b signature @ B, 0 32 u, b> <s  message_to_send !  

У результаті повідомлення, яке ми відправимо в смарт-контракт, записано в змінну message_to_send, про функції hashu, ed25519_sign_uint можна почитати у документації з Fift.

І для запуску тесту знову викликаємо.

message_to_send @ 
recv_external 
code 
storage @
c7
runvmctx

Ось так повинен виглядати файл із тестами на даному етапі.

Запустимо тест і він впаде, тому змінимо смарт-контракт, щоб він зміг отримувати повідомлення такого формату та перевіряти підпис.

Спочатку рахуємо з повідомлення 512 біт підпису і запишемо в змінну, далі вважаємо 32 біти змінної лічильника.

Так як у нас є функція зчитування даних зі сховища смарт-конракту використовуватимемо її.

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

var signature = in_msg~load_bits(512);
var message = in_msg;
int msg_seqno = message~load_uint(32);
(int stored_seqno, int pubkey, int order_seqno, int number_of_wins, int incoming_amount, int outgoing_amount, int owner_wc, int owner_account_id, cell orders) = unpack_state();
throw_unless(33, msg_seqno == stored_seqno);
throw_unless(34, check_signature(slice_hash(in_msg), signature, pubkey));

Співвідповідальний коміт ось тут.

Запустимо тести та побачимо, що другий тест падає. З двох причин, брак бітів у повідомленні та брак бітів у сховищі, тому код падає при парсингу. Потрібно додати підпис повідомлення, яке ми надсилаємо та скопіювати сховище з останнього тесту.

У другому тесті додамо підпис повідомлення та змінимо сховище смарт-контракту. Ось так виглядає файл із тестами в даний момент.

Напишемо четвертий тест, у якому надсилатимемо повідомлення підписане чужим приватним ключем. Створимо ще один приватний ключ та збережемо у файл not-owner.pk. Цим приватним ключем підпишемо повідомлення. Запустимо тести та переконаємось, що всі тести проходять. Коміт на поточний момент.

Тепер, нарешті, ми можемо перейти до реалізації логіки смартконтракту.
В recv_external() ми прийматимемо два типи повідомлень.

Оскільки наш контракт акумулюватиме програші гравців, ці гроші треба перераховувати творцеві лотереї. Адреса гаманця автора лотереї записується в сховище під час створення договору.

Про всяк випадок нам потрібна можливість змінювати адресу на яку відправляти грами програли. Також ми повинні мати можливість надсилати грами з лотереї на адресу власника.

Почнемо з першого. Напишемо спочатку тест, який перевірятиме, що після надсилання повідомлення смарт-контракт зберіг нову адресу у сховищі. Звернемо увагу, що у повідомлення крім лічильника та нової адреси ми передаємо ще action 7-ми бітне ціле невід'ємне число, залежно від нього, ми вибиратимемо як обробляти повідомлення в смарт-контракті.

<b 0 32 u, 1 @ 7 u, new_owner_wc @  32 i, new_owner_account_id @ 256 u, b> message_to_sign !

У тесті можна побачити, як відбувається десереалізація сховища. storage у Fift. Десеріалізація змінних описана в документації Fift.

Посилання на коміт з додаванням тесту.

Запускаємо тест і переконаємось, що він падає. Тепер додамо логіку щодо зміни адреси власника лотереї.

У смарт-контракті ми продовжуємо парсити message, зчитуємо в action. Нагадаємо, що у нас буде два action: зміна адреси та надсилання грамів.

Потім зчитуємо нову адресу власника контракту та зберігаємо у сховищі.
Запускаємо тести та бачимо, що третій тест падає. Падає через те, що контракт тепер додатково парсить 7 біт із повідомлення, яких не вистачає у тесті. Додамо до повідомлення неіснуючий action. Запустимо тести та бачимо, що всі проходять. Тут коміт на зміни. Чудово.

Тепер напишемо логіку відправлення вказаної кількості грамів на збережену раніше адресу.

Спочатку напишемо тест. Ми напишемо два тести один коли балансу не вистачає, другий коли все має пройти успішно. Тести можна переглянути у цьому коментарі.

Тепер допишемо код. Спочатку напишемо два допоміжні методи. Перший метод гет для того, щоб дізнатися поточний баланс смарт-контракту.

int balance() inline_ref method_id {
    return get_balance().pair_first();
}

І другий для надсилання грамів на інший смарт-контракт. Цей метод я повністю скопіював із іншого смарт-контракту.

() send_grams(int wc, int addr, int grams) impure {
    ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000
    cell msg = begin_cell()
    ;;  .store_uint(0, 1) ;; 0 <= format indicator int_msg_info$0 
    ;;  .store_uint(1, 1) ;; 1 <= ihr disabled
    ;;  .store_uint(1, 1) ;; 1 <= bounce = true
    ;;  .store_uint(0, 1) ;; 0 <= bounced = false
    ;;  .store_uint(4, 5)  ;; 00100 <= address flags, anycast = false, 8-bit workchain
        .store_uint (196, 9)
        .store_int(wc, 8)
        .store_uint(addr, 256)
        .store_grams(grams)
        .store_uint(0, 107) ;; 106 zeroes +  0 as an indicator that there is no cell with the data.
        .end_cell(); 
    send_raw_message(msg, 3); ;; mode, 2 for ignoring errors, 1 for sender pays fees, 64 for returning inbound message value
}

Додамо ці два методи до смарт-контракту і напишемо логіку. Спочатку парсим кількість грамів з повідомлення. Далі перевіряємо баланс, якщо не вистачає викидаємо виняток. Якщо все добре, то відправляємо грами на збережену адресу та оновлюємо лічильник.

int amount_to_send = message~load_grams();
throw_if(36, amount_to_send + 500000000 > balance());
accept_message();
send_grams(owner_wc, owner_account_id, amount_to_send);
set_data(pack_state(stored_seqno + 1, pubkey, order_seqno, number_of_wins, incoming_amount, outgoing_amount, owner_wc, owner_account_id, orders));

Ось так виглядає смарт-контракт на даний момент. Запустимо тести та переконаємося, що вони проходять.

До речі, за оброблене повідомлення у смарт-контракту щоразу списується комісія. Щоб повідомлення смарт-контракт запитав, після базових перевірок потрібно викликати accept_message().

Тепер займемося внутрішніми повідомленнями. За фактом ми тільки прийматимемо грами і надсилатимемо назад гравцеві подвійну суму при виграші і третину власнику при програші.

Спочатку напишемо простий тест. Для цього нам знадобиться тестова адреса смарт-контракту, з якої ми ніби відправляємо грами на смарт-контракт.

Адреса смартконтракту складається з двох чисел, 32-бітове ціле число відповідає за workchain і 256-ти ціле невід'ємне унікальний номер акаунту в цьому workchain. Наприклад, -1 і 12345, цю адресу і збережемо у файл.

Я скопіював функцію збереження адреси з TonUtil.fif.

// ( wc addr fname -- )  Save address to file in 36-byte format
{ -rot 256 u>B swap 32 i>B B+ swap B>file } : save-address

Давайте розберемо, як працює функція, це дасть розуміння, як працює Fift. Запускаємо Fift в інтерактивному режимі.

~/TON/build/crypto/fift -i 

Спочатку ми кладемо в стек -1, 12345 та назву майбутнього файлу "sender.addr":

-1 12345 "sender.addr" 

Наступним кроком виконується функція -rot, яка зсуває стек, таким чином, що нагорі стека виявляється унікальний номер смарт-контракту:

"sender.addr" -1 12345

256 u>B конвертує 256-бітне невід'ємне ціле в байти.

"sender.addr" -1 BYTES:0000000000000000000000000000000000000000000000000000000000003039

swap змінює місцями два верхні елементи стека.

"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 -1

32 i>B конвертує 32-бітне ціле в байти.

"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 BYTES:FFFFFFFF

B+ з'єднує дві послідовності з байтів.

 "sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039FFFFFFFF

знову swap.

BYTES:0000000000000000000000000000000000000000000000000000000000003039FFFFFFFF "sender.addr" 

І нарешті виконується запис байтів у файл B>file. Після цього наш стек порожній. Зупиняємо Fift. У поточній папці створено файл sender.addr. Перенесемо файл у створену папку test/addresses/.

Напишемо простий тест, який відправлятиме грами на смарт-контракт. Ось коміт.

Тепер займемося логікою лотереї.

Перше, що ми робимо, перевіряємо повідомлення bounced чи ні, якщо bounced, то ігноруємо. bounced отже, контракт поверне грами якщо станеться якась помилка. Ми повертаємо грами, якщо раптом виникне помилка, ми не будемо.

Перевіряємо, баланс якщо менше півграма, то просто приймаємо повідомлення і ігноруємо.

Далі паримо адресу смартконтракту з якого надійшло повідомлення.

Зчитуємо дані зі сховища і далі видаляємо старі ставки з історії, якщо їх більше двадцяти. Для зручності я написав три додаткові функції pack_order(), unpack_order(), remove_old_orders().

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

Далі, нарешті, суть смарт-контракту.

Спочатку якщо гравець програв ми зберігаємо його в історію ставок і якщо сума більше 3 г відправляємо 1/3 власнику смарт-контракту.

Якщо ж гравець виграв, то ми надсилаємо подвоєну суму на адресу гравця і далі зберігаємо інформацію про ставку в історію.

() recv_internal(int order_amount, cell in_msg_cell, slice in_msg) impure {
    var cs = in_msg_cell.begin_parse();
    int flags = cs~load_uint(4);  ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
    if (flags & 1) { ;; ignore bounced
        return ();
    }
    if (order_amount < 500000000) { ;; just receive grams without changing state 
        return ();
    }
    slice src_addr_slice = cs~load_msg_addr();
    (int src_wc, int src_addr) = parse_std_addr(src_addr_slice);
    (int stored_seqno, int pubkey, int order_seqno, int number_of_wins, int incoming_amount, int outgoing_amount, int owner_wc, int owner_account_id, cell orders) = unpack_state();
    orders = remove_old_orders(orders, order_seqno);
    if (balance() < 2 * order_amount + 500000000) { ;; not enough grams to pay the bet back, so this is re-fill
        builder order = pack_order(order_seqno, 1, now(), order_amount, src_wc, src_addr);
        orders~udict_set_builder(32, order_seqno, order);
        set_data(pack_state(stored_seqno, pubkey, order_seqno + 1, number_of_wins, incoming_amount + order_amount, outgoing_amount, owner_wc, owner_account_id, orders));
        return ();
    }
    if (rand(10) >= 4) {
        builder order = pack_order(order_seqno, 3, now(), order_amount, src_wc, src_addr);
        orders~udict_set_builder(32, order_seqno, order);
        set_data(pack_state(stored_seqno, pubkey, order_seqno + 1, number_of_wins, incoming_amount + order_amount, outgoing_amount, owner_wc, owner_account_id, orders));
        if (order_amount > 3000000000) {
            send_grams(owner_wc, owner_account_id, order_amount / 3);
        }
        return ();
    }
    send_grams(src_wc, src_addr, 2 * order_amount);
    builder order = pack_order(order_seqno, 2, now(), order_amount, src_wc, src_addr);
    orders~udict_set_builder(32, order_seqno, order);
    set_data(pack_state(stored_seqno, pubkey, order_seqno + 1, number_of_wins + 1, incoming_amount, outgoing_amount + 2 * order_amount, owner_wc, owner_account_id, orders));
}

Ось і все. Відповідний коміт.

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

Додамо геть-методи. Про те, як отримувати інформацію про смарт-контракт напишемо нижче.

Ще я забув додати код, який оброблятиме перший запит, який відбувається при публікації смарт-контракту. Відповідний коміт. І ще виправив баг із відправленням 1/3 суми на аккаунт власника.

Далі лишається опублікувати смарт-контракт. Створимо папку requests.

За основу я взяв код публікації simple-wallet-code.fc який можна знайти в офіційному репозиторії.

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

Далі ми виконуємо код публікації та отримуємо lottery-query.boc файл та адресу смартконтракту.

~/TON/build/crypto/fift -s requests/new-lottery.fif 0

Не забуваймо зберегти згенеровані файли: lottery-query.boc, lottery.addr, lottery.pk.

Серед іншого у логах виконання побачимо адресу смарт-контракту.

new wallet address = 0:044910149dbeaf8eadbb2b28722e7d6a2dc6e264ec2f1d9bebd6fb209079bc2a 
(Saving address to file lottery.addr)
Non-bounceable address (for init): 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd
Bounceable address (for later access): kQAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8KpFY

Заради інтересу зробимо запит у TON

$ ./lite-client/lite-client -C ton-lite-client-test1.config.json 
getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd

І побачимо, що обліковий запис з такою адресою порожній.

account state is empty

Відправляємо на адресу 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd 2 Gram і за кілька секунд виконуємо тугіше команду. Для надсилання грамів я використовую офіційний гаманець, А тестові грами можна попросити у когось із чату, про який я скажу в кінці статті.

> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd

Дивиться, що в мережі з'явився неініціалізований (state:account_uninit) смартконтракт з такою адресою та балансом 1 000 000 000 нанограм.

account state is (account
  addr:(addr_std
    anycast:nothing workchain_id:0 address:x044910149DBEAF8EADBB2B28722E7D6A2DC6E264EC2F1D9BEBD6FB209079BC2A)
  storage_stat:(storage_info
    used:(storage_used
      cells:(var_uint len:1 value:1)
      bits:(var_uint len:1 value:103)
      public_cells:(var_uint len:0 value:0)) last_paid:1583257959
    due_payment:nothing)
  storage:(account_storage last_trans_lt:3825478000002
    balance:(currencies
      grams:(nanograms
        amount:(var_uint len:4 value:2000000000))
      other:(extra_currencies
        dict:hme_empty))
    state:account_uninit))
x{C00044910149DBEAF8EADBB2B28722E7D6A2DC6E264EC2F1D9BEBD6FB209079BC2A20259C2F2F4CB3800000DEAC10776091DCD650004_}
last transaction lt = 3825478000001 hash = B043616AE016682699477FFF01E6E903878CDFD6846042BA1BFC64775E7AC6C4
account balance is 2000000000ng

Тепер опублікуємо смарт-контракт. Запустимо lite-client та виконаємо.

> sendfile lottery-query.boc
[ 1][t 2][1583008371.631410122][lite-client.cpp:966][!testnode] sending query from file lottery-query.boc
[ 3][t 1][1583008371.828550100][lite-client.cpp:976][!query]    external message status is 1 

Перевіримо, що контракт опубліковано.

> last
> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd

Серед іншого отримаємо.

  storage:(account_storage last_trans_lt:3825499000002
    balance:(currencies
      grams:(nanograms
        amount:(var_uint len:4 value:1987150999))
      other:(extra_currencies
        dict:hme_empty))
    state:(account_active

Бачимо, що account_active.

Відповідний коміт із змінами ось тут.

Тепер створимо запити для взаємодії зі смарт-контрактом.

Точніше перший для зміни адреси ми залишимо як самостійну роботу, а другий для відправки грамів на адресу власника зробимо. За фактом нам потрібно буде зробити те саме, що і в тесті на відправку грамів.

Ось таке повідомлення ми надсилатимемо на смартконтракт, де msg_seqno 165, action 2 та 9.5 грам для відправки.

<b 165 32 u, 2 7 u, 9500000000 Gram, b>

Не забуваємо підписати повідомлення приватним ключем lottery.pk, який згенерувався раніше під час створення смарт-контракту. Ось відповідний коміт.

Отримуємо інормацію зі смарт-контракту за допомогою геть-методів

Тепер розглянемо як запускати геть-методи смартконтракту.

запускаємо lite-client і запускаємо геть-методи, які ми написали.

$ ./lite-client/lite-client -C ton-lite-client-test1.config.json
> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd balance
arguments:  [ 104128 ] 
result:  [ 64633878952 ] 
...

В result міститься значні, що повертає функція balance() з нашого смарт-контракту.
Теж саме здійснимо ще для кількох методів.

> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd get_seqno
...
arguments:  [ 77871 ] 
result:  [ 1 ] 

Запитаємо історію ставок.

> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd get_orders
...
arguments:  [ 67442 ] 
result:  [ ([0 1 1583258284 10000000000 0 74649920601963823558742197308127565167945016780694342660493511643532213172308] [1 3 1583258347 4000000000 0 74649920601963823558742197308127565167945016780694342660493511643532213172308] [2 1 1583259901 50000000000 0 74649920601963823558742197308127565167945016780694342660493511643532213172308]) ] 

Ми будемо використовувати lite-client та геть-методи, щоб виводити інформацію про смарт-контракт на сайті.

Показуємо дані смарт-контракту на сайті

Я написав простий веб-сайт на Python, щоб показати дані зі смарт-контракту в зручному вигляді. Тут я не докладно зупинятимусь на ньому і опублікую сайт одним коммітом.

Запити до TON робляться з Python за допомогою lite-client. Для зручності сайт пакується в Docker та публікується на Google Cloud. Посилання на сайт.

пробуємо

Тепер спробуємо відправити туди грамів для поповнення з гаманця. Відправимо 40 грам. І зробимо пару ставок для наочності. Бачимо, що сайт показує історію ставок, поточний відсоток виграшу та іншу корисну інформацію.

Бачимо, Що першу ми виграли, другу програли.

Післямова

Стаття вийшла набагато довшою ніж я припускав, може можна було й коротше, а може якраз для людини, яка нічого не знає про TON і хоче написати та опублікувати не найпростіший смарт-контракт із можливістю з ним взаємодіяти. Можливо, якісь речі можна було пояснити простіше.

Можливо деякі моменти в реалізації можна було зробити більш ефективно та елегантно, але тоді на підготовку статті пішло б ще більше часу. Також можливо, що я десь помилився чи щось не зрозумів, тому якщо ви робите щось серйозне, потрібно спиратися на офіційну документацію або офіційний репозиторій з кодом TON.

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

Про майбутнє TON міркувати не буду. Можливо, платформа стане чимось більшим і нам варто витратити час на її вивчення і зайняти нішу своїми продуктами вже зараз.

Є ще Libra від Facebook, яка має потенційну аудиторію користувачів більше ніж у TON. Про Libra я майже нічого не знаю, судячи з форуму активності там набагато більше, ніж у спільноті TON. Хоча розробники та співтовариство TON більше схоже на андеграунд, що теж круто.

Посилання

  1. Офіційна документація щодо TON: https://test.ton.org
  2. Офіційний репозиторій TON: https://github.com/ton-blockchain/ton
  3. Офіційний гаманець для різних платформ: https://wallet.ton.org
  4. Репозиторій смарт-контракту із цієї статті: https://github.com/raiym/astonished
  5. Посилання на сайт смарт-контракту: https://ton-lottery.appspot.com
  6. Репозиторій на розширення для Visual Studio Code для FunC: https://github.com/raiym/func-visual-studio-plugin
  7. Чат про ТON у Telegram, який дуже допоміг розібратися на початковому етапі. Я думаю не буде помилкою, якщо скажу, що там є всі, хто писав щось для TON. Там можна попросити тестових грамів. https://t.me/tondev_ru
  8. Ще один чат про TON, в якому я знаходив корисну інформацію: https://t.me/TONgramDev
  9. Перший етап конкурсу: https://contest.com/blockchain
  10. Другий етап конкурсу: https://contest.com/blockchain-2

Джерело: habr.com

Купити надійний хостинг для сайтів із захистом від DDoS, VPS VDS сервери 🔥 Купити надійний хостинг для сайтів із захистом від DDoS, VPS VDS сервери | ProHoster