Skip to main content

TL-B Types

advanced level

This information is very low-level and could be hard to understand for newcomers. So feel free to read about it later.

In this section, complex and unconventional typed language binary (TL-B) structures are analyzed. To get started, we recommend reading this documentation first to become more familiar with the topic.

tlb structure

Either

left$0 {X:Type} {Y:Type} value:X = Either X Y;
right$1 {X:Type} {Y:Type} value:Y = Either X Y;

The Either type is used when one of two resulting types are possible. In this case, the type choice depends on the prefix bit shown. If the prefix bit is 0, the left type is serialized, while if the 1 prefix bit is used, the right one is serialized.

It is used, for example, when serializing messages, when the body is either part of the main cell or linked to another cell.

Maybe

nothing$0 {X:Type} = Maybe X;
just$1 {X:Type} value:X = Maybe X;

The Maybe type is used in conjunction with optional values. In these instances, if the first bit is 0, the value itself is not serialized (and is actually skipped), while if the value is 1, it is serialized.

Both

pair$_ {X:Type} {Y:Type} first:X second:Y = Both X Y;

The Both type variation is used only in conjunction with normal pairs, whereby both types are serialized, one after the other, without conditions.

Unary

The Unary functional type is commonly used for dynamic sizing in structures such as hml_short.

Unary presents two main options:

unary_zero$0 = Unary ~0;
unary_succ$1 {n:#} x:(Unary ~n) = Unary ~(n + 1);

Unary serialization

Generally, using the unary_zero variation is quite simple: if the first bit is 0, then the result of whole Unary deserialization is 0.

That said, the unary_succ variation is more complex because it is loaded recursively and possesses a value of ~(n + 1). This means it sequentially calls itself until it reaches unary_zero. In other words, the desired value will be equal to the number of units in a row.

For instance, let's analyze serialization of the bitstring 110.

The call chain will be as follows:

unary_succ$1 -> unary_succ$1 -> unary_zero$0

Once we reach unary_zero, the value is returned to the end of a serialized bitstring similarly to a recursive function call.

Now, to understand the result more clearly, let's retrieve the return value path, which is displayed as follows:

0 -> ~(0 + 1) -> ~(1 + 1) -> 2, that means we serialized 110 in Unary 2.

Unary deserialization

Suppose we have Foo type:

foo$_  u:(Unary 2) = Foo;

According said above, Foo will be deserialized into:



foo u:(unary_succ x:(unary_succ x:(unnary_zero)))

Hashmap

The Hashmap complex type is used for storing dict from the FunC smart contract code(dict).

The following TL-B structures are used to serialize a Hashmap with a fixed key length:

hm_edge#_ {n:#} {X:Type} {l:#} {m:#} label:(HmLabel ~l n) 
{n = (~m) + l} node:(HashmapNode m X) = Hashmap n X;

hmn_leaf#_ {X:Type} value:X = HashmapNode 0 X;
hmn_fork#_ {n:#} {X:Type} left:^(Hashmap n X)
right:^(Hashmap n X) = HashmapNode (n + 1) X;

hml_short$0 {m:#} {n:#} len:(Unary ~n) {n <= m} s:(n * Bit) = HmLabel ~n m;
hml_long$10 {m:#} n:(#<= m) s:(n * Bit) = HmLabel ~n m;
hml_same$11 {m:#} v:Bit n:(#<= m) = HmLabel ~n m;

unary_zero$0 = Unary ~0;
unary_succ$1 {n:#} x:(Unary ~n) = Unary ~(n + 1);

hme_empty$0 {n:#} {X:Type} = HashmapE n X;
hme_root$1 {n:#} {X:Type} root:^(Hashmap n X) = HashmapE n X;

This means the root structure uses HashmapE and either of its two states: including hme_empty or hme_root.

Hashmap parsing example

As an example, consider the following Cell, given in binary form.

1[1] -> {
2[00] -> {
7[1001000] -> {
25[1010000010000001100001001],
25[1010000010000000001101111]
},
28[1011100000000000001100001001]
}
}

This Cell makes use of the HashmapE structure type and possesses an 8-bit key size and its values make use of the uint16 number framework (HashmapE 8 uint16). HashmapE makes use of distinct 3 key types:

1 = 777
17 = 111
128 = 777

To parse this Hashmap, we need to know in advance which structure type to use, either hme_empty or hme_root. This is determined by identifying the correct prefix. The hme empty variation uses one bit 0 (hme_empty$0), while hme root uses one bit 1 (hme_root$1). After reading the first bit, it is determined that it is equal to one (1[1]), meaning it is the hme_root variation.

Now, let's fill out the structure variables with known values, with the initial result being: hme_root$1 {n:#} {X:Type} root:^(Hashmap 8 uint16) = HashmapE 8 uint16;

Here the one bit prefix is already read, yet within the {} denotes conditions that don’t need to be read. The condition {n:#} means that n is any uint32 number, while {X:Type} means that X can employ any type.

The next portion that needs to be read is the root:^(Hashmap 8 uint16), while the ^ symbol denotes a link that must be loaded.

2[00] -> {
7[1001000] -> {
25[1010000010000001100001001],
25[1010000010000000001101111]
},
28[1011100000000000001100001001]
}

Initiating Branch Parsing

According to our schema, this is the correct Hashmap 8 uint16 structure. Next, we fill it with known values and obtain a result of:

hm_edge#_ {n:#} {X:Type} {l:#} {m:#} label:(HmLabel ~l 8) 
{8 = (~m) + l} node:(HashmapNode m uint16) = Hashmap 8 uint16;

As is shown above, conditional variables {l:#} and {m:#} have now appeared, yet the values of both variables are unknown to us. Also, after reading the corresponding label, it's clear that n is involved in the equation {n = (~m) + l}, in this case we calculate l and m, the sign tells us the resulting value of ~.

To determine the value of l, we must load the label:(HmLabel ~l uint16) sequence. As is shown below, the HmLabel has 3 basic structural options:

hml_short$0 {m:#} {n:#} len:(Unary ~n) {n <= m} s:(n * Bit) = HmLabel ~n m;
hml_long$10 {m:#} n:(#<= m) s:(n * Bit) = HmLabel ~n m;
hml_same$11 {m:#} v:Bit n:(#<= m) = HmLabel ~n m;

Each option is determined by the corresponding prefix. Currently, our root cell is made up of 2 zero bits, which is displayed as: (2[00]). Therefore, the only logical option is hml_short$0, which makes use of a prefix beginning with 0.

Fill hml_short with known values:

hml_short$0 {m:#} {n:#} len:(Unary ~n) {n <= 8} s:(n * Bit) = HmLabel ~n 8

In this case, we don't know the value of n, but since it has a ~ character, it’s possible to calculate it. To accomplish this, we load len:(Unary ~n), more about Unary here.

In this case, we started with 2[00], yet after defining the type HmLabel, only one of the two bits still exists.

Therefore, we load it and see that its value is 0, which means it clearly uses the unary_zero$0 variation. That means that the n value using the HmLabel variation is zero.

Next, it is necessary to complete the hml_short variation sequence using the calculated n value:

hml_short$0 {m:#} {n:#} len:0 {n <= 8} s:(0 * Bit) = HmLabel 0 8

It turns out we have an empty HmLabel denoted as, s = 0, therefore there is nothing to download.

Next, we supplement our structure with the calculated value of l as follows:

hm_edge#_ {n:#} {X:Type} {l:0} {m:#} label:(HmLabel 0 8) 
{8 = (~m) + 0} node:(HashmapNode m uint16) = Hashmap 8 uint16;

Now that we have calculated the value of l, we can also calculate m using the equation n = (~m) + 0, i.e. m = n - 0, m = n = 8.

After determining all unknown values it is now possible to load the node:(HashmapNode 8 uint16).

As far as the HashmapNode goes, we have options:

hmn_leaf#_ {X:Type} value:X = HashmapNode 0 X;
hmn_fork#_ {n:#} {X:Type} left:^(Hashmap n X)
right:^(Hashmap n X) = HashmapNode (n + 1) X;

In this case, we determine the option not by using the prefix, but by using the parameter. This means that if n = 0, then the correct end result will be either hmn_leaf or hmn_fork. In this example, the result is n = 8 (the hmn_fork variation). We make use of the hmn_fork variation and fill in the known values:

hmn_fork#_ {n:#} {X:uint16} left:^(Hashmap n uint16) 
right:^(Hashmap n uint16) = HashmapNode (n + 1) uint16;

After entering the known values, we must calculate the HashmapNode (n + 1) uint16. This means that the resulting value of n must be equal to our parameter, i.e. 8. To calculate the local value of n, we need to calculate it using the following formula: n = (n_local + 1) -> n_local = (n - 1) -> n_local = (8 - 1) -> n_local = 7.

hmn_fork#_ {n:#} {X:uint16} left:^(Hashmap 7 uint16) 
right:^(Hashmap 7 uint16) = HashmapNode (7 + 1) uint16;

Now that we know the above formula is required, obtaining the end result is simple. Next we load the left and right branches and for each subsequent branch the process is repeated.

Analyzing Loaded Hashmap Values

Continuing with the previous example, let’s examine how the process of loading branches works (for dict values)., i.e. 28[1011100000000000001100001001]

The end result becomes hm_edge yet again and the next step is to fill the sequence with the correct known values as follows:

hm_edge#_ {n:#} {X:Type} {l:#} {m:#} label:(HmLabel ~l 7) 
{7 = (~m) + l} node:(HashmapNode m uint16) = Hashmap 7 uint16;

Next the HmLabel response is loaded using the HmLabel variation because the prefix is 10.

hml_long$10 {m:#} n:(#<= m) s:(n * Bit) = HmLabel ~n m;

Now, let's fill in the sequence:

hml_long$10 {m:#} n:(#<= 7) s:(n * Bit) = HmLabel ~n 7;

The new construction - n:(#<= 7), clearly denotes a sizing value that corresponds to the number 7, which is in fact a log2 from the number + 1. But for simplicity, we could count the number of bits needed to write the number 7. Relatedly, the number 7 in binary form is 111; therefore 3 bits are required, meaning the value for n = 3.

hml_long$10 {m:#} n:(## 3) s:(n * Bit) = HmLabel ~n 7;

Next we load n into the sequence, with an end result of 111, which as we noted above = 7 coincidentally. Next, we load s into the sequence, 7 bits - 0000000. Remember, s is part of the key.

Next we return to the top of the sequence and fill in the resulting l:

hm_edge#_ {n:#} {X:Type} {l:#} {m:#} label:(HmLabel 7 7) 
{7 = (~m) + 7} node:(HashmapNode m uint16) = Hashmap 7 uint16;

Then we calculate the value of m, m = 7 - 7, therefore the value of m = 0. Since the value of m = 0, the structure is perfect for use with a HashmapNode:

hmn_leaf#_ {X:Type} value:X = HashmapNode 0 X;

Next we substitute our uint16 type and load the value. The remaining 16 bits of 0000001100001001 in decimal form is 777, therefore our value.

Now let's restore the key, we must combine the ordered list of all parts of the key that were computed previously. Each of the two related key parts unite with one bit based on which type branches are used. For the right branch, ‘1’ bit is added, and for the left branch ‘0’ bit is added. If a full HmLabel exists above, then its bits are added to the key.

In this case specifically, 7 bits are taken from the HmLabel 0000000 and ‘1’ bit is added before the sequence of zeros because the value was obtained from the right branch. The end result is 8 bits in total or 10000000, meaning the key value equals 128.

Other Hashmap Types

Now that we have discussed Hashmaps and how to load the standardized Hashmap type, let’s explain how the additional Hashmap types work.

HashmapAugE

ahm_edge#_ {n:#} {X:Type} {Y:Type} {l:#} {m:#} 
label:(HmLabel ~l n) {n = (~m) + l}
node:(HashmapAugNode m X Y) = HashmapAug n X Y;

ahmn_leaf#_ {X:Type} {Y:Type} extra:Y value:X = HashmapAugNode 0 X Y;

ahmn_fork#_ {n:#} {X:Type} {Y:Type} left:^(HashmapAug n X Y)
right:^(HashmapAug n X Y) extra:Y = HashmapAugNode (n + 1) X Y;

ahme_empty$0 {n:#} {X:Type} {Y:Type} extra:Y
= HashmapAugE n X Y;

ahme_root$1 {n:#} {X:Type} {Y:Type} root:^(HashmapAug n X Y)
extra:Y = HashmapAugE n X Y;

The main difference between the HashmapAugE and the regular Hashmap is the presence of an extra:Y field in each node (not just in leafs with values).

PfxHashmap

phm_edge#_ {n:#} {X:Type} {l:#} {m:#} label:(HmLabel ~l n) 
{n = (~m) + l} node:(PfxHashmapNode m X)
= PfxHashmap n X;

phmn_leaf$0 {n:#} {X:Type} value:X = PfxHashmapNode n X;
phmn_fork$1 {n:#} {X:Type} left:^(PfxHashmap n X)
right:^(PfxHashmap n X) = PfxHashmapNode (n + 1) X;

phme_empty$0 {n:#} {X:Type} = PfxHashmapE n X;
phme_root$1 {n:#} {X:Type} root:^(PfxHashmap n X)
= PfxHashmapE n X;

The main difference between the PfxHashmap and the regular Hashmap is its ability to store different key lengths due to the presence of the phmn_leaf$0 and phmn_fork$1 nodes.

VarHashmap

vhm_edge#_ {n:#} {X:Type} {l:#} {m:#} label:(HmLabel ~l n) 
{n = (~m) + l} node:(VarHashmapNode m X)
= VarHashmap n X;
vhmn_leaf$00 {n:#} {X:Type} value:X = VarHashmapNode n X;
vhmn_fork$01 {n:#} {X:Type} left:^(VarHashmap n X)
right:^(VarHashmap n X) value:(Maybe X)
= VarHashmapNode (n + 1) X;
vhmn_cont$1 {n:#} {X:Type} branch:Bit child:^(VarHashmap n X)
value:X = VarHashmapNode (n + 1) X;

// nothing$0 {X:Type} = Maybe X;
// just$1 {X:Type} value:X = Maybe X;

vhme_empty$0 {n:#} {X:Type} = VarHashmapE n X;
vhme_root$1 {n:#} {X:Type} root:^(VarHashmap n X)
= VarHashmapE n X;

The main difference between the VarHashmap and the regular Hashmap is its ability to store different key lengths due to the presence of the vhmn_leaf$00 and vhmn_fork$01 nodes. Additionally, the VarHashmap is able to form a common value prefix (child map) at the expense of the vhmn_cont$1.

BinTree

bta_leaf$0 {X:Type} {Y:Type} extra:Y leaf:X = BinTreeAug X Y;
bta_fork$1 {X:Type} {Y:Type} left:^(BinTreeAug X Y)
right:^(BinTreeAug X Y) extra:Y = BinTreeAug X Y;

The binary tree key generation mechanism works in a similar manner to the standardized Hashmap framework but doesn't use labels and only includes branch prefixes.

Addresses

TON addresses are formed with the sha256 hashing mechanism using the TL-B StateInit structure. This means that the address can be calculated prior to network contract deployment.

Serialization

Standard addresses such as EQBL2_3lMiyywU17g-or8N7v9hDmPCpttzBPE2isF2GTzpK4 use the base64 uri for byte encoding. Typically, they have a length of 36 bytes, the last 2 of which are the crc16 checksum calculated with XMODEM table, while the first byte represents the flag, with the second representing the workchain. The 32 bytes in the middle are the data of the address itself (also calls AccountID), often represented in schemas such as int256.

Decoding example

References

Here a link to the original article by Oleg Baranov.