Cells、Builders 和 Slices
Cells、Builders 和 Slices 是 TON 区块链的底层 primitives。 TON 区块链的虚拟机 TVM 使用cell来表示持久存储中的所有数据结构,以及内存中的大部分数据结构。
Cells
Cell
是一种 primitive 和数据结构,它通常由多达 个连续排列的比特和多达 个指向其他 cell 的引用(refs)组成。 循环引用在 TVM 中是被禁止的,因此无法通过 TVM 的机制创建循环引用。这意味着,单元(cells)可以被视为自身的 [四叉树][quadtree] 或 有向无环图(DAG)。 智能合约代码本身由树形结构的cell表示。
单元(Cells)和单元原语是以位(bit)为导向的,而非字节(byte)为导向的:TVM 将存储在单元中的数据视为最多 位的序列(字符串或流),而不是字节。 如有必要,合约可以自由使用 -bit 整数字段,并将其序列化为 TVM cell,从而使用更少的持久存储字节来表示相同的数据。
种类
虽然 TVM 类型 Cell
指的是所有cell,但存在不同的cell类型,其内存布局也各不相同。 前面 描述的通常被称为 普通 (或简单) cell—这是最简单、最常用的cell,只能包含数据。 绝大多数关于cell及其用法的描述、指南和 参考文献 都假定是普通cell。
其他类型的cell统称为 exotic cell (或特殊cell)。 它们有时会出现在 TON 区块链上的区块和其他数据结构的实际表示中。 它们的内存布局和用途与普通cell大不相同。
所有cell的种类 (或子类型) 都由 和 之间的整数编码。 普通cell用 编码,特殊cell可用该范围内的任何其他整数编码。 奇异cell的子类型存储在其数据的前 位,这意味着有效的奇异cell总是至少有 个数据位。
TVM 目前支持以下exotic cell子类型:
- Pruned branch cell,子类型编码为 - 它们代表删除的cell子树。
- Library reference cell,子类型编码为 - 它们用于存储库,通常在 masterchain 上下文中使用。
- Merkle proof cell,子类型编码为 - 它们用于验证其他cell的树数据的某些部分是否属于完整树。
- Merkle update cell,子类型编码为 - 它们总是有两个引用,对这两个引用的行为类似于默克尔证明。
Levels
作为 [四叉树][quadtree],每个单元格都有一个名为 level 的属性,它由 和 之间的整数表示。 普通 cell的级别总是等于其所有引用级别的最大值。 也就是说,没有引用的普通 cell 的层级为 。
Exotic cell有不同的规则来决定它们的层级,这些规则在TON Docs 的本页上有描述。
序列化
在通过网络传输 cell 或在磁盘上存储 cell 之前,必须对其进行序列化。 有几种常用格式,如标准 Cell
表示法和 BoC。
标准表示法
标准 Cell
表示法是 tvm.pdf 中首次描述的 cells 通用序列化格式。 它的算法以八进制(字节)序列表示cell,首先将称为描述符的第一个 字节序列化:
- 引用描述符(Refs descriptor)根据以下公式计算:,其中 是 cell 中包含的引用数量(介于 和 之间), 是 cell 类型的标志( 表示普通, 表示特殊), 是 cell 的层级(介于 和 之间)。
- 位描述符(Bits descriptor)根据以下公式计算:,其中 是 cell 中的位数(介于 和 之间)。
然后,cell 本身的数据位被序列化为 -bit octets(字节)。 如果 不是 8 的倍数,则在数据位上附加一个二进制 和最多六个二进制 s。
接下来, 字节存储了引用的深度,即Cell树根(当前Cell)和最深引用(包括它)之间的cells数。 例如,如果一个cell只包含一个引用而没有其他引用,则其深度为 ,而被引用cell的深度为 。
最后,为每个参考cell存储其标准表示的 SHA-256 哈希值,每个参考cell占用 字节,并递归重复上述算法。 请注意,不允许循环引用cell,因此递归总是以定义明确的方式结束。 请注意,不允许循环引用cell,因此递归总是以定义明确的方式结束。
如果我们要计算这个cell的标准表示的哈希值,就需要将上述步骤中的所有字节连接在一起,然后使用 SHA-256 哈希值进行散列。 如果我们要计算这个cell的标准表示的哈希值,就需要将上述步骤中的所有字节连接在一起,然后使用 SHA-256 哈希值进行散列。 这是TVM的HASHCU
和HASHSU
指令以及 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
类型的值转换为 Builder
或 Slice
类型,然后才能修改或检查这些cell。
Builders
Builder
是一种用于使用cell创建指令的cell操作基元。 它们就像cell一样不可改变,可以用以前保存的值和cell构建新的cell。 与cells不同,Builder
类型的值只出现在TVM堆栈中,不能存储在持久存储中。 举例来说,这意味着类型为 Builder
的持久存储字段实际上是以cell的形式存储的。
Builder
类型表示部分组成的cell,为其定义了追加整数、其他cell、引用其他cell等快速操作:
- 核心库中的
Builder.storeUint()
- 核心库中的
Builder.storeInt()
- 核心库中的
Builder.storeBool()
- 核心库中的
Builder.storeSlice()
- 核心库中的
Builder.storeCoins()
- 核心库中的
Builder.storeAddress()
- 核心库中的
Builder.storeRef()
虽然您可以使用它们来手动构建 cell,但强烈建议使用[结构体][structs]:使用结构体构建cell。
Slices
Slice
是使用cell解析指令的cell操作基元。 与cell不同,它们是可变的,可以通过序列化指令提取或加载之前存储在cell中的数据。 此外,与cell不同,Slice
类型的值只出现在 TVM 堆栈中,不能存储在持久存储区中。 举例来说,这就意味着类型为 Slice
的持久存储字段实际上是以cell的形式存储的。
Slice
类型表示部分解析cell的剩余部分,或位于此类cell内并通过解析指令从中提取的值(子cell):
- 核心库中的
Slice.loadUint()
- 核心库中的
Slice.loadInt()
- 核心库中的
Slice.loadBool()
- 核心库中的
Slice.loadBits()
- 核心库中的
Slice.loadCoins()
- 核心库中的
Slice.loadAddress()
- 核心库中的
Slice.loadRef()
虽然您可以将它们用于cell的 手动解析,但强烈建议使用 [结构体][structs]:使用结构体解析cell。
序列化类型
与 Int
类型的序列化选项类似,Cell
、Builder
和Slice
在以下情况下也有不同的值编码方式:
- 作为合约和特性的存储变量,
- 以及 [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
序列化选项可应用于 Cell
、Builder
和 Slice
类型的值。
它通过直接存储和加载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
分别表示对 cell
、builder
和 slice
值的引用、而 cell
、builder
或 slice
的 remainder<…>
则表示给定值将直接存储为 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的方法:
手动
Using Structs
结构和[消息][messages]几乎就是活生生的TL-B 模式。 也就是说,它们本质上是用可维护、可验证和用户友好的 Tact 代码表达的TL-B 模式。 也就是说,它们本质上是用可维护、可验证和用户友好的 Tact 代码表达的TL-B 模式。
强烈建议使用它们及其 方法,如 Struct.toCell()
和 [Struct.fromCell()
][st-fc],而不是手动构造和解析cell,因为这样可以得到更多声明性和不言自明的合约。
上文的手动解析示例可以使用Structs重新编写,如果愿意,还可以使用字段的描述性名称:
// First Structstruct 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 isstruct 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
组成和解析,这些细节将被简化,在优化布局发生变化时也不会造成任何麻烦。
检查是否为空
Cell
和Builder
都不能直接检查空性,需要先将它们转换为Slice
。
要检查是否有任何位,请使用Slice.dataEmpty()
。要检查是否存在引用,请使用Slice.refsEmpty()
。要同时检查这两项,请使用Slice.empty()
。 要检查是否存在引用,请使用 Slice.refsEmpty()
。 要同时检查这两个文件,请使用 Slice.empty()
。
如果Slice
不完全为空,也要抛出exit code 9,请使用Slice.endParse()
。
// Preparationslet someCell = beginCell().storeUint(42, 7).endCell();let someBuilder = beginCell().storeRef(someCell);
// Obtaining our Sliceslet slice1 = someCell.asSlice();let slice2 = someBuilder.asSlice();
// .dataEmpty()slice1.dataEmpty(); // falseslice2.dataEmpty(); // true
// .refsEmpty()slice1.refsEmpty(); // trueslice2.refsEmpty(); // false
// .empty()slice1.empty(); // falseslice2.empty(); // false
// .endParse()try { slice1.endParse(); slice2.endParse();} catch (e) { e; // 9}
检查是否相等
不能使用二进制相等 ==
或不等式 !=
操作符直接比较 Builder
类型的值。 但是,cell
和slice
类型的值可以。
直接比较:
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; // truelet areCellsNotEqual = a != b; // false
let areSlicesEqual = aSlice == bSlice; // truelet 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(); // truelet areCellsNotEqual = a.hash() != b.hash(); // false
let areSlicesEqual = aSlice.hash() == bSlice.hash(); // truelet areSlicesNotEqual = aSlice.hash() != bSlice.hash(); // false