Превръщане на FunC в FunCtional с помощта на Haskell: как Serokell спечели в Telegram Blockchain Competition

Вие вероятно сте чували, че Telegram планира да стартира блокчейн платформа Ton. Но може да сте пропуснали новината, че неотдавна Telegram обяви конкурс за реализиране на един или повече смарт договори за тази платформа.

Екипът на Serokell с богат опит в разработката на големи блокчейн проекти не можа да остане безучастен. Делегирахме петима служители на конкурса и вече след две седмици те спечелиха първото място с не(толкова)скромния псевдоним Sexy Chameleon. В тази статия ще разкажа как успяха да го постигнат. Надяваме се, че в следващите десет минути ще прочетете интересна история, а в най-добрия случай ще намерите нещо полезно, което да приложите в работата си.

Но нека започнем с малко задълбочаване в контекста.

Конкурсът и неговите условия

И така, основните задачи на участниците бяха реализация на един или повече от предложените смарт договори, а също така внасяне на предложения за подобрение на екосистемата TON. Конкурсът премина от 24 септември до 15 октомври, а резултатите бяха обявени едва на 15 ноември. Доста дълго, като се има предвид, че за това време Telegram успя да проведе и обяви резултатите от конкурсите по дизайн и за разработка на приложения на C++ за тестване и оценка на качеството на VoIP обажданията в Telegram.

Избрахме два смарт договора от списъка, предложен от организаторите. За един от тях използвахме инструментите, разпространявани с TON, а вторият реализирахме на нов език, разработен от нашите инженери специално за TON и вграден в Haskell.

Изборът на функционален език за програмиране не е случаен. В нашия корпоративен блог често разказваме защо считаме, че сложността на функционалните езици е голямо преувеличение и защо в общи линии предпочитаме тях пред обектно-ориентираните. Между другото, там има и оригинал на тази статия.

Защо всъщност решихме да участваме

Накратко, защото нашата специализация са нестандартни и сложни проекти, изискващи специални умения и често представляващи научна стойност за ИТ общността. Горещо подкрепяме open-source развитието и се занимаваме с популяризирането му, както и си сътрудничим с водещи университети в Русия в областта на компютърните науки и математиката.

Интересните задачи на конкурса и свързаността с проекта Телеграм, който обожаваме, сами по себе си бяха отлична мотивация, а наградният фонд стана допълнителен стимул. 🙂

Проучване на блокчейна TON

Внимателно следим новите разработки в блокчейна, изкуствения интелект и машинното обучение и се стараем да не пропуснем значителни релизи във всяка от сферите, в които работим. Така че до момента, в който конкурсът стартира, нашият екип вече беше запознат с идеите от TON white paper. Въпреки това, преди да започнем работа с TON, не бяхме анализирали техническата документация и фактическия изходен код на платформата, поради което първата стъпка беше достатъчно очевидна — подробно проучване на официалната документация на сайта и в репозитория на проекта.

Към началото на конкурса кодът вече беше публикуван, така че, за да спестим време, решихме да потърсим ръководство или резюме, написано от потребители. За съжаление, това не даде резултат — освен инструкциите за сглобяване на платформата на Ubuntu, не намерихме други материали.

Самата документация се оказа добре разработена, но в някои моменти я беше трудно да се чете. Доста често се налагаше да се връщаме към различни точки и да превключваме от високорниви описания на абстрактни идеи към низкорниви детайли на реализацията.

Беше по-лесно, ако в спецификацията изобщо нямаше подробно описание на реализацията. Информацията за това как виртуалната машина представя своя стек, по-скоро отклонява разработчиците, създаващи смарт договори за платформата TON, отколкото да им помага.

Nix: сглобяваме проекта

В Serokell сме големи почитатели на Nix. Ние сглобяваме нашите проекти с него и ги разгръщаме с помощта на NixOps, а на всичките ни сървъри е инсталирано NixOS. Благодарение на това всички наши билдове са възпроизводими и работят под всяка операционна система, на която може да се инсталира 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 — универсален стеков език за програмиране, наподобяващ Forth. Неговата суперспособност е възможността да взаимодейства с 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 часа (включително запознаване с документацията, срещи и самата разработка). В конкурсния проект участваха петима разработчици: CTO, тим-лидер, специалисти по блокчейн платформи и разработчици на софтуер на Haskell.

Общо, нашата работа отне 380 часа (включително запознаване с документацията, срещи и самата разработка). В конкурсния проект участваха петима разработчици: CTO, тимлидер, специалисти по блокчейн платформи и софтуерни разработчици на Haskell.

Ресурсите за участие в конкурса намерихме без трудности, тъй като духът на хакатона, тясното екипно сътрудничество и необходимостта от бързо навлизане в аспекти на новите технологии — винаги е вълнуващо. Няколко безсънни нощи за постигане на максимален резултат при ограничени ресурси са компенсирани с безценен опит и отлични спомени. Освен това работата по подобни задачи — винаги е добра проверка на процесите в компанията, тъй като е много трудно да се постигат наистина достойни резултати без отлично налагано вътрешно взаимодействие.

Освен лириката: впечатлени сме от обема работа, извършена от екипа на TON. Те успяха да построят сложна, красива и най-важното, работеща система. TON се показа като платформа с голям потенциал. Но за да се развива тази екосистема, трябва да направим още много, както от гледна точка на нейното използване в блокчейн проекти, така и от гледна точка на усъвършенстване на средствата за разработка. Гордеем се, че сега сме част от този процес.

Ако след прочитането на тази статия имате още въпроси или идеи за това как да приложите TON за решаване на вашите задачи, пишете ни — с радост ще споделим опит.

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster