Na pewno słyszałeś o tym, że Telegram . Ale mogłeś przeoczyć wiadomość, że niedawno Telegram na realizację jednego lub kilku smart kontraktów dla tej platformy.
Zespół Serokell z bogatym doświadczeniem w tworzeniu dużych projektów blockchain nie mógł pozostać obojętny. Delegowaliśmy pięciu pracowników do konkursu, a już po dwóch tygodniach zajęli pierwsze miejsce pod (nie)skromnym losowym nickiem Sexy Chameleon. W tym artykule opowiem o tym, jak to osiągnęli. Mamy nadzieję, że w ciągu najbliższych dziesięciu minut przeczytasz interesującą historię, a może nawet znajdziesz w niej coś przydatnego, co będziesz mógł zastosować w swojej pracy.
Ale zacznijmy od krótkiego wprowadzenia w kontekst.
Konkurs i jego warunki
Otóż głównymi zadaniami uczestników było zrealizowanie jednego lub więcej zaproponowanych smart kontraktów oraz przedstawienie propozycji dotyczących usprawnienia ekosystemu TON. Konkurs odbył się od 24 września do 15 października, a wyniki ogłoszono dopiero 15 listopada. Dość długo, biorąc pod uwagę, że w tym czasie Telegram zdążył przeprowadzić i ogłosić wyniki konkursów na projekty graficzne oraz programowanie aplikacji w C++ do testowania i oceny jakości rozmów VoIP w Telegramie.
Wybraliśmy z listy zaproponowanej przez organizatorów dwa smart kontrakty. Do jednego z nich użyliśmy narzędzi dostępnych razem z TON, a drugi zrealizowaliśmy w nowym języku, stworzonym przez naszych inżynierów specjalnie dla TON i zintegrowanym z Haskell.
Wybór funkcjonalnego języka programowania nie jest przypadkowy. W naszym często opowiadamy, dlaczego uważamy złożoność języków funkcjonalnych za dużą przesadę i dlaczego w ogóle wolimy je od obiektowych. Przy okazji znalazł się tam także .
Dlaczego w ogóle zdecydowaliśmy się wziąć udział
Krótko mówiąc, ponieważ naszą specjalnością są niestandardowe i złożone projekty, które wymagają szczególnych umiejętności i często mają wartość naukową dla społeczności IT. Gorąco wspieramy rozwój open-source i zajmujemy się jego popularyzacją, a także współpracujemy z czołowymi uczelniami w Rosji w dziedzinie nauk komputerowych i matematyki.
Interesujące zadania konkursu oraz zaangażowanie w nasz ukochany projekt Telegram były same w sobie doskonałą motywacją, a pula nagród stała się dodatkowym bodźcem. 🙂
Badanie blockchaina TON
Bacznie obserwujemy nowe osiągnięcia w blockchainie, sztucznej inteligencji i uczeniu maszynowym, starając się nie przegapić żadnego znaczącego wydania w każdej z dziedzin, w których działamy. Dlatego w momencie rozpoczęcia konkursu nasz zespół był już zaznajomiony z pomysłami z . Jednak do momentu pracy z TON nie analizowaliśmy dokumentacji technicznej ani rzeczywistego kodu źródłowego platformy, więc pierwszy krok był całkiem oczywisty — dokładne badanie oficjalnej dokumentacji na i w .
Początek konkursu kod został już opublikowany, więc aby zaoszczędzić czas, postanowiliśmy poszukać przewodnika lub podsumowania napisanego przez użytkowników. Niestety, nie przyniosło to rezultatów — oprócz instrukcji dotyczącej budowy platformy na Ubuntu, nie znaleźliśmy żadnych innych materiałów.
Sama dokumentacja okazała się starannie opracowana, ale w niektórych momentach trudno było ją czytać. Dość często musieliśmy wracać do różnych punktów i przełączać się z wysokopoziomowych opisów abstrakcyjnych idei na niskopoziomowe szczegóły implementacji.
Byłoby prościej, gdyby w specyfikacji w ogóle nie było szczegółowego opisu implementacji. Informacje o tym, jak wirtualna maszyna przedstawia swój stos, raczej odwracają uwagę programistów tworzących smart kontrakty dla platformy TON, niż im pomagają.
Nix: budujemy projekt
W Serokell jesteśmy wielkimi fanami . Budujemy z nimi nasze projekty i wdrażamy je za pomocą , a na wszystkich naszych serwerach jest zainstalowany . Dzięki temu wszystkie nasze kompilacje są reprodukowalne i działają na dowolnym systemie operacyjnym, na który można zainstalować Nix.
Dlatego zaczęliśmy od stworzenia . Dzięki niemu skompilowanie TON jest maksymalnie proste:
$ cd ~/.config/nixpkgs/overlays && git clone https://github.com/serokell/ton.nix
$ cd /path/to/ton/repo && nix-shell
[nix-shell]$ cmakeConfigurePhase && makeZauważ, że nie musisz instalować żadnych zależności. Nix w magiczny sposób zrobi wszystko za Ciebie, niezależnie od tego, czy używasz NixOS, Ubuntu czy macOS.
Programowanie dla TON
Kod smart kontraktów w TON Network jest wykonywany na TON Virtual Machine (TVM). TVM jest bardziej skomplikowana niż większość innych maszyn wirtualnych i ma bardzo interesujące funkcje, na przykład potrafi pracować z kontynuacjami (continuations) i linkami do danych.
Co więcej, zespół TON stworzył aż trzy nowe języki programowania:
(prawdopodobnie od liczby — uniwersalny język programowania oparty na stosie, przypominający . Jego supermocą jest możliwość interakcji z TVM.
FunC — język programowania kontraktów, który jest podobny do i kompiluje się do innego języka — Fift Assembler.
Fift Assembler — biblioteka Fift do generowania binarnego kodu wykonywalnego dla TVM. Fift Assembler nie ma kompilatora. To .
Nasze prace konkursowe
W końcu nadszedł czas, aby przyjrzeć się rezultatom naszych wysiłków.
Asynchroniczny kanał płatności
Kanał płatności (payment channel) to smart kontrakt, który pozwala dwóm użytkownikom wysyłać płatności poza blockchainem. W rezultacie oszczędzają nie tylko pieniądze (brak prowizji), ale też czas (nie muszą czekać, aż przetworzony zostanie kolejny blok). Płatności mogą być dowolnie małe i odbywać się tak często, jak to potrzebne. Przy tym stronom nie trzeba ufać, ponieważ sprawiedliwość ostatecznego rozliczenia jest gwarantowana przez smart kontrakt.
Znaleźliśmy dość proste rozwiązanie tego problemu. Dwie strony mogą wymieniać podpisane wiadomości, z których każda zawiera dwie liczby — całkowitą kwotę zapłaconą przez każdego z uczestników. Te dwie liczby działają jak w tradycyjnych systemach rozproszonych i określają porządek zdarzeń «zdarzyło się przed» w transakcjach. Korzystając z tych danych, kontrakt będzie mógł rozwiązać każdy możliwy konflikt.
W rzeczywistości, aby zrealizować ten pomysł, wystarczy jedna liczba, ale оставилиśmy obie, ponieważ w ten sposób mogliśmy stworzyć wygodniejszy interfejs użytkownika. Oprócz tego postanowiliśmy dołączyć do każdej wiadomości rozmiar płatności. Bez niego, jeśli wiadomość z jakiegoś powodu zaginie, chociaż wszystkie kwoty i ostateczne rozliczenie będą poprawne, użytkownik może nie zauważyć straty.
Aby sprawdzić nasz pomysł, poszukaliśmy przykładów użycia takiego prostego i zwięzłego protokołu płatności. Ku naszemu zaskoczeniu, znaleźliśmy tylko dwa:
- podobne podejścia, tylko w przypadku jednokierunkowego kanału.
- , który opisuje tę samą ideę co my, ale bez wyjaśnienia wielu istotnych szczegółów, takich jak ogólna poprawność oraz procedura rozwiązywania konfliktów.
Zrozumieliśmy, że warto szczegółowo opisać nasz protokół, zwracając szczególną uwagę na jego poprawność. Po kilku iteracjach specyfikacja była gotowa, i teraz również możecie .
Zrealizowaliśmy kontrakt w FunC, a narzędzie wiersza poleceń do interakcji z naszym kontraktem w pełni napisaliśmy w Fift, jak zalecali organizatorzy. Mogliśmy wybrać dowolny inny język dla naszego CLI, ale chcieliśmy spróbować właśnie Fift, aby zobaczyć, jak się sprawdzi w praktyce.
Szczerze mówiąc, pracując z Fift, nie dostrzegliśmy istotnych powodów, aby preferować ten język nad popularne i powszechnie używane języki z rozwiniętymi narzędziami i bibliotekami. Programowanie w języku stosowym jest dość nieprzyjemne, ponieważ trzeba nieustannie pamiętać, co gdzie leży w stosie, a kompilator w tym nie pomaga.
Dlatego naszym zdaniem jedynym uzasadnieniem istnienia Fift jest jego rola jako języka hosta dla Fift Assembler. Ale czy nie lepiej byłoby wbudować assembler TVM w jakiś istniejący język, zamiast wymyślać nowy dla tej, w zasadzie jedynej, celu?
TVM Haskell eDSL
Teraz nadszedł czas, aby opowiedzieć o naszym drugim smart kontrakcie. Postanowiliśmy stworzyć portfel z wieloma podpisami, ale pisanie kolejnego smart kontraktu w FunC byłoby zbyt nudne. Chcieliśmy dodać jakiś smaczek, a nim był nasz własny język assemblera dla TVM.
Podobnie jak Fift Assembler, nasz nowy język jest wbudowany, tylko jako host zamiast Fift wybraliśmy Haskell, co umożliwiło nam w pełni wykorzystać jego zaawansowany system typów. Pracując z smart kontraktami, gdzie koszt nawet drobnego błędu może być bardzo wysoki, statyczna typizacja, naszym zdaniem, jest dużą zaletą.
Aby pokazać, jak wygląda assembler TVM wbudowany w Haskella, zaimplementowaliśmy standardowy portfel. Oto kilka rzeczy, na które warto zwrócić uwagę:
- Ten kontrakt składa się z jednej funkcji, ale możesz używać ich ile chcesz. Kiedy definiujesz nową funkcję w języku hosta (czyli w Haskellu), nasza eDSL pozwala ci wybrać, czy chcesz, aby zamieniła się w osobną podprogramę w TVM, czy aby była po prostu wbudowana w miejsce wywołania.
- Podobnie jak w Haskellu, funkcje mają typy, które są sprawdzane w czasie kompilacji. W naszej eDSL typ wejściowy funkcji to typ stosu, którego funkcja oczekuje, a typ wyniku to typ stosu, który powstanie po wywołaniu.
- W kodzie znajdują się adnotacje
stacktype, opisujące oczekiwany typ stosu w punkcie wywołania. W oryginalnym kontrakcie portfela były to po prostu komentarze, ale w naszej eDSL są one faktycznie częścią kodu i są sprawdzane w czasie kompilacji. Mogą służyć jako dokumentacja lub twierdzenia, które pomagają programiście zlokalizować problem, jeśli typ stosu zmieni się przy modyfikacji kodu. Oczywiście, takie adnotacje nie wpływają na wydajność w czasie wykonywania, ponieważ nie jest generowany żaden kod TVM dla nich. - To wciąż prototyp, napisany w ciągu dwóch tygodni, więc przed projektem jest jeszcze wiele pracy. Na przykład, wszystkie instancje klas, które widzisz w poniższym kodzie, muszą być generowane automatycznie.
Oto jak wygląda implementacja portfela multisig w naszej eDSL:
main :: IO ()
main = putText $ pretty $ declProgram procedures methods
where
procedures =
[ ("recv_external", decl recvExternal)
, ("recv_internal", decl recvInternal)
]
methods =
[ ("seqno", declMethod getSeqno)
]
data Storage = Storage
{ sCnt :: Word32
, sPubKey :: PublicKey
}
instance DecodeSlice Storage where
type DecodeSliceFields Storage = [PublicKey, Word32]
decodeFromSliceImpl = do
decodeFromSliceImpl @Word32
decodeFromSliceImpl @PublicKey
instance EncodeBuilder Storage where
encodeToBuilder = do
encodeToBuilder @Word32
encodeToBuilder @PublicKey
data WalletError
= SeqNoMismatch
| SignatureMismatch
deriving (Eq, Ord, Show, Generic)
instance Exception WalletError
instance Enum WalletError where
toEnum 33 = SeqNoMismatch
toEnum 34 = SignatureMismatch
toEnum _ = error "Uknown MultiSigError id"
fromEnum SeqNoMismatch = 33
fromEnum SignatureMismatch = 34
recvInternal :: '[Slice] :-> '[]
recvInternal = drop
recvExternal :: '[Slice] :-> '[]
recvExternal = do
decodeFromSlice @Signature
dup
preloadFromSlice @Word32
stacktype @[Word32, Slice, Signature]
-- cnt cs sign
pushRoot
decodeFromCell @Storage
stacktype @[PublicKey, Word32, Word32, Slice, Signature]
-- pk cnt' cnt cs sign
xcpu @1 @2
stacktype @[Word32, Word32, PublicKey, Word32, Slice, Signature]
-- cnt cnt' pk cnt cs sign
equalInt >> throwIfNot SeqNoMismatch
push @2
sliceHash
stacktype @[Hash Slice, PublicKey, Word32, Slice, Signature]
-- hash pk cnt cs sign
xc2pu @0 @4 @4
stacktype @[PublicKey, Signature, Hash Slice, Word32, Slice, PublicKey]
-- pubk sign hash cnt cs pubk
chkSignU
stacktype @[Bool, Word32, Slice, PublicKey]
-- ? cnt cs pubk
throwIfNot SignatureMismatch
accept
swap
decodeFromSlice @Word32
nip
dup
srefs @Word8
pushInt 0
if IsEq
then ignore
else do
decodeFromSlice @Word8
decodeFromSlice @(Cell MessageObject)
stacktype @[Slice, Cell MessageObject, Word8, Word32, PublicKey]
xchg @2
sendRawMsg
stacktype @[Slice, Word32, PublicKey]
endS
inc
encodeToCell @Storage
popRoot
getSeqno :: '[] :-> '[Word32]
getSeqno = do
pushRoot
cToS
preloadFromSlice @Word32Pełny kod źródłowy naszego eDSL oraz kontraktu portfela z wieloma podpisami można znaleźć w A bardziej na temat wbudowanych języków nasz kolega Georgij Agapow.
Wnioski z konkursu i TON
Łącznie nasza praca zajęła 380 godzin (w tym zapoznanie się z dokumentacją, spotkaniami oraz samą pomocą programistyczną). W konkursowym projekcie uczestniczyło pięciu programistów: CTO, lider zespołu, specjaliści od platform blockchain i programiści oprogramowania w Haskellu.
Zasoby do udziału w konkursie znaleźliśmy bez trudu, ponieważ duch hackathonu, bliska praca zespołowa oraz konieczność szybkiego wdrożenia się w aspekty nowych technologii są zawsze pasjonujące. Kilka bezsennych nocy w dążeniu do osiągnięcia maksymalnych wyników w warunkach ograniczonych zasobów rekompensuje bezcenne doświadczenie i wspaniałe wspomnienia. Ponadto praca nad takimi zadaniami zawsze stanowi dobrą weryfikację procesów w firmie, ponieważ uzyskanie naprawdę dobrych rezultatów bez doskonale działającej wewnętrznej współpracy jest niezwykle trudne.
Poza liryką: byliśmy pod wrażeniem ogromu pracy, jaką wykonał zespół TON. Udało im się zbudować złożony, piękny, a co najważniejsze, działający system. TON pokazał się jako platforma o dużym potencjale. Jednak aby ten ekosystem mógł się rozwijać, trzeba zrobić jeszcze bardzo wiele, zarówno w kontekście jego wykorzystania w projektach blockchainowych, jak i w zakresie doskonalenia narzędzi deweloperskich. Jesteśmy dumni, że teraz jesteśmy częścią tego procesu.
Jeśli po przeczytaniu tego artykułu pozostały Ci jakieś pytania lub pojawiły się pomysły, jak zastosować TON do rozwiązania Twoich zadań, — z przyjemnością podzielimy się doświadczeniem.
Źródło: habr.com
