О томе како написати и објавити паметни уговор у Телеграм отвореној мрежи (ТОН)

О томе како написати и објавити паметни уговор у ТОН-у

О чему је овај чланак?

У чланку ћу причати о томе како сам учествовао у првом (од два) Телеграм блоцкцхаин такмичења, нисам узео награду и одлучио да своје искуство забележим у чланку како не би потонуо у заборав и, можда, помогао неко.

Пошто нисам желео да пишем апстрактни код, већ да урадим нешто што функционише, за чланак сам написао паметни уговор за тренутну лутрију и веб локацију која приказује податке паметног уговора директно из ТОН-а без коришћења међускладишта.

Чланак ће бити користан онима који желе да направе свој први паметни уговор у ТОН-у, али не знају одакле да почну.

Користећи лутрију као пример, прећи ћу од инсталирања окружења до објављивања паметног уговора, интеракције са њим и писања веб странице за примање и објављивање података.

О учешћу на конкурсу

Прошлог октобра, Телеграм је најавио блокчејн такмичење са новим језицима Fift и FunC. Било је потребно изабрати било који од пет предложених паметних уговора. Мислио сам да би било лепо да урадим нешто другачије, научим језик и направим нешто, чак и ако нећу морати ништа друго да пишем у будућности. Плус, тема је стално на уснама.

Вреди рећи да нисам имао искуства у развоју паметних уговора.

Планирао сам да учествујем до самог краја док не будем могао па да напишем прегледни чланак, али нисам успео одмах у првом. И написао новчаник са укљученим вишеструким потписом FunC и генерално је функционисало. Узео сам то као основу паметни уговор на Солидити.

Тада сам мислио да је ово сасвим сигурно довољно да заузмем бар неко наградно место. Као резултат тога, око 40 од 60 учесника је постало награђено, а ја нисам био међу њима. Уопште, нема ништа лоше у овоме, али једна ствар ме је засметала. У тренутку објављивања резултата преглед теста за мој уговор није био урађен, питао сам учеснике у ћаскању да ли има још неко ко га нема, није их било.

Очигледно обраћајући пажњу на моје поруке, два дана касније судије су објавиле коментар и још увек не разумем да ли су случајно пропустили мој паметни уговор током суђења или су једноставно помислили да је толико лош да му није потребан коментар. Поставио сам питање на страници, али нисам добио одговор. Иако није тајна ко је судио, сматрао сам да је непотребно писати личне поруке.

Много времена је потрошено на разумевање, па је одлучено да се напише чланак. Пошто још нема пуно информација, овај чланак ће помоћи да се уштеди време за све заинтересоване.

Концепт паметних уговора у ТОН-у

Пре него што било шта напишете, морате да схватите са које стране да приступите овој ствари. Стога ћу вам сада рећи од којих делова се систем састоји. Тачније, које делове треба да знате да бисте написали макар какав уговор о раду.

Фокусираћемо се на писање паметног уговора и сарадњу TON Virtual Machine (TVM), Fift и FunC, па чланак више личи на опис развоја редовног програма. Овде се нећемо задржавати на томе како сама платформа функционише.

Уопштено о томе како то функционише TVM и језик Fift постоји добра званична документација. Док сам учествовао на такмичењу и сада док сам писао актуелни уговор, често сам јој се обраћао.

Главни језик на коме су паметни уговори написани је FunC. Тренутно не постоји документација о томе, па да бисте нешто написали морате проучити примере паметних уговора из званичног репозиторијума и имплементацију самог језика тамо, плус можете погледати примере паметних уговора из претходна два такмичења. Везе на крају чланка.

Рецимо да смо већ написали паметни уговор за FunC, након тога компајлирамо код у Фифт асемблер.

Састављени паметни уговор остаје да буде објављен. Да бисте то урадили, потребно је да упишете функцију Fift, који ће као улаз узети код паметног уговора и неке друге параметре, а излаз ће бити датотека са екстензијом .boc (што значи „врећа ћелија“), и, у зависности од тога како то напишемо, приватни кључ и адресу, који се генерише на основу кода паметног уговора. Већ сада можете послати грам на адресу паметног уговора који још није објављен.

За објављивање паметног уговора у ТОН-у примљено .boc датотека ће морати да се пошаље у блок ланац помоћу лаганог клијента (више о томе у наставку). Али пре објављивања, потребно је да пренесете грам на генерисану адресу, иначе паметни уговор неће бити објављен. Након објављивања, можете да ступите у интеракцију са паметним уговором тако што ћете му послати поруке споља (на пример, коришћењем лаког клијента) или изнутра (на пример, један паметни уговор шаље другом поруку унутар ТОН).

Када схватимо како се код објављује, постаје лакше. Отприлике знамо шта желимо да напишемо и како ће наш програм функционисати. И док пишемо, тражимо како је то већ имплементирано у постојећим паметним уговорима, или гледамо у код за имплементацију Fift и FunC у званичном репозиторију, или погледајте у службеној документацији.

Врло често сам тражио кључне речи у Телеграм чету где су се окупили сви учесници такмичења и запослени у Телеграму, и десило се да су се током такмичења сви окупили и почели да разговарају о Фифту и ФунЦ-у. Линк на крају чланка.

Време је да пређемо са теорије на праксу.

Припрема окружења за рад са ТОН-ом

Урадио сам све што ће бити описано у чланку о МацОС-у и двапут проверио у чистом Убунту 18.04 ЛТС на Доцкер-у.

Прва ствар коју треба да урадите је да преузмете и инсталирате lite-client са којима можете слати захтеве ТОН-у.

Упутства на званичном сајту описују процес инсталације прилично детаљно и јасно и изостављају неке детаље. Овде следимо упутства, успут инсталирајући зависности које недостају. Нисам сам компајлирао сваки пројекат и инсталирао га из званичног Убунту спремишта (на МацОС-у који сам користио 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.

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

Упућивање првих захтева ТОН-у

Сада да покренемо 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%. Ако нема довољно грама за плаћање, онда ћемо трансакцију сматрати допуном.

Штавише, важно је да се опкладе могу видети у реалном времену иу прикладном облику, тако да корисник може одмах да схвати да ли је победио или изгубио. Стога, потребно је да направите веб локацију која ће приказивати опкладе и резултате директно са ТОН-а.

Писање паметног уговора

Ради практичности, истакао сам код за ФунЦ, додатак се може пронаћи и инсталирати у Висуал Студио Цоде претрази, ако изненада желите нешто да додате, учинио сам додатак јавно доступним. Такође, неко је претходно направио додатак за рад са Фифтом, можете га такође инсталирати и пронаћи у ВСЦ-у.

Хајде да одмах направимо спремиште у које ћемо урезати међурезултате.

Да бисмо себи олакшали живот, написаћемо паметни уговор и тестирати га локално док не буде спреман. Тек након тога ћемо то објавити у ТОН-у.

Паметни уговор има две екстерне методе којима се може приступити. Први, recv_external() ова функција се извршава када захтев за уговор долази из спољашњег света, односно не из ТОН-а, на пример, када сами генеришемо поруку и шаљемо је преко лите-клијента. друго, recv_internal() ово је када се у самом ТОН-у било који уговор односи на наш. У оба случаја, можете проследити параметре функцији.

Почнимо са једноставним примером који ће радити ако буде објављен, али у њему нема функционалног оптерећења.

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

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

Овде треба да објаснимо шта је то slice. Сви подаци ускладиштени у ТОН Блоцкцхаин-у су збирка 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

Саставили смо Фифт асемблерски код 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, ово је код написан у Фифту за Фифт асемблер.

Пошто желимо да покренемо и тестирамо паметни уговор локално, креираћемо датотеку 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

За сада се чини јасним, сада додајмо истој датотеци код који ћемо користити за покретање ТВМ-а.

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 снимамо контекст, односно податке са којима ће ТВМ (или стање мреже) бити покренут. Чак и током такмичења, један од програмера је показао како се ствара 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 ! 

Након што смо припремили константе и варијабле, покрећемо ТВМ помоћу команде 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()

Програмер сам бира формат поруке који уговор може да прихвати.

Али обично

  • прво, желимо да заштитимо наш уговор од спољашњег света и да га учинимо тако да само власник уговора може да му шаље спољне поруке.
  • друго, када пошаљемо исправну поруку ТОН-у, желимо да се ово деси тачно једном и када поново пошаљемо исту поруку, паметни уговор је одбацује.

Дакле, скоро сваки уговор решава ова два проблема, пошто наш уговор прихвата екстерне поруке, морамо да водимо рачуна и о томе.

Урадићемо то обрнутим редоследом. Прво, да решимо проблем са понављањем, ако је уговор већ примио такву поруку и обрадио је, неће је извршити други пут. А онда ћемо решити проблем тако да само одређени круг људи може да шаље поруке паметном уговору.

Постоје различити начини за решавање проблема са дуплираним порукама. Ево како ћемо то урадити. У паметном уговору иницијализујемо бројач примљених порука са почетном вредношћу 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.

Хајде да додамо скирпт код у сх, који ће једноставно дуплирати компајлирану датотеку 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. Да бисмо то урадили, покренимо Фифт у интерактивном режиму и извршимо четири команде.

`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 можете прочитати у документацији Фифта.

И поново позивамо да извршимо тест.

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 у Фифту. Десеријализација варијабли је описана у документацији Фифт.

Веза за урезивање уз додатак теста.

Покренимо тест и уверимо се да не успе. Сада додајмо логику да променимо адресу власника лутрије.

У паметном уговору настављамо да анализирамо 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-битног целог броја одговорног за радни ланац и 256-битног ненегативног целог јединственог броја налога у овом радном ланцу. На пример, -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

Хајде да погледамо како функција функционише, ово ће дати разумевање како Фифт функционише. Покрените Фифт у интерактивном режиму.

~/TON/build/crypto/fift -i 

Прво гурамо -1, 12345 и име будуће датотеке "сендер.аддр" на стек:

-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 што можете наћи у званичном репозиторијуму.

Нешто на шта вреди обратити пажњу. Генеришемо складиште паметног уговора и улазну поруку. Након овога се генерише адреса паметног уговора, односно адреса је позната и пре објављивања у ТОН-у. Затим треба да пошаљете неколико грама на ову адресу, а тек након тога да пошаљете датотеку са самим паметним уговором, пошто мрежа узима провизију за чување паметног уговора и операције у њему (валидатори који чувају и извршавају смарт уговор). уговори). Код се може погледати овде.

Затим извршавамо код за објављивање и добијамо 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

Само ради забаве, упутимо захтев ТОН-у

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

И видећемо да је налог са овом адресом празан.

account state is empty

Шаљемо на адресу 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd 2 грама и након неколико секунди извршимо исту команду. За слање грама користим службени новчаник, а можете тражити од некога из чета тест грам, о чему ћу говорити на крају чланка.

> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd

Изгледа као неиницијализован (state:account_uninit) паметни уговор са истом адресом и стањем од 1 нанограма.

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

Сада хајде да објавимо паметни уговор. Хајде да покренемо лите-цлиент и извршимо.

> 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 КСНУМКС, 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]) ] 

Користићемо лите-цлиент и добити методе за приказ информација о паметном уговору на сајту.

Приказивање података паметног уговора на веб локацији

Написао сам једноставну веб локацију у Питхон-у да прикажем податке из паметног уговора на згодан начин. Овде се нећу детаљније задржавати на томе и објавићу сајт у једној обавези.

Захтеви за ТОН су направљени од Python уз помоћ lite-client. Ради практичности, сајт је упакован у Доцкер и објављен на Гоогле Цлоуд-у. Линк.

Хајде да покушамо

Хајде сада да покушамо да пошаљемо граме тамо за допуну из новчаник. Послаћемо 40 грама. И хајде да направимо неколико опклада ради јасноће. Видимо да сајт приказује историју опклада, тренутни проценат добитака и друге корисне информације.

Видимода смо прву добили, другу изгубили.

Афтерворд

Испоставило се да је чланак много дужи него што сам очекивао, можда је могао бити краћи, или можда само за особу која не зна ништа о ТОН-у и жели да напише и објави не тако једноставан паметни уговор са могућношћу интеракције са то. Можда су се неке ствари могле једноставније објаснити.

Можда су неки аспекти имплементације могли бити урађени ефикасније и елегантније, али би тада било потребно још више времена за припрему чланка. Такође је могуће да сам негде погрешио или нешто нисам разумео, па ако радите нешто озбиљно, треба да се ослоните на званичну документацију или званично складиште са ТОН кодом.

Треба напоменути да пошто је сам ТОН још увек у активној фази развоја, може доћи до промена које ће прекинути било који од корака у овом чланку (што се десило док сам писао, већ је исправљено), али општи приступ је мало је вероватно да ће се променити.

Нећу говорити о будућности ТОН-а. Можда ће платформа постати нешто велико и требало би да проведемо време проучавајући је и сада попунимо нишу нашим производима.

Ту је и Вага са Фејсбука, која има потенцијалну публику корисника већу од ТОН-а. Не знам скоро ништа о Ваги, судећи по форуму тамо је много више активности него у ТОН заједници. Иако су програмери и заједница ТОН-а више као ундергроунд, што је такође цоол.

референце

  1. Званична ТОН документација: https://test.ton.org
  2. Званични ТОН репозиторијум: https://github.com/ton-blockchain/ton
  3. Званични новчаник за различите платформе: https://wallet.ton.org
  4. Репозиторијум паметних уговора из овог чланка: https://github.com/raiym/astonished
  5. Линк ка веб локацији паметног уговора: https://ton-lottery.appspot.com
  6. Репозиторијум за екстензију за Висуал Студио Цоде за ФунЦ: https://github.com/raiym/func-visual-studio-plugin
  7. Разговарајте о ТОН-у у Телеграму, што је заиста помогло да се то схвати у почетној фази. Мислим да неће бити грешка ако кажем да су сви који су нешто написали за ТОН ту. Тамо можете тражити и тестне грама. https://t.me/tondev_ru
  8. Још једно ћаскање о ТОН-у у којем сам нашао корисне информације: https://t.me/TONgramDev
  9. Прва фаза такмичења: https://contest.com/blockchain
  10. Друга фаза такмичења: https://contest.com/blockchain-2

Извор: ввв.хабр.цом

Додај коментар