Przekształcanie FunC w FunCtional z Haskellem: jak Serokell wygrał konkurs Telegram Blockchain

Prawdopodobnie słyszałeś ten Telegram wkrótce uruchomi platformę blockchain Ton. Ale mogłeś przegapić wiadomość, która niedawno pojawiła się w Telegramie ogłosił konkurs na wdrożenie jednego lub większej liczby inteligentnych kontraktów dla tej platformy.

Zespół Serokell, posiadający duże doświadczenie w tworzeniu dużych projektów blockchain, nie mógł stać z boku. Do konkursu delegowaliśmy pięciu pracowników, którzy już dwa tygodnie później zajęli w nim pierwsze miejsce pod (nie)skromnym, losowym pseudonimem Seksowny Kameleon. W tym artykule opowiem o tym, jak to zrobili. Mamy nadzieję, że w ciągu najbliższych dziesięciu minut przeczytacie chociaż ciekawą historię, a co najwyżej znajdziecie w niej coś przydatnego, co będziecie mogli zastosować w swojej pracy.

Ale zacznijmy od małego kontekstu.

Konkurencja i jej uwarunkowania

Zatem głównymi zadaniami uczestników było wdrożenie jednego lub większej liczby proponowanych inteligentnych kontraktów, a także przedstawienie propozycji ulepszenia ekosystemu TON. Konkurs trwał od 24 września do 15 października, a wyniki ogłoszono dopiero 15 listopada. Całkiem długo, biorąc pod uwagę, że w tym czasie Telegramowi udało się przeprowadzić i ogłosić wyniki konkursów na projektowanie i rozwój aplikacji w C++ do testowania i oceny jakości połączeń VoIP w Telegramie.

Z listy zaproponowanej przez organizatorów wybraliśmy dwa smart kontrakty. W jednym z nich wykorzystaliśmy narzędzia dystrybuowane z TON, a drugie zaimplementowaliśmy w nowym języku opracowanym przez naszych inżynierów specjalnie dla TON i wbudowanym w Haskell.

Wybór funkcjonalnego języka programowania nie jest przypadkowy. W naszym blog firmowy Często rozmawiamy o tym, dlaczego uważamy, że złożoność języków funkcjonalnych jest ogromną przesadą i dlaczego generalnie wolimy je od języków obiektowych. Nawiasem mówiąc, zawiera również oryginał tego artykułu.

Dlaczego w ogóle zdecydowaliśmy się wziąć udział?

Krótko mówiąc, ponieważ naszą specjalizacją są projekty niestandardowe i złożone, które wymagają specjalnych umiejętności i często mają wartość naukową dla społeczności IT. Mocno wspieramy rozwój open source i angażujemy się w jego popularyzację, a także współpracujemy z wiodącymi rosyjskimi uczelniami w dziedzinie informatyki i matematyki.

Ciekawe zadania konkursowe i zaangażowanie w nasz ukochany projekt Telegram były same w sobie doskonałą motywacją, ale fundusz nagród stał się dodatkową zachętą. 🙂

Badania blockchain TON

Uważnie monitorujemy nowe osiągnięcia w blockchain, sztucznej inteligencji i uczeniu maszynowym i staramy się nie przegapić ani jednej istotnej premiery w każdym z obszarów, w których pracujemy. Dlatego zanim konkurs się rozpoczął, nasz zespół był już zaznajomiony z pomysłami z Biała księga TON. Jednak przed rozpoczęciem pracy z TON nie analizowaliśmy dokumentacji technicznej i faktycznego kodu źródłowego platformy, więc pierwszy krok był dość oczywisty – dokładne przestudiowanie oficjalnej dokumentacji na witryna internetowa i repozytoria projektów.

Zanim konkurs się rozpoczął, kod był już opublikowany, więc chcąc zaoszczędzić czas, postanowiliśmy poszukać poradnika lub podsumowania napisanego przez użytkownicy. Niestety nie dało to żadnych rezultatów – poza instrukcją montażu platformy na Ubuntu nie znaleźliśmy żadnych innych materiałów.

Sama dokumentacja została dobrze zbadana, ale w niektórych obszarach była trudna do odczytania. Dość często musieliśmy wracać do pewnych punktów i przechodzić od opisów abstrakcyjnych pomysłów na wysokim poziomie do szczegółów implementacji na niskim poziomie.

Byłoby łatwiej, gdyby specyfikacja w ogóle nie zawierała szczegółowego opisu realizacji. Informacje o tym, jak maszyna wirtualna reprezentuje swój stos, częściej odwracają uwagę programistów tworzących inteligentne kontrakty dla platformy TON, niż im pomagają.

Nix: składanie projektu w całość

W Serokell jesteśmy wielkimi fanami Wodnik. Zbieramy z nimi nasze projekty i wdrażamy je za pomocą NixOpsi zainstalowane na wszystkich naszych serwerach System operacyjny Nix. Dzięki temu wszystkie nasze buildy są powtarzalne i działają na każdym systemie operacyjnym, na którym można zainstalować Nixa.

Zaczęliśmy więc od tworzenia Nakładka Nix z wyrażeniem do montażu TON. Z jego pomocą kompilacja TON jest tak prosta, jak to tylko możliwe:

$ cd ~/.config/nixpkgs/overlays && git clone https://github.com/serokell/ton.nix
$ cd /path/to/ton/repo && nix-shell
[nix-shell]$ cmakeConfigurePhase && make

Pamiętaj, ż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 inteligentnego kontraktu w sieci TON działa na maszynie wirtualnej TON (TVM). TVM jest bardziej złożony niż większość innych maszyn wirtualnych i ma bardzo interesującą funkcjonalność, z którą może na przykład współpracować kontynuacje и linki do danych.

Co więcej, chłopaki z TON stworzyli trzy nowe języki programowania:

Pięć to uniwersalny język programowania stosu, który przypomina Naprzód. Jego super umiejętnością jest możliwość interakcji z TVM.

ZabawaC to inteligentny język programowania kontraktów, podobny do C i jest skompilowany na inny język - Fift Assembler.

Piąty Asembler — Biblioteka Fift do generowania binarnego kodu wykonywalnego dla TVM. Piąty asembler nie ma kompilatora. Ten Wbudowany język specyficzny dla domeny (eDSL).

Nasz konkurs działa

Wreszcie nadszedł czas, aby przyjrzeć się wynikom naszych wysiłków.

Asynchroniczny kanał płatności

Kanał płatności to inteligentny kontrakt, który umożliwia dwóm użytkownikom wysyłanie płatności poza blockchain. Dzięki temu oszczędzasz nie tylko pieniądze (nie ma prowizji), ale także czas (nie musisz czekać na przetworzenie kolejnego bloku). Płatności mogą być tak małe, jak to pożądane i tak często, jak to konieczne. W takim przypadku strony nie muszą sobie ufać, gdyż rzetelność ostatecznego rozliczenia gwarantuje inteligentny kontrakt.

Znaleźliśmy dość proste rozwiązanie problemu. Dwie strony mogą wymieniać podpisane wiadomości, każda zawierająca dwa numery — pełną kwotę zapłaconą przez każdą ze stron. Te dwie liczby działają podobnie zegar wektorowy w tradycyjnych systemach rozproszonych i ustalają kolejność transakcji „zdarzały się wcześniej”. Korzystając z tych danych, umowa będzie w stanie rozwiązać ewentualny konflikt.

Właściwie do realizacji tego pomysłu wystarczy jedna liczba, ale zostawiliśmy obie, ponieważ w ten sposób moglibyśmy stworzyć wygodniejszy interfejs użytkownika. Dodatkowo postanowiliśmy w każdej wiadomości podać kwotę płatności. Bez tego, jeśli z jakiegoś powodu wiadomość zaginie, to mimo że wszystkie kwoty i ostateczne obliczenia będą prawidłowe, użytkownik może nie zauważyć straty.

Aby przetestować nasz pomysł, szukaliśmy przykładów zastosowania tak prostego i zwięzłego protokołu kanału płatności. Co zaskakujące, znaleźliśmy tylko dwa:

  1. Opis podobne podejście, tylko w przypadku kanału jednokierunkowego.
  2. Instruktaż, który opisuje tę samą ideę co nasza, ale bez wyjaśnienia wielu ważnych szczegółów, takich jak ogólna poprawność i procedury rozwiązywania konfliktów.

Stało się jasne, że warto szczegółowo opisać nasz protokół, zwracając szczególną uwagę na jego poprawność. Po kilku iteracjach specyfikacja była gotowa i teraz Ty też możesz. Spójrz na nią.

Kontrakt zaimplementowaliśmy w FunC, a narzędzie wiersza poleceń umożliwiające interakcję z naszym kontraktem napisaliśmy w całości w Fift, zgodnie z zaleceniami organizatorów. Mogliśmy wybrać dowolny inny język dla naszego CLI, ale chcieliśmy wypróbować Fit, aby zobaczyć, jak radzi sobie w praktyce.

Szczerze mówiąc, po współpracy z Fift nie widzieliśmy żadnych przekonujących powodów, aby preferować ten język od popularnych i aktywnie używanych języków z rozwiniętymi narzędziami i bibliotekami. Programowanie w języku opartym na stosie jest dość nieprzyjemne, ponieważ trzeba stale mieć w głowie to, co jest na 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 byłoby lepiej osadzić asembler TVM w jakimś istniejącym języku, zamiast wymyślać nowy, w zasadzie wyłącznie w tym celu?

TVM Haskell eDSL

Nadszedł czas, aby porozmawiać o naszym drugim inteligentnym kontrakcie. Postanowiliśmy opracować portfel z wieloma podpisami, ale pisanie kolejnej inteligentnej umowy w FunC byłoby zbyt nudne. Chcieliśmy dodać trochę pikanterii i był to nasz własny język asemblera dla TVM.

Podobnie jak Fift Assembler, nasz nowy język jest osadzony, ale jako hosta wybraliśmy Haskell zamiast Fift, co pozwoliło nam w pełni wykorzystać jego zaawansowany system typów. W przypadku pracy z inteligentnymi kontraktami, gdzie koszt nawet małego błędu może być bardzo wysoki, naszym zdaniem statyczne pisanie jest dużą zaletą.

Aby zademonstrować jak wygląda asembler TVM osadzony w Haskell, zaimplementowaliśmy na nim standardowy portfel. Oto kilka rzeczy, na które warto zwrócić uwagę:

  • Ta umowa składa się z jednej funkcji, ale możesz używać ich tyle, ile chcesz. Kiedy definiujesz nową funkcję w języku hosta (np. Haskell), nasz eDSL pozwala Ci wybrać, czy ma ona stać się oddzielną procedurą w TVM, czy po prostu wstawioną w miejscu wywołania.
  • Podobnie jak Haskell, funkcje mają typy sprawdzane w czasie kompilacji. W naszym eDSL typem wejściowym funkcji jest typ stosu, jakiego oczekuje funkcja, a typem wyniku jest typ stosu, który zostanie utworzony po wywołaniu.
  • Kod zawiera adnotacje stacktype, opisujący oczekiwany typ stosu w punkcie wywołania. W oryginalnej umowie dotyczącej portfela były to tylko komentarze, ale w naszym eDSL są one w rzeczywistości częścią kodu i są sprawdzane w czasie kompilacji. Mogą służyć jako dokumentacja lub instrukcje pomagające programiście znaleźć problem w przypadku zmiany kodu i zmiany typu stosu. Oczywiście takie adnotacje nie wpływają na wydajność środowiska wykonawczego, ponieważ nie jest dla nich generowany żaden kod TVM.
  • To wciąż prototyp napisany w dwa tygodnie, więc nad projektem jest jeszcze dużo pracy do wykonania. Na przykład wszystkie instancje klas widoczne w poniższym kodzie powinny zostać wygenerowane automatycznie.

Tak wygląda wdrożenie portfela multisig na naszym 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 naszej umowy dotyczącej eDSL i portfela z wieloma podpisami można znaleźć pod adresem to repozytorium. I więcej szczegółowo opowiedziane o językach wbudowanych, nasz kolega Georgy Agapov.

Wnioski dotyczące konkursu i TON

W sumie nasza praca zajęła 380 godzin (wliczając zapoznanie się z dokumentacją, spotkania i faktyczny rozwój). W projekcie konkursowym wzięło udział pięciu programistów: CTO, lider zespołu, specjaliści od platformy blockchain oraz programiści Haskell.

Znaleźliśmy zasoby, aby bez trudu wziąć udział w konkursie, ponieważ duch hackatonu, ścisła praca zespołowa i potrzeba szybkiego zanurzenia się w aspekty nowych technologii są zawsze ekscytujące. Kilka nieprzespanych nocy w celu osiągnięcia maksymalnych rezultatów w warunkach ograniczonych zasobów rekompensuje bezcenne doświadczenie i doskonałe wspomnienia. Ponadto praca nad takimi zadaniami jest zawsze dobrym sprawdzianem procesów w firmie, ponieważ niezwykle trudno jest osiągnąć naprawdę przyzwoite wyniki bez dobrze funkcjonującej interakcji wewnętrznej.

Teksty na bok: byliśmy pod wrażeniem ilości pracy włożonej przez zespół TON. Udało im się zbudować złożony, piękny i co najważniejsze działający system. TON udowodnił, że jest platformą o ogromnym potencjale. Aby jednak ten ekosystem mógł się rozwijać, należy zrobić znacznie więcej, zarówno jeśli chodzi o jego wykorzystanie w projektach blockchain, jak i pod kątem udoskonalania narzędzi programistycznych. Jesteśmy dumni, że możemy teraz być częścią tego procesu.

Jeśli po przeczytaniu tego artykułu nadal masz pytania lub pomysły, jak wykorzystać TON do rozwiązania swoich problemów, Napisz do nas – chętnie podzielimy się naszym doświadczeniem.

Źródło: www.habr.com

Dodaj komentarz