Ператвараючы FunC у FunCtional з дапамогай Haskell: як Serokell перамаглі ў Telegram Blockchain Competition

Вы напэўна чулі аб тым, што Telegram збіраецца запусціць блокчейн-платформу Ton. Але вы маглі прапусціць навіну, што не так даўно Telegram аб'явіў конкурс на рэалізацыю аднаго ці некалькіх смарт-кантрактаў для гэтай платформы.

Каманда Serokell з багатым вопытам распрацоўкі буйных блокчейн праектаў не магла застацца ў баку. Мы дэлегавалі на конкурс пецярых супрацоўнікаў, а ўжо праз два тыдні яны занялі ў ім першае месца пад (ня)сціплым рандомным нікам Sexy Chameleon. У гэтым артыкуле я раскажу пра тое, як ім гэта ўдалося. Спадзяемся, за бліжэйшыя дзесяць хвілін вы як мінімум прачытаеце цікавую гісторыю, а як максімум знойдзеце ў ёй нешта карыснае, што зможаце прымяніць у сваёй працы.

Але давайце пачнем з невялікага апускання ў кантэкст.

Конкурс і яго ўмовы

Такім чынам, асноўнымі задачамі ўдзельнікаў сталі рэалізацыя аднаго ці больш з прапанаваных смарт-кантрактаў, а таксама занясенне прапаноў па паляпшэнні экасістэмы TON. Конкурс праходзіў з 24 верасня па 15 кастрычніка, а вынікі аб'явілі толькі 15 лістапада. Даволі доўга, улічваючы, што за гэты час Telegram паспеў правесці і абвясціць вынікі кантэстаў па дызайне і па распрацоўцы прыкладанняў на C++ для тэставання і адзнакі якасці VoIP-званкоў у Telegram.

Мы выбралі са спіса, прапанаванага арганізатарамі, два смарт-кантракты. Для аднаго з іх мы выкарыстоўвалі прылады, якія распаўсюджваюцца разам з TON, а другі рэалізавалі на новай мове, распрацаваным нашымі інжынерамі спецыяльна для TON і убудаваным у Haskell.

Выбар функцыянальнай мовы праграмавання не выпадковы. У нашым карпаратыўным блогу мы часта распавядаем, чаму лічым складанасць функцыянальных моў вялікім перабольшаннем і чаму мы ў цэлым аддаем перавагу іх аб'ектна-арыентаваным. Дарэчы, у ім ёсць і арыгінал гэтага артыкула.

Чаму мы ўвогуле вырашылі ўдзельнічаць

Калі сцісла, таму што наша спецыялізацыя — гэта нестандартныя і складаныя праекты, якія патрабуюць адмысловых навыкаў і часцяком уяўляюць навуковую каштоўнасць для IT-супольнасці. Мы горача падтрымліваем open-source распрацоўку і займаемся яе папулярызацыяй, а таксама супрацоўнічаем з кіроўнымі ўніверсітэтамі Расіі ў вобласці кампутарных навук і матэматыкі.

Цікавыя задачы конкурсу і дачыненне да горача любімага намі праекту Тэлеграм самі па сабе былі выдатнай матывацыяй, ну а прызавы фонд стаў дадатковым стымулам. 🙂

Даследаванне блокчейна TON

Мы пільна сочым за новымі распрацоўкамі ў блокчэйне, штучным інтэлекце і машынным навучанні і стараемся не прапускаць ніводнага значнага рэлізу ў кожнай са сфер, у якіх працуем. Таму да моманту старту конкурсу наша каманда ўжо была знаёмая з ідэямі з TON white paper. Аднак да пачатку працы з TON мы не аналізавалі тэхнічную дакументацыю і фактычны зыходны код платформы, таму першы крок быў дастаткова відавочны - стараннае даследаванне афіцыйнай дакументацыі на сайце і ў рэпазітары праекта.

Да пачатку конкурсу код ужо быў апублікаваны, таму, каб зэканоміць час, мы вырашылі пашукаць кіраўніцтва ці выцісканне, напісанае карыстальнікамі. Нажаль, выніку гэта не дало - акрамя інструкцыі па зборцы платформы на Ubuntu, іншых матэрыялаў мы не знайшлі.

Сама дакументацыя аказалася старанна прапрацаванай, але чытаць яе ў некаторых момантах было складана. Даволі часта нам даводзілася вяртацца да тых ці іншых пунктаў і пераключацца з высокаўзроўневых апісанняў абстрактных ідэй на нізкаўзроўневыя дэталі рэалізацыі.

Было б прасцей, калі б у спецыфікацыі ўвогуле не было падрабязнага апісання рэалізацыі. Інфармацыя аб тым, як віртуальная машына прадстаўляе свой стэк, хутчэй адцягвае распрацоўшчыкаў, якія ствараюць смарт-кантракты для платформы TON, чым дапамагае ім.

Nix: збіраем праект

У Serokell мы вялікія прыхільнікі Nix. Мы збіраем ім нашы праекты і разгортваем іх з дапамогай NixOps, а на ўсіх нашых серверах усталявана АС Nix. Дзякуючы гэтаму ўсе нашы білды ўзнаўляльныя і працуюць пад любой аперацыйнай сістэмай, на якую можна ўсталяваць Nix.

Таму мы пачалі са стварэння Nix overlay з выразам для зборкі TON. З яго дапамогай скампіляваць TON максімальна проста:

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

Заўважце, вам не трэба ўстанаўліваць ніякія залежнасці. Nix магічным чынам зробіць усё за вас па-за залежнасцю ад таго, ці карыстаецеся вы NixOS, Ubuntu ці macOS.

Праграмаванне для TON

Код смарт-кантрактаў у TON Network выконваецца на TON Virtual Machine (TVM). TVM складаней, чым большасць іншых віртуальных машын, і валодае вельмі цікавым функцыяналам, напрыклад яна ўмее працаваць з працягамі (continuations) и спасылкамі на дадзеныя.

Больш за тое, хлопцы з TON стварылі цэлых тры новыя мовы праграмавання:

Fift — універсальная сцяковая мова праграмавання, якая нагадвае Чацвёртае. Яго супер-здольнасць - магчымасць узаемадзейнічаць з TVM.

FunC - мова праграмавання смарт кантрактаў, які падобны на C і кампілюецца ў яшчэ адну мову - Fift Assembler.

Fift Assembler - бібліятэка Fift для генерацыі двайковага выкананага кода для TVM. У Fift Assembler адсутнічае кампілятар. Гэта убудаваная прадметна-арыентаваная мова (eDSL).

Нашы конкурсныя працы

Нарэшце, надышоў час паглядзець на вынікі нашых намаганняў.

Асінхронны плацежны канал

Плацежны канал (payment channel) - смарт-кантракт, які дазваляе двум карыстальнікам адпраўляць плацяжы за межамі блокчейна. У выніку эканомяцца не толькі грошы (адсутнічае камісія), але і час (вам не трэба чакаць, пакуль апрацуецца чарговы блок). Плацяжы могуць быць колькі заўгодна маленькімі і адбывацца настолькі часта, наколькі гэта патрабуецца. Пры гэтым бакам не абавязкова давяраць адзін аднаму, таму што справядлівасць канчатковага разліку гарантавана смарт-кантрактам.

Мы знайшлі даволі простае вырашэнне праблемы. Два бакі могуць абменьвацца падпісанымі паведамленнямі, кожны з якіх утрымлівае два чысла — поўную суму, выплачаную кожным з удзельнікаў. Гэтыя два лікі працуюць як вектарны гадзіннік у традыцыйных размеркаваных сістэмах і задаюць парадак "адбылося да" на транзакцыях. Выкарыстоўваючы гэтыя дадзеныя, кантракт зможа вырашыць любы магчымы канфлікт.

Насамрэч для рэалізацыі гэтай ідэі дастаткова і аднаго ліку, але мы пакінулі абодва, паколькі так мы змаглі зрабіць больш зручны карыстацкі інтэрфейс. Апроч гэтага, мы вырашылі ўключыць у кожнае паведамленне памер плацяжу. Без яго, калі паведамленне па нейкіх прычынах згубіцца, то, хоць усе сумы і канчатковы разлік будуць карэктнымі, карыстач страту можа не заўважыць.

Каб праверыць нашу ідэю, мы пашукалі прыклады выкарыстання такога простага і лаканічнага пратакола плацежнага канала. Да здзіўлення, мы выявілі ўсяго два:

  1. Апісанне падобнага падыходу, толькі для выпадку аднанакіраванага канала.
  2. Тутарыял, у якім апісана тая ж ідэя, што і ў нас, толькі без тлумачэння многіх важных дэталяў, такіх як агульная карэктнасць і працэдура вырашэння канфліктаў.

Стала ясна, што ёсць сэнс падрабязна апісаць наш пратакол, надаўшы адмысловую ўвагу яго карэктнасці. Пасля некалькіх ітэрацый спецыфікацыя была гатова, і зараз вы таксама можаце на яе паглядзець.

Мы рэалізавалі кантракт на FunC, а ўтыліту каманднага радка для ўзаемадзеяння з нашым кантрактам мы цалкам напісалі на Fift, як рэкамендавалі арганізатары. Мы маглі б выбраць любую іншую мову для нашага CLI, але нам было цікава паспрабаваць менавіта Fift, каб паглядзець, як ён пакажа сябе ў справе.

Шчыра кажучы, папрацаваўшы з Fift, мы не ўбачылі важкіх прычын аддаваць перавагу гэтай мове папулярным і актыўна выкарыстоўваемым мовам з развітым інструментарыем і бібліятэкамі. Праграмаваць на стэкавай мове даволі непрыемна, паколькі даводзіцца ўвесь час памятаць што дзе ляжыць у стэку, і кампілятар з гэтым ніяк не дапамагае.

Таму адзіным, на наш погляд, апраўданнем існавання Fift з'яўляецца яго роля ў якасці хост-мовы для Fift Assembler. Але ці не лепш было б убудаваць асэмблер TVM у нейкую існуючую мову, а не прыдумляць новую для гэтай, па сутнасці адзінай, мэты?

TVM Haskell eDSL

Цяпер прыйшоў час расказаць аб другім нашым смарт-кантракце. Мы вырашылі распрацаваць кашалёк з мультыподпісам, але пісаць яшчэ адзін смарт-кантракт на FunC было б занадта сумна. Нам хацелася дадаць якую-небудзь разыначку, і ёю стала наша ўласная мова асэмблера для TVM.

Як і Fift Assembler, наша новая мова ўбудаваная, толькі ў якасці хаста замест Fift мы абралі Haskell, што дазволіла нам у поўнай меры выкарыстаць яго прасунутую сістэму тыпаў. Пры працы са смарт-кантрактамі, дзе кошт нават невялікай памылкі можа быць вельмі высокай, статычная тыпізацыя, на наш погляд, з'яўляецца вялікай перавагай.

Каб прадэманстраваць, як выглядае асэмблер TVM, убудаваны ў Haskell, мы рэалізавалі на ім стандартны кашалёк. Вось некалькі рэчаў, на які варта звярнуць увагу:

  • Гэты кантракт складаецца з адной функцыі, але вы можаце выкарыстоўваць колькі заўгодна. Калі вы вызначаеце новую функцыю на мове хаста (гэта значыць на Haskell), наш eDSL дазваляе вам выбраць, ці вы хочаце, каб яна ператварылася ў асобную падпраграму ў TVM або проста ўбудавана ў месца выкліку.
  • Як і ў Haskell, у функцый ёсць тыпы, якія правяраюцца падчас кампіляцыі. У нашым eDSL тып ўваходу функцыі гэта тып стэка, які функцыя чакае, а тып выніку гэта тып стэка, які атрымаецца пасля выкліку.
  • У кодзе ёсць анатацыі stacktype, якія апісваюць чаканы тып стэка ў кропцы выкліку. У арыгінальным кантракце кашалька гэта былі проста каментарыі, але ў нашым eDSL яны фактычна з'яўляюцца часткай кода і правяраюцца падчас кампіляцыі. Яны могуць служыць дакументацыяй ці сцвярджэннямі, якія дапамагаюць распрацоўніку знайсці праблему ў выпадку, калі пры змене кода тып стэка зменіцца. Само сабой, такія анатацыі не ўплываюць на прадукцыйнасць падчас выканання, паколькі ніякі TVM код для іх не генеруецца.
  • Гэта ўсё яшчэ прататып, напісаны за два тыдні, таму над праектам трэба яшчэ шмат працы. Напрыклад, усе асобнікі класаў, якія вы бачыце ў прыведзеным ніжэй кодзе, павінны генеравацца аўтаматычна.

Вось як выглядае рэалізацыя multisig-кашалька на нашым 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

Поўны зыходны код нашага eDSL і кантракт кашалька з мультыподпісам вы можаце знайсці ў гэтым рэпазітары. А больш падрабязна расказаў пра ўбудаваныя мовы наш калега Георгій Агапаў.

Высновы аб конкурсе і TON

У суме наша праца заняла 380 гадзін (разам са знаёмствам з дакументацыяй, нарадамі і непасрэдна распрацоўкай). У конкурсным праекце прынялі ўдзел пяць распрацоўшчыкаў: СТА, цім-лід, спецыялісты па блокчэйн-платформах і распрацоўшчыкі праграмнага забеспячэння на Haskell.

Рэсурсы для ўдзелу ў кантэсце мы знайшлі без працы, бо дух хакатона, цесная камандная праца, неабходнасць хуткага апускання ў аспекты новых тэхналогій - гэта заўсёды займальна. Некалькі бяссонных начэй дзеля дасягнення максімальнага выніку ва ўмовах абмежаваных рэсурсаў кампенсуюцца бясцэнным досведам і выдатнымі ўспамінамі. Да таго ж праца над падобнымі задачамі – заўсёды добрая праверка працэсаў кампаніі, бо дамагчыся сапраўды годных вынікаў без выдатна адладжанага ўнутранага ўзаемадзеяння вельмі цяжка.

Акрамя лірыкі: мы былі ўражаныя аб'ёмам працы, праведзеным камандай TON. Ім удалося пабудаваць складаную, прыгожую, і самае галоўнае, працуючую сістэму. TON паказаў сябе як платформа, якая мае вялікі патэнцыял. Аднак для таго, каб гэтая экасістэма развівалася, трэба зрабіць яшчэ вельмі шмат, як з пункту гледжання яе выкарыстання ў блокчейн праектах, так і з пункту гледжання ўдасканалення інструментаў распрацоўкі. Мы ганарымся тым, што зараз з'яўляемся часткай гэтага працэсу.

Калі пасля прачытання гэтага артыкула ў вас засталіся нейкія пытанні ці з'явіліся ідэі аб тым, як прымяніць TON для вырашэння вашых задач, напішыце нам - Мы з радасцю падзелімся вопытам.

Крыніца: habr.com

Дадаць каментар