Skip to content

Cells, Builders, and Slices

Cells, Builders, and Slices are low-level primitives of TON Blockchain. The virtual machine of TON Blockchain, TVM, uses cells to represent all data structures in persistent storage and most in memory.

Cells

Cell is a primitive and a data structure, which ordinarily consists of up to 1023 continuously laid out bits and up to 4 references (refs) to other cells. Circular references are forbidden and cannot be created by means of TVM, which means cells can be viewed as quadtrees or directed acyclic graphs (DAGs) of themselves. Contract code itself is represented by a tree of cells.

Cells and cell primitives are bit-oriented, not byte-oriented: TVM regards data kept in cells as sequences (strings or streams) of up to 1023 bits, not bytes. If necessary, contracts are free to use, for example, 21-bit integer fields serialized into TVM cells, thus using fewer persistent storage bytes to represent the same data.

Kinds

While the TVM type Cell refers to all cells, there are different kinds of cells with various memory layouts. The one described earlier is commonly referred to as an ordinary (or simple) cell—that is the simplest and most commonly used flavor of cells, which can only contain data. The vast majority of descriptions, guides, and references to cells and their usage assume ordinary ones.

Other kinds of cells are collectively called exotic (or special) cells. They sometimes appear in actual representations of blocks and other data structures on TON Blockchain. Their memory layouts and purposes differ significantly from ordinary cells.

Kinds (or subtypes) of all cells are encoded by an integer between 1-1 and 255255. Ordinary cells are encoded by 1-1, while exotic ones can be encoded by any other integer in that range. The subtype of an exotic cell is stored in the first 88 bits of its data, which means valid exotic cells always have at least 88 data bits.

TVM currently supports the following exotic cell subtypes:

  • Pruned branch cell, with subtype encoded as 11 — they represent deleted subtrees of cells.
  • Library reference cell, with subtype encoded as 22 — they are used for storing libraries, usually in masterchain contexts.
  • Merkle proof cell, with subtype encoded as 33 — they are used for verifying that certain portions of another cell’s tree data belong to the full tree.
  • Merkle update cell, with subtype encoded as 44 — they always have two references and behave like a Merkle proof for both of them.

Levels

Every cell, being a quadtree, has an attribute called level, which is represented by an integer between 00 and 33. The level of an ordinary cell is always equal to the maximum of the levels of all its references. That is, the level of an ordinary cell without references is equal to 00.

Exotic cells have different rules for determining their level, which are described on this page in TON Docs.

Serialization

Before a cell can be transferred over the network or stored on disk, it must be serialized. There are several common formats, such as the standard Cell representation and BoC.

Standard representation

The standard Cell representation is a common serialization format for cells, first described in tvm.pdf. Its algorithm serializing cells into octet (byte) sequences begins by serializing the first 2 bytes, called descriptors:

  • The Refs descriptor is calculated according to the formula: r+8×k+32×lr + 8 \times k + 32 \times l, where rr is the number of references contained in the cell (between 0 and 4), kk is a flag indicating the cell kind (0 for ordinary and 1 for exotic), and ll is the level of the cell (between 0 and 3).
  • The Bits descriptor is calculated according to the formula: b8+b8\lfloor\frac{b}{8}\rfloor + \lceil\frac{b}{8}\rceil, where bb is the number of bits in the cell (between 0 and 1023).

Then, the data bits of the cell themselves are serialized as b8\lceil\frac{b}{8}\rceil 8-bit octets (bytes). If bb is not a multiple of eight, a binary 1 followed by up to six binary 0s is appended to the data bits.

Next, 2 bytes store the depth of the refs, i.e., the number of cells between the root of the cell tree (the current cell) and the deepest reference, including it. For example, a cell containing only one reference and no further references would have a depth of 1, while the referenced cell would have a depth of 0.

Finally, for every referenced cell, the SHA-256 hash of its standard representation is stored, occupying 32 bytes per referenced cell, recursively repeating the said algorithm. Note that cyclic cell references are not allowed, so this recursion always terminates in a well-defined manner.

If we were to compute the hash of the standard representation of this cell, all bytes from the above steps would be concatenated together and then hashed using the SHA-256 hash function. This is the algorithm behind the HASHCU and HASHSU instructions of TVM and the respective Cell.hash() and Slice.hash() functions of Tact.

Bag of Cells

Bag of Cells, or BoC for short, is a format for serializing and deserializing cells into byte arrays as described in boc.tlb TL-B schema.

Read more about BoC in TON Docs: Bag of Cells.

Immutability

Cells are read-only and immutable, but there are two major sets of ordinary cell manipulation instructions in TVM:

  • Cell creation (or serialization) instructions, which are used to construct new cells from previously stored values and cells.
  • Cell parsing (or deserialization) instructions, which are used to extract or load data previously stored into cells via serialization instructions.

In addition, there are instructions specific to exotic cells to create them and inspect their values. However, ordinary cell parsing instructions can still be used on exotic cells, in which case they are automatically replaced by ordinary cells during such deserialization attempts.

All cell manipulation instructions require transforming values of Cell type into either Builder or Slice types before such cells can be modified or inspected.

Builders

Builder is a cell manipulation primitive used for cell creation instructions. They are immutable just like cells and allow constructing new cells from previously stored values and cells. Unlike cells, values of type Builder appear only on the TVM stack and cannot be stored in persistent storage. This means, for example, that persistent storage fields with type Builder are actually stored as cells under the hood.

The Builder type represents partially composed cells, for which fast operations to append integers, other cells, references to other cells, and many other operations are defined:

While you may use them for manual construction of cells, it’s strongly recommended to use Structs instead: Construction of cells with Structs.

Slices

Slice is a cell manipulation primitive used for cell parsing instructions. Unlike cells, slices are mutable and allow extraction or loading of data previously stored in cells via serialization instructions. Also unlike cells, values of type Slice appear only on the TVM stack and cannot be stored in persistent storage. This means, for example, that persistent storage fields with type Slice would actually be stored as cells under the hood.

The Slice type represents either the remainder of a partially parsed cell or a value (subcell) residing inside such a cell, extracted from it by a parsing instruction:

While you may use slices for manual parsing of cells, it is strongly recommended to use Structs instead: Parsing cells with Structs.

Serialization types

Similar to serialization options of the Int type, Cell, Builder, and Slice also have various representations for encoding their values in the following cases:

contract SerializationExample {
someCell: Cell as remaining;
someSlice: Slice as bytes32;
// Constructor function,
// necessary for this example contract to compile
init() {
self.someCell = emptyCell();
self.someSlice = beginCell().storeUint(42, 256).asSlice();
}
}

remaining

The remaining serialization option can be applied to values of Cell, Builder, and Slice types.

It affects the process of constructing and parsing cell values by causing them to be stored and loaded directly rather than as a reference. To draw parallels with cell manipulation instructions, specifying remaining is like using Builder.storeSlice() and Slice.loadBits() instead of the default Builder.storeRef() and Slice.loadRef().

In addition, the TL-B representation produced by Tact changes too:

struct TwoCell {
cRef: Cell; // ^cell in TL-B
cRem: Cell as remaining; // cell in TL-B
}
struct TwoBuilder {
bRef: Builder; // ^builder in TL-B
bRem: Builder as remaining; // builder in TL-B
}
struct TwoSlice {
sRef: Slice; // ^slice in TL-B
sRem: Slice as remaining; // slice in TL-B
}
contract SerializationExample {
cell2: TwoCell;
builder2: TwoBuilder;
slice2: TwoSlice;
// Constructor function,
// necessary for this example contract to compile
init() {
self.cell2 = TwoCell{
cRef: emptyCell(),
cRem: emptyCell(),
};
self.builder2 = TwoBuilder{
bRef: beginCell(),
bRem: beginCell(),
};
self.slice2 = TwoSlice{
sRef: emptySlice(),
sRem: emptySlice(),
};
}
}

Here, ^cell, ^builder, and ^slice in TL-B syntax mean a reference to Cell, Builder, and Slice values respectively, whereas cell, builder, or slice indicate that the given value will be stored directly as a Cell, Builder, or Slice rather than as a reference.

To give a real-world example, imagine that you need to notice and react to inbound Jetton transfers in your smart contract. The appropriate Message structure for doing so would look like this:

message(0x7362d09c) JettonTransferNotification {
// Unique identifier used to trace transactions across multiple contracts
// Defaults to 0, which means we don't mark messages to trace their chains
queryId: Int as uint64 = 0;
// Amount of Jettons transferred
amount: Int as coins;
// Address of the sender of the Jettons
sender: Address;
// Optional custom payload
forwardPayload: Slice as remaining;
}

And the receiver in the contract would look like this:

receive(msg: JettonTransferNotification) {
// ... you do you ...
}

Upon receiving a Jetton transfer notification message, its cell body is converted into a Slice and then parsed as a JettonTransferNotification Message. At the end of this process, the forwardPayload will contain all the remaining data of the original message cell.

In this context, it’s not possible to violate the Jetton standard by placing the forwardPayload: Slice as remaining field in any other position within the JettonTransferNotification Message. That’s because Tact prohibits the usage of as remaining for any field other than the last one in Structs and Messages, preventing misuse of contract storage and reducing gas consumption.

bytes32

bytes64

Operations

Construct and parse

In Tact, there are at least two ways to construct and parse cells:

Manually

Construction via BuilderParsing via Slice
beginCell()Cell.beginParse()
.storeUint(42, 7)Slice.loadUint(7)
.storeInt(42, 7)Slice.loadInt(7)
.storeBool(true)Slice.loadBool(true)
.storeSlice(slice)Slice.loadBits(slice)
.storeCoins(42)Slice.loadCoins(42)
.storeAddress(address)Slice.loadAddress()
.storeRef(cell)Slice.loadRef()
.endCell()Slice.endParse()

Using Structs (recommended)

Structs and Messages are almost like living TL-B schemas, meaning they are essentially TL-B schemas expressed in maintainable, verifiable, and user-friendly Tact code.

It is strongly recommended to use them and their methods, such as Struct.toCell() and Struct.fromCell(), instead of manually constructing and parsing cells, as this allows for much more declarative and self-explanatory contracts.

The examples of manual parsing above can be rewritten using Structs, with descriptive names for fields if desired:

// First Struct
struct Showcase {
id: Int as uint8;
someImportantNumber: Int as int8;
isThatCool: Bool;
payload: Slice;
nanoToncoins: Int as coins;
wackyTacky: Address;
jojoRef: Adventure; // another Struct
}
// Here it is
struct Adventure {
bizarre: Bool = true;
time: Bool = false;
}
fun example() {
// Basics
let s = Showcase.fromCell(
Showcase{
id: 7,
someImportantNumber: 42,
isThatCool: true,
payload: emptySlice(),
nanoToncoins: 1330 + 7,
wackyTacky: myAddress(),
jojoRef: Adventure{ bizarre: true, time: false },
}.toCell());
s.isThatCool; // true
}

Note that Tact’s auto-layout algorithm is greedy. For example, struct Adventure occupies very little space and will not be stored as a reference Cell. Instead, it will be provided directly as a Slice.

By using Structs and Messages instead of manual Cell composition and parsing, those details will be simplified away and will not cause any hassle when the optimized layout changes.

Check if empty

Neither Cell nor Builder can be checked for emptiness directly — one needs to convert them to Slice first.

To check if there are any bits, use Slice.dataEmpty(). To check if there are any references, use Slice.refsEmpty(). To check both at the same time, use Slice.empty().

To also throw an exit code 9 whenever the Slice isn’t completely empty, use Slice.endParse().

// Preparations
let someCell = beginCell().storeUint(42, 7).endCell();
let someBuilder = beginCell().storeRef(someCell);
// Obtaining our Slices
let slice1 = someCell.asSlice();
let slice2 = someBuilder.asSlice();
// .dataEmpty()
slice1.dataEmpty(); // false
slice2.dataEmpty(); // true
// .refsEmpty()
slice1.refsEmpty(); // true
slice2.refsEmpty(); // false
// .empty()
slice1.empty(); // false
slice2.empty(); // false
// .endParse()
try {
slice1.endParse();
slice2.endParse();
} catch (e) {
e; // 9
}

Check if equal

Values of type Builder cannot be compared directly using the binary equality == or inequality != operators. However, values of type Cell and Slice can.

Direct comparisons:

let a = beginCell().storeUint(123, 8).endCell();
let aSlice = a.asSlice();
let b = beginCell().storeUint(123, 8).endCell();
let bSlice = b.asSlice();
let areCellsEqual = a == b; // true
let areCellsNotEqual = a != b; // false
let areSlicesEqual = aSlice == bSlice; // true
let areSlicesNotEqual = aSlice != bSlice; // false

Note that direct comparison via the == or != operators implicitly uses SHA-256 hashes of the standard Cell representation under the hood.

Explicit comparisons using .hash() are also available:

let a = beginCell().storeUint(123, 8).endCell();
let aSlice = a.asSlice();
let b = beginCell().storeUint(123, 8).endCell();
let bSlice = b.asSlice();
let areCellsEqual = a.hash() == b.hash(); // true
let areCellsNotEqual = a.hash() != b.hash(); // false
let areSlicesEqual = aSlice.hash() == bSlice.hash(); // true
let areSlicesNotEqual = aSlice.hash() != bSlice.hash(); // false