Transformowanie FunC w FunCtional z Haskell: jak Serokell wygrał w Telegram Blockchain Competition

Na pewno słyszałeś o tym, że Telegram zamierza uruchomić platformę blockchain Ton. Ale mogłeś przeoczyć wiadomość, że niedawno Telegram ogłosił konkurs 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 blogu korporacyjnym 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 oryginał tego artykułu.

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 ton white paper. 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 stronie i w repozytorium projektu..

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 Nix. Budujemy z nimi nasze projekty i wdrażamy je za pomocą NixOps, a na wszystkich naszych serwerach jest zainstalowany NixOS. 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 overlay Nix z wyrażeniem do budowy TON. 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 && make

Zauważ, ż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 Forth. Jego supermocą jest możliwość interakcji z TVM.

FunC — język programowania kontraktów, który jest podobny do C 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 wbudowany język zorientowany na domenę (eDSL).

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 wektorowe zegary 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:

  1. Opis podobne podejścia, tylko w przypadku jednokierunkowego kanału.
  2. Tutorial, 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 na nią spojrzeć.

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 @Word32

Pełny kod źródłowy naszego eDSL oraz kontraktu portfela z wieloma podpisami można znaleźć w tej repozytorium. A bardziej szczegółowo opisał 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ń, napisz do nas — z przyjemnością podzielimy się doświadczeniem.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster