跳转到内容

Cells、Builders 和 Slices

CellsBuildersSlices 是 TON 区块链的底层 primitives。 TON 区块链的虚拟机 TVM 使用cell来表示持久存储中的所有数据结构,以及内存中的大部分数据结构。

Cells

Cell是一种 primitive 和数据结构,它通常由多达 10231023 个连续排列的比特和多达 44 个指向其他 cell 的引用(refs)组成。 循环引用在 TVM 中是被禁止的,因此无法通过 TVM 的机制创建循环引用。这意味着,单元(cells)可以被视为自身的 [四叉树][quadtree] 或 有向无环图(DAG)。 智能合约代码本身由树形结构的cell表示。

单元(Cells)和单元原语是以位(bit)为导向的,而非字节(byte)为导向的:TVM 将存储在单元中的数据视为最多 10231023 位的序列(字符串或流),而不是字节。 如有必要,合约可以自由使用 2121-bit 整数字段,并将其序列化为 TVM cell,从而使用更少的持久存储字节来表示相同的数据。

种类

虽然 TVM 类型 Cell 指的是所有cell,但存在不同的cell类型,其内存布局也各不相同。 前面 描述的通常被称为 普通 (或简单) cell—这是最简单、最常用的cell,只能包含数据。 绝大多数关于cell及其用法的描述、指南和 参考文献 都假定是普通cell。

其他类型的cell统称为 exotic cell (或特殊cell)。 它们有时会出现在 TON 区块链上的区块和其他数据结构的实际表示中。 它们的内存布局和用途与普通cell大不相同。

所有cell的种类 (或子类型) 都由 1-1255255之间的整数编码。 普通cell用 1-1编码,特殊cell可用该范围内的任何其他整数编码。 奇异cell的子类型存储在其数据的前 88 位,这意味着有效的奇异cell总是至少有 88 个数据位。

TVM 目前支持以下exotic cell子类型:

Levels

作为 [四叉树][quadtree],每个单元格都有一个名为 level 的属性,它由 0033之间的整数表示。 普通 cell的级别总是等于其所有引用级别的最大值。 也就是说,没有引用的普通 cell 的层级为 00

Exotic cell有不同的规则来决定它们的层级,这些规则在TON Docs 的本页上有描述。

序列化

在通过网络传输 cell 或在磁盘上存储 cell 之前,必须对其进行序列化。 有几种常用格式,如标准 Cell 表示法BoC

标准表示法

标准 Cell 表示法是 tvm.pdf 中首次描述的 cells 通用序列化格式。 它的算法以八进制(字节)序列表示cell,首先将称为描述符的第一个 22 字节序列化:

  • 引用描述符(Refs descriptor)根据以下公式计算:r+8k+32lr + 8 _ k + 32 _ l,其中 rr 是 cell 中包含的引用数量(介于 0044 之间),kk 是 cell 类型的标志(00 表示普通11 表示特殊),ll 是 cell 的层级(介于 0033 之间)。
  • 位描述符(Bits descriptor)根据以下公式计算:b8+b8\lfloor\frac{b}{8}\rfloor + \lceil\frac{b}{8}\rceil,其中 bb 是 cell 中的位数(介于 0010231023 之间)。

然后,cell 本身的数据位被序列化为 b8\lceil\frac{b}{8}\rceil 88-bit octets(字节)。 如果 bb 不是 8 的倍数,则在数据位上附加一个二进制 11 和最多六个二进制 00s。

接下来, 22 字节存储了引用的深度,即Cell树根(当前Cell)和最深引用(包括它)之间的cells数。 例如,如果一个cell只包含一个引用而没有其他引用,则其深度为 11,而被引用cell的深度为 00

最后,为每个参考cell存储其标准表示的 SHA-256 哈希值,每个参考cell占用 3232 字节,并递归重复上述算法。 请注意,不允许循环引用cell,因此递归总是以定义明确的方式结束。 请注意,不允许循环引用cell,因此递归总是以定义明确的方式结束。

如果我们要计算这个cell的标准表示的哈希值,就需要将上述步骤中的所有字节连接在一起,然后使用 SHA-256 哈希值进行散列。 如果我们要计算这个cell的标准表示的哈希值,就需要将上述步骤中的所有字节连接在一起,然后使用 SHA-256 哈希值进行散列。 这是TVMHASHCUHASHSU指令以及 Tact 的Cell.hash()Slice.hash()函数背后的算法。

Bag of Cells

boc.tlb TL-B schema 所述,Bag of Cells(简称 BoC)是一种将cell序列化和去序列化为字节数组的格式。

在 TON Docs 中阅读有关 BoC 的更多信息:Bag of Cells

不变性 (Immutability)

cell是只读和不可变的,但 TVM 中有两组主要的 ordinary cell操作指令:

  • cell创建(或序列化)指令,用于根据先前保存的值和cell构建新cell;
  • cell解析(或反序列化)指令,用于提取或加载之前通过序列化指令存储到cell中的数据。

此外,还有专门针对 exotic cell的指令来创建这些cell并期望它们的值。 此外,exotic cell 有专门的指令来创建它们并预期它们的值。不过,普通(ordinary) cell解析指令仍可用于 exotic cell,在这种情况下,它们会在反序列化尝试中被自动替换为 普通(ordinary) cell。

所有cell操作指令都需要将 Cell 类型的值转换为 BuilderSlice类型,然后才能修改或检查这些cell。

Builders

Builder 是一种用于使用cell创建指令的cell操作基元。 它们就像cell一样不可改变,可以用以前保存的值和cell构建新的cell。 与cells不同,Builder类型的值只出现在TVM堆栈中,不能存储在持久存储中。 举例来说,这意味着类型为 Builder 的持久存储字段实际上是以cell的形式存储的。

Builder 类型表示部分组成的cell,为其定义了追加整数、其他cell、引用其他cell等快速操作:

虽然您可以使用它们来手动构建 cell,但强烈建议使用[结构体][structs]:使用结构体构建cell

Slices

Slice 是使用cell解析指令的cell操作基元。 与cell不同,它们是可变的,可以通过序列化指令提取或加载之前存储在cell中的数据。 此外,与cell不同,Slice 类型的值只出现在 TVM 堆栈中,不能存储在持久存储区中。 举例来说,这就意味着类型为 Slice 的持久存储字段实际上是以cell的形式存储的。

Slice 类型表示部分解析cell的剩余部分,或位于此类cell内并通过解析指令从中提取的值(子cell):

虽然您可以将它们用于cell的 手动解析,但强烈建议使用 [结构体][structs]:使用结构体解析cell

序列化类型

Int类型的序列化选项类似,CellBuilderSlice 在以下情况下也有不同的值编码方式:

  • 作为合约特性存储变量
  • 以及 [Structs](/zh-cn/book/structs and-messages#structs) 和 [Messages](/zh-cn/book/structs and-messages#messages) 的字段。
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

remaining 序列化选项可应用于 CellBuilderSlice类型的值。

它通过直接存储和加载cell值而不是作为引用来影响cell值的构建和解析过程。 它通过直接存储和加载cell值而不是作为引用来影响cell值的构建和解析过程。 与 cell操作指令 相似,指定 remaining 就像使用 Builder.storeSlice()Slice.loadBits() 而不是 Builder.storeRef()Slice.loadRef(),后者是默认使用的。

此外,Tact 产生的 TL-B 表示也会发生变化:

contract SerializationExample {
// By default
cRef: Cell; // ^cell in TL-B
bRef: Builder; // ^builder in TL-B
sRef: Slice; // ^slice in TL-B
// With `remaining`
cRem: Cell as remaining; // remainder<cell> in TL-B
bRem: Builder as remaining; // remainder<builder> in TL-B
sRem: Slice as remaining; // remainder<slice> in TL-B
// Constructor function,
// necessary for this example contract to compile
init() {
self.cRef = emptyCell();
self.bRef = beginCell();
self.sRef = emptySlice();
self.cRem = emptyCell();
self.bRem = beginCell();
self.sRem = emptySlice();
}
}

其中,TL-B 语法中的 ^cell^builder^slice 分别表示对 cellbuilderslice值的引用、而 cellbuildersliceremainder<…> 则表示给定值将直接存储为 Slice,而不是作为引用。

现在,举一个真实世界的例子,想象一下你需要注意到智能合约中的入站 jetton 传输并做出反应。 相应的 [信息][消息] 结构如下: 相应的 [信息][消息] 结构如下:

message(0x7362d09c) JettonTransferNotification {
queryId: Int as uint64; // arbitrary request number to prevent replay attacks
amount: Int as coins; // amount of jettons transferred
sender: Address; // address of the sender of the jettons
forwardPayload: Slice as remaining; // optional custom payload
}

合同中的 receiver 应该是这样的:

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

收到 jetton 传输通知消息后,其cell体会被转换为 Slice,然后解析为 JettonTransferNotification 消息。在此过程结束时,forwardPayload 将包含原始信息cell的所有剩余数据。 在此过程结束时,forwardPayload 将包含原始信息cell的所有剩余数据。

在这里,将 forwardPayload: Slice as remaining 字段放在 JettonTransferNotification 消息中的任何其他位置都不会违反 jetton 标准。 这是因为 Tact 禁止在[Structs][结构]和[Messages][消息]的最后一个字段之外的任何字段中使用 as remaining,以防止滥用合同存储空间并减少 gas 消耗。

bytes32

bytes64

操作

构建和解析

在 Tact 中,至少有两种构建和解析cell的方法:

手动

通过 Builder进行建造通过 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

结构和[消息][messages]几乎就是活生生的TL-B 模式。 也就是说,它们本质上是用可维护、可验证和用户友好的 Tact 代码表达的TL-B 模式。 也就是说,它们本质上是用可维护、可验证和用户友好的 Tact 代码表达的TL-B 模式

强烈建议使用它们及其 方法,如 Struct.toCell()和 [Struct.fromCell()][st-fc],而不是手动构造和解析cell,因为这样可以得到更多声明性和不言自明的合约。

上文的手动解析示例可以使用Structs重新编写,如果愿意,还可以使用字段的描述性名称:

// 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
}

请注意,Tact 的自动布局算法是贪婪的。 请注意,Tact 的自动布局算法是贪婪的。例如,struct Adventure 占用的空间很小,它不会以引用 Cell 的形式存储,而是直接以 Slice 的形式提供。

通过使用 结构 和 [消息][messages],而不是手动 Cell 组成和解析,这些细节将被简化,在优化布局发生变化时也不会造成任何麻烦。

检查是否为空

CellBuilder都不能直接检查空性,需要先将它们转换为Slice

要检查是否有任何位,请使用Slice.dataEmpty()。要检查是否存在引用,请使用Slice.refsEmpty()。要同时检查这两项,请使用Slice.empty()。 要检查是否存在引用,请使用 Slice.refsEmpty()。 要同时检查这两个文件,请使用 Slice.empty()

如果Slice不完全为空,也要抛出exit code 9,请使用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
}

检查是否相等

不能使用二进制相等 == 或不等式 != 操作符直接比较 Builder 类型的值。 但是,cellslice类型的值可以。

直接比较:

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

请注意,通过 ==!= 操作符进行的直接比较隐含地使用了标准 Cell 表示法SHA-256 哈希值。

还可使用 .hash() 进行显式比较:

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