TON: Is Haskell More Fun than FunC?

Telegram Open Network

Telegram Open Network is a relatively new smart-contracts platform developed by the team behind the Telegram messenger. It was announced in late 2017 and the first source code was published in September this year. Five weeks ago, they started a competition. In it, developers were asked to either implement a smart-contract or contribute to the platform in one way or another.

TON Blockchain research

We always try to keep on top of recent developments in the areas that we work in, blockchain being one of them, thus we were already familiar with the ideas from the TON white paper. However, we hadn’t looked at the technical documentation and the actual source code of the platform before, so this was an obvious first step. If you are interested too, you can find the official documentation at https://test.ton.org and in the source repository.

Nix: Building the code

Here at Serokell, we are huge fans of Nix: all our servers run on NixOS, we build our projects with Nix and deploy using NixOps. It helps us make sure builds are reproducible and will work on any OS supported by Nix without us worrying about OS or distribution specific aspects of the build.

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

Programming for TON

The code of smart-contracts deployed to the TON Network is executed by a virtual machine called the TON Virtual Machine (TVM). More complex than most virtual machines, this one provides some quite unconventional capabilities, such as native support for continuations and data references.

  • FunC is a smart-contract programming language that feels a lot like C and compiles to yet another language called Fift Assembler.
  • Fift Assembler is a little different from “traditional” programming languages in that it doesn’t have a compiler. Instead, it is an Embedded Domain-Specific Language (eDSL), in other words, it is a Forth library that allows one to write Forth programs that generate binary executable code for the TVM.

Our competition entries

Asynchronous payment channel

A “payment channel” is a smart-contract that allows two users to send payments to each other off-chain, thus saving money (transaction fees) and time (they don’t have to wait for a block to be issued). This way, the payments can be as small and frequent as needed, and the users still do not need to trust each other, as the final settlement is guaranteed by the smart-contract.

  1. this tutorial that presents the same idea as ours, but does not go into a lot of detail regarding correctness and dispute resolution.

TVM Haskell eDSL

We also decided to implement a multisignature wallet, but we thought that writing another FunC contract would be not that interesting, so we added a twist: our own assembler language for TVM. Just as Fift Assembler, our new language was embedded into another language but we chose Haskell as the host. This gave us access to all the power of Haskell’s static types, and we are firm believers of static typing, especially when working with smart-contracts — an area where the cost of a small mistake can be very high.

  • Just like in Haskell, functions have their types specified and these are checked during compilation. In our eDSL, the input type of a function is the type of the stack that it expects, and the output type is the type of the stack that its invocation will result in.
  • There are stacktype annotations here and there in the code. 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. These can serve as documentation and as assertions that can help the developer find an issue in case they make a change in the code and something doesn't compile. Of course, they do not have any effect on performance at run time as they do not result in any TVM code being generated.
  • It is still a prototype quickly put together in about two weeks. There is plenty of room for improvement, for example all class instances you will see in the code below can (and should) be auto-generated.
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 WalletErrorinstance 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 SeqNoMismatchpush @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

Our thoughts on the competition & TON

First of all, we enjoyed the competition a lot! It gave us an unexpected break from our daily responsibilities (not that we don’t enjoy doing what we do daily, but nevertheless). This spirit of a hackathon, close teamwork, the need to quickly dive into a new technology — I think all engineers know how exciting it is.

Serokell is a software development company focused on building innovative solutions for complex problems. Come visit us at serokell.io!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store