О томе како написати и објавити паметни уговор у ТОН-у
О чему је овај чланак?
У чланку ћу причати о томе како сам учествовао у првом (од два) Телеграм блоцкцхаин такмичења, нисам узео награду и одлучио да своје искуство забележим у чланку како не би потонуо у заборав и, можда, помогао неко.
Пошто нисам желео да пишем апстрактни код, већ да урадим нешто што функционише, за чланак сам написао паметни уговор за тренутну лутрију и веб локацију која приказује податке паметног уговора директно из ТОН-а без коришћења међускладишта.
Чланак ће бити користан онима који желе да направе свој први паметни уговор у ТОН-у, али не знају одакле да почну.
Користећи лутрију као пример, прећи ћу од инсталирања окружења до објављивања паметног уговора, интеракције са њим и писања веб странице за примање и објављивање података.
О учешћу на конкурсу
Прошлог октобра, Телеграм је најавио блокчејн такмичење са новим језицима Fift и FunC. Било је потребно изабрати било који од пет предложених паметних уговора. Мислио сам да би било лепо да урадим нешто другачије, научим језик и направим нешто, чак и ако нећу морати ништа друго да пишем у будућности. Плус, тема је стално на уснама.
Вреди рећи да нисам имао искуства у развоју паметних уговора.
Планирао сам да учествујем до самог краја док не будем могао па да напишем прегледни чланак, али нисам успео одмах у првом. И са укљученим вишеструким потписом FunC и генерално је функционисало. Узео сам то као основу .
Тада сам мислио да је ово сасвим сигурно довољно да заузмем бар неко наградно место. Као резултат тога, око 40 од 60 учесника је постало награђено, а ја нисам био међу њима. Уопште, нема ништа лоше у овоме, али једна ствар ме је засметала. У тренутку објављивања резултата преглед теста за мој уговор није био урађен, питао сам учеснике у ћаскању да ли има још неко ко га нема, није их било.
Очигледно обраћајући пажњу на моје поруке, два дана касније судије су објавиле коментар и још увек не разумем да ли су случајно пропустили мој паметни уговор током суђења или су једноставно помислили да је толико лош да му није потребан коментар. Поставио сам питање на страници, али нисам добио одговор. Иако није тајна ко је судио, сматрао сам да је непотребно писати личне поруке.
Много времена је потрошено на разумевање, па је одлучено да се напише чланак. Пошто још нема пуно информација, овај чланак ће помоћи да се уштеди време за све заинтересоване.
Концепт паметних уговора у ТОН-у
Пре него што било шта напишете, морате да схватите са које стране да приступите овој ствари. Стога ћу вам сада рећи од којих делова се систем састоји. Тачније, које делове треба да знате да бисте написали макар какав уговор о раду.
Фокусираћемо се на писање паметног уговора и сарадњу TON Virtual Machine (TVM), Fift и FunC, па чланак више личи на опис развоја редовног програма. Овде се нећемо задржавати на томе како сама платформа функционише.
Уопштено о томе како то функционише TVM и језик Fift постоји добра званична документација. Док сам учествовао на такмичењу и сада док сам писао актуелни уговор, често сам јој се обраћао.
Главни језик на коме су паметни уговори написани је FunC. Тренутно не постоји документација о томе, па да бисте нешто написали морате проучити примере паметних уговора из званичног репозиторијума и имплементацију самог језика тамо, плус можете погледати примере паметних уговора из претходна два такмичења. Везе на крају чланка.
Рецимо да смо већ написали паметни уговор за FunC, након тога компајлирамо код у Фифт асемблер.
Састављени паметни уговор остаје да буде објављен. Да бисте то урадили, потребно је да упишете функцију Fift, који ће као улаз узети код паметног уговора и неке друге параметре, а излаз ће бити датотека са екстензијом .boc (што значи „врећа ћелија“), и, у зависности од тога како то напишемо, приватни кључ и адресу, који се генерише на основу кода паметног уговора. Већ сада можете послати грам на адресу паметног уговора који још није објављен.
За објављивање паметног уговора у ТОН-у примљено .boc датотека ће морати да се пошаље у блок ланац помоћу лаганог клијента (више о томе у наставку). Али пре објављивања, потребно је да пренесете грам на генерисану адресу, иначе паметни уговор неће бити објављен. Након објављивања, можете да ступите у интеракцију са паметним уговором тако што ћете му послати поруке споља (на пример, коришћењем лаког клијента) или изнутра (на пример, један паметни уговор шаље другом поруку унутар ТОН).
Када схватимо како се код објављује, постаје лакше. Отприлике знамо шта желимо да напишемо и како ће наш програм функционисати. И док пишемо, тражимо како је то већ имплементирано у постојећим паметним уговорима, или гледамо у код за имплементацију Fift и FunC у званичном репозиторију, или погледајте у службеној документацији.
Врло често сам тражио кључне речи у Телеграм чету где су се окупили сви учесници такмичења и запослени у Телеграму, и десило се да су се током такмичења сви окупили и почели да разговарају о Фифту и ФунЦ-у. Линк на крају чланка.
Време је да пређемо са теорије на праксу.
Припрема окружења за рад са ТОН-ом
Урадио сам све што ће бити описано у чланку о MacOS-у и још једном сам то проверио. Ubuntu 18.04 LTS на Докеру.
Прва ствар коју треба да урадите је да преузмете и инсталирате lite-client са којима можете слати захтеве ТОН-у.
Упутства на званичном веб-сајту описују процес инсталације прилично темељно и јасно, изостављајући неке детаље. Овде пратимо упутства, инсталирајући све недостајуће зависности успут. Нисам сам компајлирао сваки пројекат и инсталирао сам из званичног репозиторијума. 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.
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 parameterslast получает последний созданный блок с сервера.
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, ово је адреса коју ћемо сачувати у датотеци.
Копирао сам функцију за чување адресе са .
// ( 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 12345256 u>B претвара 256-битни ненегативан цео број у бајтове.
"sender.addr" -1 BYTES:0000000000000000000000000000000000000000000000000000000000003039swap мења два горња елемента стека.
"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 -132 i>B претвара 32-битни цео број у бајтове.
"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 BYTES:FFFFFFFFB+ повезује два низа бајтова.
"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.
Узео сам шифру публикације као основу што у званичном репозиторијуму.
Нешто на шта вреди обратити пажњу. Генеришемо складиште паметног уговора и улазну поруку. Након овога се генерише адреса паметног уговора, односно адреса је позната и пре објављивања у ТОН-у. Затим треба да пошаљете неколико грама на ову адресу, а тек након тога да пошаљете датотеку са самим паметним уговором, пошто мрежа узима провизију за чување паметног уговора и операције у њему (валидатори који чувају и извршавају смарт уговор). уговори). .
Затим извршавамо код за објављивање и добијамо 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 грама. И хајде да направимо неколико опклада ради јасноће. Видимо да сајт приказује историју опклада, тренутни проценат добитака и друге корисне информације.
да смо прву добили, другу изгубили.
Афтерворд
Испоставило се да је чланак много дужи него што сам очекивао, можда је могао бити краћи, или можда само за особу која не зна ништа о ТОН-у и жели да напише и објави не тако једноставан паметни уговор са могућношћу интеракције са то. Можда су се неке ствари могле једноставније објаснити.
Можда су неки аспекти имплементације могли бити урађени ефикасније и елегантније, али би тада било потребно још више времена за припрему чланка. Такође је могуће да сам негде погрешио или нешто нисам разумео, па ако радите нешто озбиљно, треба да се ослоните на званичну документацију или званично складиште са ТОН кодом.
Треба напоменути да пошто је сам ТОН још увек у активној фази развоја, може доћи до промена које ће прекинути било који од корака у овом чланку (што се десило док сам писао, већ је исправљено), али општи приступ је мало је вероватно да ће се променити.
Нећу говорити о будућности ТОН-а. Можда ће платформа постати нешто велико и требало би да проведемо време проучавајући је и сада попунимо нишу нашим производима.
Ту је и Вага са Фејсбука, која има потенцијалну публику корисника већу од ТОН-а. Не знам скоро ништа о Ваги, судећи по форуму тамо је много више активности него у ТОН заједници. Иако су програмери и заједница ТОН-а више као ундергроунд, што је такође цоол.
референце
- Званична ТОН документација:
- Званични ТОН репозиторијум:
- Званични новчаник за различите платформе:
- Репозиторијум паметних уговора из овог чланка:
- Линк ка веб локацији паметног уговора:
- Репозиторијум за екстензију за Висуал Студио Цоде за ФунЦ:
- Разговарајте о ТОН-у у Телеграму, што је заиста помогло да се то схвати у почетној фази. Мислим да неће бити грешка ако кажем да су сви који су нешто написали за ТОН ту. Тамо можете тражити и тестне грама.
- Још једно ћаскање о ТОН-у у којем сам нашао корисне информације:
- Прва фаза такмичења:
- Друга фаза такмичења:
Извор: ввв.хабр.цом
