Превращая 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, а на всех наших серверах установлена 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 часов (вместе со знакомством с документацией, совещаниями и непосредственно разработкой). В конкурсном проекте приняли участие пять разработчиков: СТО, тим-лид, специалисты по блокчейн-платформам и разработчики программного обеспечения на Haskell.


    Ресурсы для участия в контесте мы нашли без труда, так как дух хакатона, тесная командная работа, необходимость быстрого погружения в аспекты новых технологий — это всегда увлекательно. Несколько бессонных ночей ради достижения максимального результата в условиях ограниченных ресурсов компенсируются бесценным опытом и отличными воспоминаниями. К тому же работа над подобными задачами — всегда хорошая проверка процессов компании, так как добиться действительно достойных результатов без отлично отлаженного внутреннего взаимодействия крайне тяжело.


    Кроме лирики: мы были впечатлены объемом работы, проделанным командой TON. Им удалось построить сложную, красивую, и самое главное, работающую систему. TON показал себя как платформа, имеющая большой потенциал. Однако для того, чтобы эта экосистема развивалась, нужно сделать еще очень много, как с точки зрения ее использования в блокчейн проектах, так и с точки зрения совершенствования инструментов разработки. Мы гордимся тем, что теперь являемся частью этого процесса.


    Если после прочтения этой статьи у вас остались какие-то вопросы или появились идеи о том, как применить TON для решения ваших задач, напишите нам — мы с радостью поделимся опытом.

    • +15
    • 2,5k
    • 6
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 6

      +1
      Fift — универсальный стековый язык программирования, напоминающий Forth. Его супер-способность — возможность взаимодействовать с TVM.

      Такой суперспособностью обладает софт сделанный с использованием возможностей Форт, если разработчик не закрыл специально данную опцию.

      Для примера nnCron — это компактный (900k), но мощный планировщик и менеджер автоматизации.. по определению имеет такую возможность т.к. код данной программы реализован на Форт (SP-Forth — базовая Форт-система русскоязычного Форт сообщества, а также принятая и американской FIG)

      P.S. Книга по пониманию возможностей Форт языка ЛЕО БРОУДИ «СПОСОБ МЫШЛЕНИЯ — ФОРТ ЯЗЫК И ФИЛОСОФИЯ ДЛЯ РЕШЕНИЯ ЗАДАЧ» (нестареющая литература из далёкого 1984 года)
      … есть и другие изданные книги по Форт (Forth) на русском языке.

      Форум русcкоязычного сообщества пользователей языка Форт
        +3

        Перечитал ваш комментарий три раза, но не понял мысль которую вы пытались донести...

          0
          Реклама языка Forth и его сообщества.
            0
            В статье указана однa «суперспособность» языка Fifth присутствующая в данном проекте, но не раскрыты (и вероятно не задействованы другие мощные возможности Fifth языка отнесённого к Forth), хотя и дана ссылка на Википедию и указано, что авторам проекта было трудно реализовывать базовый код в рамках использования стека Fifth

            P.S. На что и дан был пример, где эта и другие способности Форт языка реализованы в полной мере до реализации своего штатного DSL не менее эффективно в сравнении с Haskell + книга где некоторые нетривиальные техники использования Форт описаны достаточно подробно.

            Интересно с функциональной точки зрения услышать развёрнутое мнение сравнения возможностей Haskell и Factor языка отнесённого к группе конкатенативных, но использующим и функциональную парадигму.

          +1
          Как гарантировать что пользователи перевели друг другу деньги за пределами блокчейна?
          1. Пользователи обмениваются двумя цифрами
          2. Можно было и одной, но лучше двумя
          3. ???
          4. Profit!
          Миллениалы изобрели долговые расписки?

          Ладно, если без шуток, то молодцы, что победили, тем более что на Хаскелле. Но на мой взгляд статью сложно читать, потому что она странно структурирована. Мне кажется, что в будущем статьи такого рода лучше писать чуть-чуть иначе.

          Введение — описать конкурс, написать сумму которую вы выиграли (это же не зарплата — информация всё равно доступна публично), упомянуть Хаскель. В общем, создать впечатление у читателя, что сейчас будет инструкция по загребанию денег функциональной лопатой на блокчейне, а не реклама вашей компании, которую если и упомянуть, то один раз и мимоходом.

          Далее — описать TON со всеми его языками, расписать что/почему вы использовали Nix (опять-таки — без лишнего упоминания компании). В описании первого решения (неспециалистам по блокчейну) из этой статьи не очевидно что именно вы сделали, и как оно работает, но понятно что стековые языки вам не очень зашли, а ещё вы написали спецификацию на 400 строк текста.

          То же самое и со вторым решением — пришлось искать что такое «мультиподпись» что бы понять, в чем вообще смысл того, что вы сделали. Да, листинг кода — это хорошо, заголовок статьи все-таки именно это и обещал.

          Подводя итоги можно пожаловаться на странную документацию у TONа, сравнить свои результаты с другими, отметить что у вас более формальное и корректное решение.

          Ну и под конец уже заключение — там уже самое место что бы описывать что у вас за компания, почему вы пошли на конкурс, как себя чувствовали конкурсанты, и давать все ссылки на ваши блоги и статьи.

          Для сравнения — из статьи на blockchainwire стало более понятно что конкретно вы сделали всего за два абзаца. Здесь же цель статьи и посыл размазывается по тексту и теряется. Вы хотели по рекламировать себя? Похвастаться? Вы хотели порекомендовать инструментарий вашей команды? Или посоветовать всем пересесть на Nix? Или она ориентирована только на тех, кто разбирается в блокчейне и готов последовать по каждой ссылке чтобы читать документацию и я вообще зря сюда влез? В общем, подобные статьи, особенно содержащие столько отсылок к компании должны быть как код на Хаскелле. Логично структурированные, и где каждая часть — чистая и выразительная. Но, опять-таки, это всё — только моё мнение, а сам я и на конкурсах такого уровня не побеждал, и на хабре статьями не делился.
            0

            Спасибо за такой развернутый комментарий! Статья действительно получилась несколько вырванной из контекста (это связано с тем, что основная ее часть — перевод статьи из нашего блога, который ориентирован на тех, кто хорошо знаком с компанией и ее принципами). Но я согласен, было бы лучше абстрагироваться от оригинала и написать новый пост со своей собственной структурой, более подходящей для Хабра. В следующий раз так и поступим: )

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое