How to develop and publish a smart-contract in the Telegram Open Network (TON) (Ⅰ)

What is this article about?

In this article, I will tell about my participation in the first (out of two so far) Telegram blockchain contest. I didn’t win any prize. However, decided to combine and share the unique experience I have had from the start to finish line, so my observations could help anyone who is interested.

Since I didn’t want to write some abstract code, instead make something useful. I created instant lottery smart-contract and website which shows smart-contract data directly from Telegram Open Network (TON) avoiding any middle storage layers.

The article will be particularly useful for those, who want to write their first smart-contract for TON but don’t know where to start.

Using the lottery as an example, I will go from setting up the environment to publishing a smart contract, interacting with it. Moreover, I will create a website that will show smart-contract data.

About participation in the contest

In October 2019 Telegram organized a blockchain contest with new programming languages Fift and FunC. The developers were asked to write any smart-contract out of five suggested. I thought that it would be interesting to do something out of the ordinary: learn a new language and create smart-contract even if it can be the last time using this it. Besides this topic is in trend nowadays.

It has to be mentioned that I have never had any sort of experience in writing the smart-contracts for blockchain networks.

My plan was to participate until the very end. Then I was going to write a summary article, but I was not selected further than the first stage of the competition, where I have submitted workable version of multi-signature wallet written in FunC. Smart-contract was based on Solidity example for Ethereum Network.

I imagined that should make it, at this level of competition my work should be enough to gain a prize-winning spot. However, about 40 out of 60 participants happened to win and that excluded me. In general that is okay and could happen, but one thing bothered me: the review along with the test for my contract was not done, upon the announcement of the results. Thus, I asked other participants in the chat, if anyone else faced the same situation, there were none.

I believe, my messages draw some attention and after two days judges published several comments. I still did not get it: did they skipped my smart-contract by accident during the evaluation period? Or was it such a bad work that they decided it does not worth any comment at all? I asked these questions on the contest page, unfortunately questions were ignored as well. Although, it is not a secret who the judge was, nevertheless writing a private messages would have been too much to ask.

That being said it was decided to write this article that explains the subject in detail, since I have already spent plenty of time to understand how things work. In general, there is lack of information provided, so this article will save time for everyone who is interested.

The concept of smart-contract in TON

Before writing the first smart-contract we need to understand how to approach this thing in general. Therefore, now I will describe the complete set of the system, or more precisely which parts we should know in order to write at least some kind of functioning smart-contract.

We will focus on writing smart-contract using FunC and Fift, which will be compiled into Fift-assembler and then will be executed in TON Virtual Machine (TVM). Therefore, the article is more like a description of the development of a regular program. We will not dwell on how the blockchain platform works.

How Fift and TVM work is well described in the official documentation. During the contest and now while writing current smart-contract I made a use of these docs.

The main language for writing smart-contract is FunC. Currently there is no documentation available, therefore in order to develop something, we need to study the existing examples in the official repository, you may also find language implementation itself there and other participants’ submissions from previous two contests. References are at the end of the article.

Let’s say we have written the smart-contract using FunC after that we compile FunC code into Fift-assembler.

The compiled code should be published in TON. In order to be able to do that we should write Fift code, that takes as arguments smart-contract code and several other parameters and generates .boc file (which stands for “bag of cells”) and depending on the way this code is written private key and address can be generated as well, based on the code of smart-contract.

We can send grams to the generated address even if the smart-contract is not published yet.

Since TON charges a fee for storage and transactions, before publishing a smart contract, you need to transfer grams to the generated address, otherwise, it will not be published.

As means to publish smart-contract, we should send generated .boc file in TON using lite-client (details are below). After that, we can interact with the smart-contract by sending external messages (e.g.: using lite-client) or internal messages (e.g.: when one smart-contract sends a message to another).

Now, when we understand the process of publishing smart-contract code, it becomes easier. We already have an idea of what exactly we want to create and how it should work. While we code we can use existing smart-contracts as a reference or check the implementation of Fift and FunC in the official repository or in the documentation.

Frequently, I used Telegram-chat as a sourse searching by keywords, where during the contest all participants gathered along with Telegram employees and chatted about Fift and FunC. Link to chat in the end.

Let’s move to implementation.

Preparing the environment to work with TON

Everything described below I have done in MacOS and re-tested in Ubuntu 18.04 LTS with Docker.

The first thing we need to do is to install lite-client that will allow us to send requests to TON.

Instruction on the website explains the process of the installation pretty clear. Here we just follow the instruction and install missing dependencies. I didn’t compile each library separately instead installed them from the official Ubuntu repository (used brew on MacOS).

apt -y install git 
apt -y install wget 
apt -y install cmake 
apt -y install g++ 
apt -y install zlib1g-dev 
apt -y install libssl-dev 

Once all dependencies installed we install lite-client, Fift and FunC.

Let’s clone the TON repository together with its dependencies. For convenience, we will run everything in ~/TON folder.

cd ~/TON
git clone https://github.com/newton-blockchain/ton.git
cd ./ton
git submodule update --init --recursive

The repository also contains Fift and FunC implementations.

Now we are ready to build the project. We cloned the repository into ~/TON/ton folder. In ~/TON we should create build folder and execute the following.

mkdir ~/TON/build
cd ~/TON/build
cmake ../ton

Since we are going to write smart-contract we need more than just lite-client meaning: Fift and FunC too, therefore, we compile them as well.

cmake --build . --target lite-client
cmake --build . --target fift
cmake --build . --target func

Next, we should download a configuration file that contains information about TON node to which lite-client will connect.

wget https://newton-blockchain.github.io/global.config.json

First requests in TON

Now let’s run installed lite-client.

cd ~/TON/build
./lite-client/lite-client -C global.config.json

If the installation was successful we will see lite-client connection logs.

[ 1][t 2][1582054822.963129282][lite-client.h:201][!testnode] conn ready
[ 2][t 2][1582054823.085654020][lite-client.cpp:277][!testnode] server version is 1.1, capabilities 7
[ 3][t 2][1582054823.085725069][lite-client.cpp:286][!testnode] server time is 1582054823 (delta 0)
...

We can execute help and see available commands.

help

Let’s list the commands that will be used in this article.

list of available commands:
`last` Get last block and state info from server
`sendfile` <filename> Load a serialized message from <filename> and send it to server
`getaccount` <addr> [<block-id-ext>] Loads the most recent state of specified account; <addr> is in [<workchain>:]<hex-or-base64-addr> format
`runmethod` <addr> [<block-id-ext>] <method-id> <params>... Runs GET method <method-id> of account <addr> with specified parameters

Now we are ready to start writing the smart-contract.

Implementation

Idea

As I described above the smart-contract that we are going to write is a lottery.

Moreover, it is not just an ordinary lottery, where there is a necessity to wait an hour, a day or even more, it is an instant lottery, in which the player transfers N Grams to the given address and immediately receives back 2 * N Grams or loses. Probability of winning we will set around 40%. If smart-contract do not have enough Grams to pay, we consider it as a top-up transaction.

It is extremely important to see the bets in a real-time and in a convenient way so that the players could easily understand if they won or lost. To solve this we will create a website that will show bets’ history directly from TON.

Writing the smart-contract

For convenience, I created a syntax highlighter for FunC language, the plugin could be found in the Visual Studio Code plugin search and code available on GitHub. There is also a plugin for Fift language available and can be installed in the VSC.

Right away we can create a git repository and commit interim results.

To simplify our lives, we will write and test locally until the smart-contract is ready. Then once it is all set, we will publish it in TON.

Smart-contract has two external methods that can be triggered. First is recv_external() this method is used when someone sends a request from external network (not within TON), for example when we send a message with lite-client. Second is recv_internal() this method is invoked when our contract receives message from some other contract within TON. In both cases, we can provide arbitrary parameters for these methods.

Let’s start with a simple example that will still work upon publishing, however will not carry actual payload.

() recv_internal(slice in_msg) impure {
    ;; TODO: implementation 
}

() recv_external(slice in_msg) impure {
    ;; TODO: implementation 
}

Here we should explain what is the slice. All data stored in TON Blockchain is the collection of TVM cells or cell for short, in such cell you can store up to 1023 bits of information and up to 4 references to other cells.

TVM cell slice or slice is part of the cell that is used for parsing the data, it will be more understandable later. Most important for us is that we can pass data in slice into smart-contract and depending on the message itself we can use recv_internal() and recv_external() methods to process it.

impure — keyword indicates that a method changes data in the smart-contract persistance storage.

Now let’s save contract code in lottery-code.fc file and compile.

~/TON/build/crypto/func -APSR -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc 

The meaning of flags can be checked with help command.

~/TON/build/crypto/func -help

Now we have compiled Fift-assembler code in a file named lottery-compiled.fif.

// lottery-compiled.fif

"Asm.fif" include
// automatically generated from `/Users/rajymbekkapisev/TON/ton/crypto/smartcont/stdlib.fc` `./lottery-code.fc` 
PROGRAM{
  DECLPROC recv_internal
  DECLPROC recv_external
  recv_internal PROC:<{
    //  in_msg
    DROP    // 
  }>
  recv_external PROC:<{
    //  in_msg
    DROP    // 
  }>
}END>c

We can run it locally. Let’s configure the environment first.

Note, that first line includes Asm.fif file, which contains an implementation of Fift-assembler using Fift language.

Since we want to execute and test smart-contract locally we will create lottery-test-suite.fif, copy compiled code and change the last line, write smart-contract code into constant code than only we will be able to pass it to TON Virtual Machine.

"TonUtil.fif" include
"Asm.fif" include

PROGRAM{
  DECLPROC recv_internal
  DECLPROC recv_external
  recv_internal PROC:<{
    //  in_msg
    DROP    // 
  }>
  recv_external PROC:<{
    //  in_msg
    DROP    // 
  }>
}END>s constant code

So far it seems to be clear, now let’s add the code to the same file that we will use to start TVM.

0 tuple 0x076ef1ea , // magic
0 , 0 , // actions msg_sents
1570998536 , // unix_time
1 , 1 , 3 , // block_lt, trans_lt, rand_seed
0 tuple 100000000000000 , dictnew , , // remaining balance
0 , dictnew , // contract_address, global_config
1 tuple // wrap to another tuple
constant c7

0 constant recv_internal // to run recv_internal() 
-1 constant recv_external // to invoke recv_external()

In c7 constant we should write context with which TVM will be launched (or network status). During the contest, one developer showed how c7 being formed and I just copied. In this article, we also might need to change rand_seed because it is used to generate random number and if the number will remain same during the tests, it will always return the same number.

recv_internal and recv_external are just constants 0 and -1 that will be responsible for executing the corresponding functions of the smart-contract.

Now we are ready to write the first test for our empty smart-contract. For clarity, we will add all tests into the same file lottery-test-suite.fif.

Let’s create variable storage and write empty cell in it, storage will be the permanent storage of the smart-contract.

message is the variable that we will pass to the smart-contract from the external environment (or as per documentation from “nowhere”). Let’s make message empty for now.

variable storage 
<b b> storage ! 

variable message 
<b b> message ! 

Now, after we have prepared constants and variables we can run TVM with runvmctx and pass created parameters.

message @ 
recv_external 
code 
storage @ 
c7 
runvmctx 

As a result, we have this interim Fift code.

And now let’s run following code.

export FIFTPATH=~/TON/ton/crypto/fift/lib // export once 
~/TON/build/crypto/fift -s lottery-test-suite.fif 

The code should be executde without errors and we should be able to see the following logs.

execute SETCP 0
execute DICTPUSHCONST 19 (xC_,1)
execute DICTIGETJMPZ
execute DROP
execute implicit RET
[ 3][t 0][1582281699.325381279][vm.cpp:479] steps: 5 gas: used=304, max=9223372036854775807, limit=9223372036854775807, credit=0

Great, we have just wrote the first workable version of the smart-contract along with the test.

Processing external messages in a smart-contract

Now let’s start adding functionality. Let’s start with processing messages from “nowhere” in recv_external().

How to structure the message is up to the developer. But usually,

  • At first, we want to protect smart-contract from the outside world and process messages only sent by the owner.
  • Secondly, when we send a valid message we want the smart-contract to process it only once even if we will send the same message more than once. Also known as “replay attack”.

Therefore, almost every smart-contract solves these two problems. Since our smart-contract will receive external messages we should take care of this. We will do it in the reverse order, we will solve the second issue and then move to the first.

There are different ways to solve a replay attack problem. This is one of the options: in smart-contract, we will initialize the counter with 0 that will count the number of received messages. In each message, among other parameters we will send to smart-contract current counter value. If the counter value in the message doesn’t match the counter value in the smart-contract we will reject such message. When it will match, we will process the message and increase the smart-contract counter by one.

Let’s go back to lottery-test-suite.fif and add the second test. We should send the wrong counter number and code must throw an exception. For example, smart-contract counter will be 166 and we will send 165.

<b 166 32 u, b> storage !
<b 165 32 u, b> message !

message @ 
recv_external 
code 
storage @ 
c7 
runvmctx

drop 
exit_code ! 
."Exit code " exit_code @ . cr 
exit_code @ 33 - abort"Test #2 Not passed"

Let’s execute.

 ~/TON/build/crypto/fift -s lottery-test-suite.fif 

And observe that test fails.

[ 1][t 0][1582283084.210902214][words.cpp:3046] lottery-test-suite.fif:67: abort": Test #2 Not passed
[ 1][t 0][1582283084.210941076][fift-main.cpp:196] Error interpreting file `lottery-test-suite.fif`: error interpreting included file `lottery-test-suite.fif` : lottery-test-suite.fif:67: abort": Test #2 Not passed

At this point lottery-test-suite.fif should be like this.

Now let’s add counter into the smart-contract in lottery-code.fc.

() recv_internal(slice in_msg) impure {
    ;; TODO: implementation 
}

() recv_external(slice in_msg) impure {
    if (slice_empty?(in_msg)) {
        return (); 
    }
    int msg_seqno = in_msg~load_uint(32);
    var ds = begin_parse(get_data());
    int stored_seqno = ds~load_uint(32);
    throw_unless(33, msg_seqno == stored_seqno);
}

Message sent to the smart-contract will be stored in slice in_msg.

First, we should check if a message is empty, if so, we just exit execution.

Next, we should start parsing the message. in_msg~load_uint(32) that loads 32 bits from the message unsigned integer number 165.

Next, we should load 32 bits from the smart-contract storage. Then check if these two numbers are the same, if not exception should be thrown. In our case, since we have sent not matching counter, the exception will be thrown.

Let’s compile.

~/TON/build/crypto/func -APSR -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc 

Let’s copy outcome into lottery-test-suite.fif code, considering to change the last line. Monitor, if the test is passed successfully.

~/TON/build/crypto/fift -s lottery-test-suite.fif

Here is the commit with the current results.

Note that every time we change the smart-contract code we need to copy compiled code into lottery-test-suite.fif which is inconvenient. Thus, we will create a small script, which will write compiled code into a constant. We will just have to include this file in lottery-test-suite.fif.

In the project folder create build.sh file with the following code.

#!/bin/bash

~/TON/build/crypto/func -SPA -R -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc

Make it executable.

chmod +x ./build.sh

Article carried from:How to develop and publish a smart-contract in the Telegram Open Network (TON) / Хабр

2 Likes