跳转到内容

细胞、建造者和切片

单元构建器切片是 TON 区块链的底层基元。 TON 区块链的虚拟机 TVM使用单元格来表示持久存储中的所有数据结构,以及内存中的大部分数据结构。

Cells

Cell是一种基元和数据结构,它通常由多达 10231023 个连续排列的比特和多达 44 个指向其他单元格的引用(refs)组成。 禁止循环引用,也不能通过TVM的方式创建循环引用,这意味着单元格可以被视为四叉树有向无环图(DAG)。 合同代码本身由单元格树形结构表示。

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

Kinds

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

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

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

TVM目前支持以下奇异细胞子类型:

Levels

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

外来细胞有不同的规则来决定它们的等级,这些规则在TON Docs 的本页上有描述。

Serialization

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

Standard representation

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

  • Refs descriptor 的计算公式如下: r+8k+32lr + 8 * k + 32 * l,其中 rr 是单元格中包含的引用数(介于 0044之间), kk 是单元格类型标志(00 表示 ordinary11 表示 exotic ), ll 是单元格的 level (介于 0033之间)。
  • Bits descriptor 的计算公式为 b8+b8\lfloor\frac{b}{8}\rfloor + \lceil\frac{b}{8}\rceil,其中 bb 是单元格中的位数(介于 0010231023之间)。

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

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

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

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

Bag of Cells

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

在 TON Docs 中阅读有关 BoC 的更多信息:细胞袋

Immutability

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

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

此外,exotic单元格有专门的指令来创建它们并预期它们的值。不过,普通 单元格解析指令仍可用于奇异 单元格,在这种情况下,它们会在反序列化尝试中被自动替换为普通 单元格。

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

Builders

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

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

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

Slices

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

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

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

Serialization types

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

  • 作为 contractstraitsstorage variables
  • 以及 [Structs](/book/structs and-messages#structs) 和 [Messages](/book/structs and-messages#messages) 的字段。
contract SerializationExample {
someCell: Cell as remaining;
someSliceSlice as bytes32;
// 构造函数,
// 本示例合同编译所必需的
init() {
self.someCell = emptyCell();
self.someSlice = beginCell().storeUint(42, 256).asSlice();
}
}

remaining

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

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

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

contract SerializationExample {
// 默认情况下
cRef: Cell; // ^cell in TL-B
bRef: Builder; // ^builder in TL-B
sRef: Slice; // ^slice in TL-B
// With `remaining`
cRemCell as remaining; // remainder<cell> in TL-B
bRemBuilder as remaining; // remainder<builder> in TL-B
sRemSlice as remaining; // remainder<slice> in TL-B
// 构造函数,
// 本示例合同编译所必需
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 {
queryIdInt as uint64; // 任意请求编号,以防止重放攻击
amountInt as coins; // 传输的捷通数量
sender:地址; // 净币发送方的地址
forwardPayloadSlice as remaining; // 可选自定义有效载荷
}

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

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

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

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

bytes32

bytes64

Operations

Construct and parse

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

Manually

通过 Builder进行建造通过 切片 进行解析
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 模式

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

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

// First Struct
struct Showcase {
id: Int as uint8;
someImportantNumber: Int as int8;
isThatCoolBool;
payloadSlice;
nanoToncoinsInt as coins;
wackyTacky: Address;
jojoRef: Adventure; // another Struct
}
// Here it is
struct Adventure {
bizarre: Bool = true;
timeBool = 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 的自动布局算法是贪婪的。例如,struct Adventure 占用的空间很小,它不会以引用 Cell 的形式存储,而是直接以 Slice 的形式提供。

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

Check if empty

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

要检查是否有任何位,请使用[Slice.dataEmpty()][s-de]。要检查是否存在引用,请使用[Slice.refsEmpty()][s-re]。要同时检查这两项,请使用Slice.empty()

如果Slice不完全为空,也要抛出exit code 9,请使用Slice.endParse()

// 准备工作
let someCell = beginCell().storeUint(42, 7).endCell();
let someBuilder = beginCell().storeRef(someCell);
// 获取切片
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

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

直接比较:

a = beginCell().storeUint(123, 8).endCell();
aSlice = a.asSlice();
b = beginCell().storeUint(123, 8).endCell();
bSlice = b. asSlice(); 让 areCellsEqual = a == b; // true 让 areCellsNotEqual = a !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 areSliceEqual = aSlice.asSlice(); // truehash(); // true
let areCellsNotEqual = a.hash() != b.hash(); // false
let areSlicesEqual = aSlice.hash() == bSlice.hash(); // true
let areSlicesNotEqual = aSlice.hash() != bSlice.hash(); // false