Перетворюючи 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 ми великі шанувальники Шухер. Ми збираємо їм наші проекти та розвертаємо їх за допомогою 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

Додати коментар або відгук