Turning FunC into FunCtional with Haskell: How Serokell won the Telegram Blockchain Competition

You must have heard that Telegram is going to launch Ton blockchain platform. But you might have missed the news that not so long ago Telegram announced a competition for the implementation of one or more smart contracts for this platform.

The Serokell team with rich experience in developing large blockchain projects could not stand aside. We delegated five employees to the competition, and two weeks later they took first place in it under the (not) modest random nickname Sexy Chameleon. In this article, I will talk about how they did it. We hope that in the next ten minutes you will at least read an interesting story, and at most you will find something useful in it that you can apply in your work.

But let's start with a little contextualization.

Competition and its conditions

So, the main tasks of the participants were the implementation of one or more of the proposed smart contracts, as well as making proposals to improve the TON ecosystem. The competition was held from September 24 to October 15, and the results were announced only on November 15. Quite a long time, considering that during this time Telegram managed to hold and announce the results of contests on design and development of applications in C ++ for testing and evaluating the quality of VoIP calls in Telegram.

We have selected two smart contracts from the list proposed by the organizers. For one of them, we used tools distributed with TON, and the second one was implemented in a new language developed by our engineers specifically for TON and built into Haskell.

The choice of a functional programming language is not accidental. In our corporate blog we often talk about why we consider the complexity of functional languages ​​to be a big exaggeration and why we generally prefer them to object-oriented languages. By the way, it also has original of this article.

Why did we decide to participate in the first place?

In short, because our specialization is non-standard and complex projects that require special skills and are often of scientific value to the IT community. We passionately support and popularize open-source development, and cooperate with leading universities in Russia in the field of computer science and mathematics.

The interesting tasks of the competition and involvement in our beloved Telegram project were in themselves an excellent motivation, and the prize fund became an additional incentive. πŸ™‚

TON Blockchain Research

We keep a close eye on new developments in blockchain, artificial intelligence and machine learning and try not to miss a single significant release in each of the areas in which we work. Therefore, by the time the competition started, our team was already familiar with the ideas from TON white paper. However, before working with TON, we did not analyze the technical documentation and the actual source code of the platform, so the first step was quite obvious - a thorough study of the official documentation on Online and project repositories.

By the start of the contest, the code had already been published, so to save time, we decided to look for a manual or a squeeze written by users. Unfortunately, this did not give any result - apart from instructions for building the platform on Ubuntu, we did not find other materials.

The documentation itself turned out to be carefully crafted, but it was difficult to read it at some points. Quite often, we had to go back to one point or another and switch from high-level descriptions of abstract ideas to low-level implementation details.

It would be easier if the spec didn't have a detailed description of the implementation at all. Information about how the virtual machine represents its stack distracts developers who create smart contracts for the TON platform rather than helps them.

Nix: building the project

At Serokell we are big fans nix. We collect our projects with them and deploy them with NixOps, and all our servers have Nix OS. Because of this, all our builds are reproducible and work under any operating system on which Nix can be installed.

So we started by creating Nix overlay with TON assembly expression. With it, compiling TON is as simple as possible:

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

Note that you don't need to install any dependencies. Nix will magically do everything for you, whether you're using NixOS, Ubuntu, or macOS.

Programming for TON

The smart contract code in the TON Network runs on the TON Virtual Machine (TVM). TVM is more complex than most other virtual machines, and has some very interesting features, for example, it can work with continuations ΠΈ links to data.

Moreover, the guys from TON have created three new programming languages:

Five - a universal stack programming language, reminiscent of Forth. His super ability is the ability to interact with TVM.

func is a smart contract programming language that is similar to C and compiled into another language - Fift Assembler.

Fifth Assembler - Fift library for generating binary executable code for TVM. Fift Assembler lacks a compiler. This embeddable domain-specific language (eDSL).

Our competition entries

Finally, it's time to look at the results of our efforts.

Asynchronous payment channel

A payment channel is a smart contract that allows two users to send payments outside of the blockchain. As a result, not only money is saved (there is no commission), but also time (you do not have to wait until the next block is processed). Payments can be arbitrarily small and occur as often as required. At the same time, the parties do not have to trust each other, since the fairness of the final settlement is guaranteed by the smart contract.

We found a fairly simple solution to the problem. Two parties can exchange signed messages, each of which contains two numbers - the full amount paid by each of the participants. These two numbers work like vector clock in traditional distributed systems and set the β€œhappened before” order on transactions. Using this data, the contract will be able to resolve any possible conflict.

In fact, one number is enough to implement this idea, but we left both because this way we could make a more convenient user interface. In addition, we decided to include the amount of the payment in each message. Without it, if the message is lost for some reason, then, although all amounts and the final calculation will be correct, the user may not notice the loss.

To test our idea, we looked for examples of using such a simple and concise payment channel protocol. Surprisingly, we found only two:

  1. Description similar approach, only for the case of a unidirectional link.
  2. Tutorial, which describes the same idea as ours, only without explaining many important details, such as general correctness and conflict resolution procedure.

It became clear that it makes sense to describe our protocol in detail, paying special attention to its correctness. After several iterations, the specification was ready and now you can too look at her.

We implemented the contract in FunC, and we wrote the command line utility for interacting with our contract entirely in Fift, as recommended by the organizers. We could have chosen any other language for our CLI, but we were interested in trying Fift to see how it performs in practice.

To be honest, having worked with Fift, we did not see any good reasons to prefer this language to popular and actively used languages ​​with developed tools and libraries. Programming in a stack language is quite unpleasant, because you have to constantly keep in mind what is where on the stack, and the compiler does not help with this.

Therefore, in our opinion, the only justification for the existence of Fift is its role as a host language for Fift Assembler. But wouldn't it be better to embed the TVM assembler in some existing language, rather than invent a new one for this, in fact, the only purpose?

TVM Haskell eDSL

Now it's time to talk about our second smart contract. We decided to develop a multi-signature wallet, but writing another smart contract in FunC would be too boring. We wanted to add some zest, and that was our own assembly language for TVM.

Like Fift Assembler, our new language is embeddable, only we chose Haskell as the host instead of Fift, which allows us to take full advantage of its advanced type system. When working with smart contracts, where the cost of even a small mistake can be very high, static typing is, in our opinion, a big advantage.

To demonstrate what TVM assembler looks like built into Haskell, we have implemented a standard wallet on it. Here are a few things to watch out for:

  • This contract consists of one function, but you can use as many as you like. When you define a new function in a host language (i.e. Haskell), our eDSL allows you to choose whether you want it to be turned into a separate subroutine in TVM or simply built into the call site.
  • Like Haskell, functions have types that are checked at compile time. In our eDSL, the input type of a function is the type of stack that the function expects, and the result type is the type of stack that will result after the call.
  • There are annotations in the code stacktype, which describe the expected stack type at the point of the call. In the original wallet contract, these were just comments, but in our eDSL they are actually part of the code and are checked at compile time. They can serve as documentation or assertions to help a developer find a problem in case the stack type changes when the code changes. Of course, such annotations do not affect runtime performance, since no TVM code is generated for them.
  • It's still a prototype written in two weeks, so there's still a lot of work to be done on the project. For example, all instances of the classes you see in the code below should be generated automatically.

Here is what the multisig wallet implementation looks like on our 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

The full source code for our eDSL and multisig wallet contract can be found at this repository. And more told in detail about built-in languages, our colleague Georgy Agapov.

Conclusions about the competition and TON

In total, our work took 380 hours (together with familiarity with the documentation, meetings and development itself). Five developers took part in the competition project: CTO, team leader, blockchain platform specialists and Haskell software developers.

We found resources to participate in the contest without difficulty, as the spirit of the hackathon, close teamwork, the need to quickly dive into aspects of new technologies is always exciting. A few sleepless nights in order to achieve maximum results in conditions of limited resources are compensated by invaluable experience and great memories. In addition, working on such tasks is always a good test of the company's processes, since it is extremely difficult to achieve really decent results without well-functioning internal interaction.

Lyrics aside, we were impressed with the amount of work done by the TON team. They managed to build a complex, beautiful, and most importantly, a working system. TON has shown itself to be a platform with great potential. However, in order for this ecosystem to develop, there is still a lot to be done, both in terms of its use in blockchain projects and in terms of improving development tools. We are proud to be part of this process now.

If after reading this article you have any questions or ideas about how to apply TON to solve your problems, write to us We are happy to share our experience.

Source: habr.com

Add a comment