细胞、建造者和切片
单元、构建器和切片是 TON 区块链的底层基元。 TON 区块链的虚拟机 TVM使用单元格来表示持久存储中的所有数据结构,以及内存中的大部分数据结构。
Cells
Cell
是一种基元和数据结构,它通常由多达 个连续排列的比特和多达 个指向其他单元格的引用(refs)组成。 禁止循环引用,也不能通过TVM的方式创建循环引用,这意味着单元格可以被视为四叉树或有向无环图(DAG)。 合同代码本身由单元格树形结构表示。
单元和 单元基元 是面向比特的,而不是面向字节的:TVM 将保存在单元中的数据视为最多为 比特的序列(字符串或数据流),而不是字节。 如有必要,合约可以自由使用 -bit 整数字段,将其序列化为 TVM 单元,从而使用更少的持久存储字节来表示相同的数据。
Kinds
虽然 TVM 类型 单元格
指的是所有单元格,但有不同的单元格类型,其内存布局也各不相同。 前面描述的单元格(#cells)通常被称为_ordinary_(或 simple)单元格—这是最简单、最常用的单元格,只能包含数据。 绝大多数关于细胞及其用法的描述、指南和参考文献都假定细胞是普通的。
其他类型的细胞统称为_外来细胞_(或特殊细胞)。 它们有时会出现在 TON 区块链上的区块和其他数据结构的实际表示中。 它们的内存布局和用途与普通电池大不相同。
所有细胞的种类(或亚型)都由 和 之间的整数编码。 普通单元格用 编码,特殊单元格可用该范围内的任何其他整数编码。 奇异单元的子类型存储在其数据的前 位,这意味着有效的奇异单元总是至少有 个数据位。
TVM目前支持以下奇异细胞子类型:
- 剪枝单元格,子类型编码为 - 它们代表删除的单元格子树。
- 图书馆引用单元,子类型编码为 - 它们用于存储图书馆,通常在masterchain上下文中使用。
- 梅克尔证明单元,子类型编码为 - 它们用于验证其他单元的树数据的某些部分是否属于完整树。
- 梅克尔更新单元,子类型编码为 - 它们总是有两个引用,对这两个引用的行为类似于梅克尔证明。
Levels
作为 四叉树,每个单元格都有一个名为 level 的属性,它由 和 之间的整数表示。 普通 单元格的级别总是等于其所有引用级别的最大值。 也就是说,没有引用的普通单元格的电平等于 。
外来细胞有不同的规则来决定它们的等级,这些规则在TON Docs 的本页上有描述。
Serialization
在通过网络传输单元格或在磁盘上存储单元格之前,必须对其进行序列化。 有几种常用格式,如标准 Cell
表示法和BoC。
Standard representation
标准单元格
表示法是tvm.pdf中首次描述的单元格通用序列化格式。 它的算法以八进制(字节)序列表示单元,首先将称为描述符的第一个 字节序列化:
- Refs descriptor 的计算公式如下: ,其中 是单元格中包含的引用数(介于 和 之间), 是单元格类型标志( 表示 ordinary 和 表示 exotic ), 是单元格的 level (介于 和 之间)。
- Bits descriptor 的计算公式为 ,其中 是单元格中的位数(介于 和 之间)。
然后,单元本身的数据位被序列化为 -bit octets(字节)。 如果 不是 8 的倍数,则在数据位上附加一个二进制 和最多六个二进制 s。
接下来, 字节存储了引用的深度,即单元格树根(当前单元格)和最深引用(包括它)之间的单元格数。 例如,如果一个单元格只包含一个引用而没有其他引用,则其深度为 ,而被引用单元格的深度为 。
最后,为每个参考单元存储其标准表示的SHA-256 哈希值,每个参考单元占用 字节,并递归重复上述算法。 请注意,不允许循环引用单元格,因此递归总是以定义明确的方式结束。
如果我们要计算这个单元格的标准表示的哈希值,就需要将上述步骤中的所有字节连接在一起,然后使用 SHA-256 哈希值进行散列。 这是TVM的HASHCU
和HASHSU
指令以及 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
类型的值转换为 Builder
或 Slice
类型,然后才能修改或检查这些单元。
Builders
Builder
是一种用于使用单元格创建指令的单元格操作基元。 它们就像单元格一样不可改变,可以用以前保存的值和单元格构建新的单元格。 与单元格不同,Builder
类型的值只出现在TVM堆栈中,不能存储在持久存储区中。 举例来说,这意味着类型为 Builder
的持久存储字段实际上是以单元格的形式存储的。
Builder
类型表示部分组成的单元格,为其定义了追加整数、其他单元格、引用其他单元格等快速操作:
- 核心库中的
Builder.storeUint()
- 核心库中的
Builder.storeInt()
- 核心库中的
Builder.storeBool()
- 核心库中的
Builder.storeSlice()
- 核心库中的
Builder.storeCoins()
- 核心库中的
Builder.storeAddress()
- 核心库中的
Builder.storeRef()
虽然您可以使用它们来手动构建单元格,但强烈建议使用[结构体][structs]:使用结构体构建单元格。
Slices
Slice
是使用单元格解析指令的单元格操作基元。 与单元格不同,它们是可变的,可以通过序列化指令提取或加载之前存储在单元格中的数据。 此外,与单元格不同,Slice
类型的值只出现在TVM堆栈中,不能存储在持久存储区中。 举例来说,这就意味着类型为 Slice
的持久存储字段实际上是以单元格的形式存储的。
Slice
类型表示部分解析单元格的剩余部分,或位于此类单元格内并通过解析指令从中提取的值(子单元格):
- 核心库中的
Slice.loadUint()
- 核心库中的
Slice.loadInt()
- 核心库中的
Slice.loadBool()
- 核心库中的
Slice.loadBits()
- 核心库中的
Slice.loadCoins()
- 核心库中的
Slice.loadAddress()
- 核心库中的
Slice.loadRef()
虽然您可以将它们用于单元格的 手动解析,但强烈建议使用 [结构体][structs]:使用结构体解析单元格。
Serialization types
与 Int
类型的序列化选项类似,Cell
、Builder
和Slice
在以下情况下也有不同的值编码方式:
- 作为 contracts 和 traits 的 storage variables 、
- 以及 [Structs](/book/structs and-messages#structs) 和 [Messages](/book/structs and-messages#messages) 的字段。
remaining
remaining
序列化选项可应用于 Cell
、Builder
和 Slice
类型的值。
它通过直接存储和加载单元格值而不是作为引用来影响单元格值的构建和解析过程。 与 单元操作指令 相似,指定 remaining
就像使用 Builder.storeSlice()
和 Slice.loadBits()
而不是 Builder.storeRef()
和 Slice.loadRef()
,后者是默认使用的。
此外,Tact 产生的 TL-B 表示也会发生变化:
其中,TL-B 语法中的 ^cell
、^builder
和 ^slice
分别表示对 cell
、builder
和 slice
值的引用、而 cell
、builder
或 slice
的 remainder<…>
则表示给定值将直接存储为 Slice
,而不是作为引用。
现在,举一个真实世界的例子,想象一下你需要注意到智能合约中的入站 jetton 传输并做出反应。 相应的 [信息][消息] 结构如下:
合同中的 receiver 应该是这样的:
收到 jetton 传输通知消息后,其单元体会被转换为 Slice
,然后解析为 JettonTransferNotification
[消息][消息]。在此过程结束时,forwardPayload
将包含原始信息单元的所有剩余数据。
在这里,将 forwardPayload: Slice as remaining
字段放在 JettonTransferNotification
[消息][消息]中的任何其他位置都不会违反 jetton 标准。这是因为 Tact 禁止在 [Structs][结构] 和 [Messages][消息] 的最后一个字段之外的任何字段中使用 as remaining
,以防止滥用合约存储空间并减少 gas 消耗。
bytes32
bytes64
Operations
Construct and parse
在 Tact 中,至少有两种构建和解析单元格的方法:
Manually
Using Structs
结构和[消息][messages]几乎就是活生生的TL-B 模式。 也就是说,它们本质上是用可维护、可验证和用户友好的 Tact 代码表达的TL-B 模式。
强烈建议使用它们及其 方法,如 Struct.toCell()
和 Struct.fromCell()
,而不是手动构造和解析单元格,因为这样可以得到更多声明性和不言自明的合约。
上文的手动解析示例可以使用Structs重新编写,如果愿意,还可以使用字段的描述性名称:
请注意,Tact 的自动布局算法是贪婪的。例如,struct Adventure
占用的空间很小,它不会以引用 Cell
的形式存储,而是直接以 Slice
的形式提供。
通过使用 结构 和 [消息][messages],而不是手动 Cell
组成和解析,这些细节将被简化,在优化布局发生变化时也不会造成任何麻烦。
Check if empty
Cell
和Builder
都不能直接检查空性,需要先将它们转换为Slice
。
要检查是否有任何位,请使用[Slice.dataEmpty()
][s-de]。要检查是否存在引用,请使用[Slice.refsEmpty()
][s-re]。要同时检查这两项,请使用Slice.empty()
。
如果Slice
不完全为空,也要抛出exit code 9,请使用Slice.endParse()
。
Check if equal
不能使用二进制相等 ==
或不等式 !=
操作符直接比较 Builder
类型的值。但是,Cell
和 Slice
类型的值可以。
直接比较:
请注意,通过 ==
或 !=
操作符进行的直接比较隐含地使用了标准 Cell
表示法的 SHA-256 哈希值。
还可使用 .hash()
进行显式比较: