Вы наверняка слышали о том, что 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) — смарт-контракт, который позволяет двум пользователям отправлять платежи за пределами блокчейна. В результате экономятся не только деньги (отсутствует комиссия), но и время (вам не надо ждать, пока обработается очередной блок). Платежи могут быть сколь угодно маленькими и происходить настолько часто, насколько это требуется. При этом сторонам не обязательно доверять друг другу, так как справедливость окончательного расчета гарантирована смарт-контрактом.
Мы нашли довольно простое решение проблемы. Две стороны могут обмениваться подписанными сообщениями, каждое из которых содержит два числа — полную сумму, уплаченную каждым из участников. Эти два числа работают как векторные часы в традиционных распределенных системах и задают порядок «произошло до» на транзакциях. Используя эти данные, контракт сможет разрешить любой возможный конфликт.
На самом деле для реализации этой идеи достаточно и одного числа, но мы оставили оба, поскольку так мы смогли сделать более удобный пользовательский интерфейс. Помимо этого, мы решили включить в каждое сообщение размер платежа. Без него, если сообщение по каким-то причинам потеряется, то, хотя все суммы и окончательный рассчет будут корректными, пользователь потерю может не заметить.
Чтобы проверить нашу идею, мы поискали примеры использования такого простого и лаконичного протокола платежного канала. К удивлению, мы обнаружили всего два:
- Описание похожего подхода, только для случая однонаправленного канала.
- Туториал, в котором описана та же идея, что и у нас, только без объяснения многих важных деталей, таких как общая корректность и процедура разрешения конфликтов.
Стало ясно, что есть смысл подробно описать наш протокол, уделив особое внимание его корректности. После нескольких итераций спецификация была готова, и теперь вы тоже можете на неё посмотреть.
Мы реализовали контракт на 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 для решения ваших задач, напишите нам — мы с радостью поделимся опытом.
Автор: JagaJaga