Cells、Builders 和 Slices 是 TON 区块链的底层 primitives。 TON 区块链的虚拟机 TVM 使用cell来表示持久存储中的所有数据结构,以及内存中的大部分数据结构。 CellsCell是一种 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−1 和 255255之间的整数编码。 普通cell用 −1−1编码,特殊cell可用该范围内的任何其他整数编码。 奇异cell的子类型存储在其数据的前 88 位,这意味着有效的奇异cell总是至少有 88 个数据位。 TVM 目前支持以下exotic cell子类型: - Pruned branch cell,子类型编码为 [size=1.21em]11 - 它们代表删除的cell子树。
- Library reference cell,子类型编码为 [size=1.21em]22 - 它们用于存储库,通常在 masterchain 上下文中使用。
- Merkle proof cell,子类型编码为 [size=1.21em]33 - 它们用于验证其他cell的树数据的某些部分是否属于完整树。
- Merkle update cell,子类型编码为 [size=1.21em]44 - 它们总是有两个引用,对这两个引用的行为类似于默克尔证明。
; F8 G1 C" G* M 7 Z9 y; ~( t7 F j# W. N
! h- V1 x4 O F' `; I) W" {% M) L; l5 `& }% H0 X3 u
Levels作为 [四叉树][quadtree],每个单元格都有一个名为 level 的属性,它由 00 和 33之间的整数表示。 普通 cell的级别总是等于其所有引用级别的最大值。 也就是说,没有引用的普通 cell 的层级为 00。 Exotic cell有不同的规则来决定它们的层级,这些规则在TON Docs 的本页上有描述。 序列化在通过网络传输 cell 或在磁盘上存储 cell 之前,必须对其进行序列化。 有几种常用格式,如标准 Cell 表示法和 BoC。 标准表示法标准 Cell 表示法是 tvm.pdf 中首次描述的 cells 通用序列化格式。 它的算法以八进制(字节)序列表示cell,首先将称为描述符的第一个 22 字节序列化: - 引用描述符(Refs descriptor)根据以下公式计算:[size=1.21em]r+8k+32lr+8k+32l,其中 [size=1.21em]rr 是 cell 中包含的引用数量(介于 [size=1.21em]00 和 [size=1.21em]44 之间),[size=1.21em]kk 是 cell 类型的标志([size=1.21em]00 表示普通,[size=1.21em]11 表示特殊),[size=1.21em]ll 是 cell 的层级(介于 [size=1.21em]00 和 [size=1.21em]33 之间)。
- 位描述符(Bits descriptor)根据以下公式计算:[size=1.21em]⌊b8⌋+⌈b8⌉⌊8b⌋+⌈8b⌉,其中 [size=1.21em]bb 是 cell 中的位数(介于 [size=1.21em]00 和 [size=1.21em]10231023 之间)。 {2 W$ c" g2 ^( C0 I
然后,cell 本身的数据位被序列化为 ⌈b8⌉⌈8b⌉ 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 哈希值进行散列。 这是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中的数据。
, J" t0 U) B/ @ J: \3 V( J' [
此外,还有专门针对 exotic cell的指令来创建这些cell并期望它们的值。 此外,exotic cell 有专门的指令来创建它们并预期它们的值。不过,普通(ordinary) cell解析指令仍可用于 exotic cell,在这种情况下,它们会在反序列化尝试中被自动替换为 普通(ordinary) cell。 所有cell操作指令都需要将 Cell 类型的值转换为 Builder或 Slice类型,然后才能修改或检查这些cell。 BuildersBuilder 是一种用于使用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()
! `2 `7 R5 s# {- h+ t. q$ K
虽然您可以使用它们来手动构建 cell,但强烈建议使用[结构体][structs]:使用结构体构建cell。 SlicesSlice 是使用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()4 l: `# w9 n8 T# s0 v9 N, o
虽然您可以将它们用于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 {
. l# S' i0 n) @ - someCell: Cell as remaining;
* t: o- U" B2 |. h X. U( s. w* S - someSlice: Slice as bytes32;* \; x9 U$ [7 Y3 N, B
: G3 Z# }. ?. }/ J, H- // Constructor function,. u% T2 Y, }3 c
- // necessary for this example contract to compile# T9 g6 U/ Y; k" O, F5 S# Y1 C
- init() {$ z+ y- [& ^* v! ~4 e
- self.someCell = emptyCell();
: K1 ^3 A1 b0 N5 l! f4 J$ i$ F - self.someSlice = beginCell().storeUint(42, 256).asSlice();* x8 ]! D8 ]5 V+ R8 l k
- }9 A6 r5 b4 V R. r
- }
复制代码 remaining此外,Tact 产生的 TL-B 表示也会发生变化:
n) X9 I$ ^# }( u ( i5 {9 }0 a; x8 B4 R/ L6 {
- contract SerializationExample {
]% c/ i1 H8 h1 ^! T; |- R - // By default& [: I4 Q2 [' l5 ]5 U" S
- cRef: Cell; // ^cell in TL-B
7 `; f) h5 I; u, y - bRef: Builder; // ^builder in TL-B' _0 @6 v, a4 ?7 ~9 M' x8 s
- sRef: Slice; // ^slice in TL-B; ?: o0 `- ~0 y& {8 M5 k: ?* \
- 5 c8 {; f$ z$ i: o, ]0 E
- // With `remaining`3 e/ n- `- s! ?: a m+ h* K
- cRem: Cell as remaining; // remainder<cell> in TL-B$ s4 o8 k( O! b4 Q) A3 x4 |" q/ h
- bRem: Builder as remaining; // remainder<builder> in TL-B/ D: U5 o: u' Z
- sRem: Slice as remaining; // remainder<slice> in TL-B
; V2 a" p- r0 N: e$ T
c [" v) b$ x- // Constructor function,7 L1 y( \0 M8 B3 n/ a; {
- // necessary for this example contract to compile6 ]( @' i! ?3 p, L3 T. z9 j
- init() {% r9 T; {% X& ~8 m' t9 ^/ k4 p
- self.cRef = emptyCell();' q3 X- H* W f$ Z% L- c
- self.bRef = beginCell();
8 O6 z& a: L( V- W - self.sRef = emptySlice();
1 C" t- F+ m4 D! W- v - self.cRem = emptyCell();+ M- c. [+ V) Y7 ]+ j
- self.bRem = beginCell();
9 f, X1 R5 j. y/ a5 { - self.sRem = emptySlice();
2 i `+ n: T5 ~& ] - }" R7 L0 R, V' x) _% o
- }
复制代码 3 r( }1 y# l" s
0 i# s2 L8 n% f& O4 P7 R( Z
其中,TL-B 语法中的 ^cell、^builder 和 ^slice 分别表示对 cell、builder和 slice值的引用、而 cell、builder 或 slice 的 remainder<…> 则表示给定值将直接存储为 Slice,而不是作为引用。 现在,举一个真实世界的例子,想象一下你需要注意到智能合约中的入站 jetton 传输并做出反应。 相应的 [信息][消息] 结构如下: 相应的 [信息][消息] 结构如下: - message(0x7362d09c) JettonTransferNotification {0 R! q7 N' O. Z& g e
- queryId: Int as uint64; // arbitrary request number to prevent replay attacks
. m" Y' k7 d; h" p2 c u - amount: Int as coins; // amount of jettons transferred1 j, x0 X! j% P% Y, Q% A! V
- sender: Address; // address of the sender of the jettons
6 H6 Z) p$ p) ?. H - forwardPayload: Slice as remaining; // optional custom payload0 n' s0 t' w- @% P
- }
复制代码 9 S: O. [5 |( E( c
# F, M8 S+ v' W
* s+ _% J) S1 h( G" C: ]+ N' B( E6 ^! P3 O; y7 o1 c3 y
- receive(msg: JettonTransferNotification) {
1 |7 e5 G% r& K% d7 o - // ... you do you ...6 z- T- y' D7 ^, M b
- }
复制代码 9 f: g9 K4 _; |, r
& z% q2 L9 o2 v+ d/ d# K收到 jetton 传输通知消息后,其cell体会被转换为 Slice,然后解析为 JettonTransferNotification 消息。在此过程结束时,forwardPayload 将包含原始信息cell的所有剩余数据。 在此过程结束时,forwardPayload 将包含原始信息cell的所有剩余数据。 在这里,将 forwardPayload: Slice as remaining 字段放在 JettonTransferNotification 消息中的任何其他位置都不会违反 jetton 标准。 这是因为 Tact 禁止在[Structs][结构]和[Messages][消息]的最后一个字段之外的任何字段中使用 as remaining,以防止滥用合同存储空间并减少 gas 消耗。
1 i$ R8 V, }$ c$ q* ^6 u Using Structs结构和[消息][messages]几乎就是活生生的TL-B 模式。 也就是说,它们本质上是用可维护、可验证和用户友好的 Tact 代码表达的TL-B 模式。 也就是说,它们本质上是用可维护、可验证和用户友好的 Tact 代码表达的TL-B 模式。 强烈建议使用它们及其 方法,如 Struct.toCell()和 [Struct.fromCell()][st-fc],而不是手动构造和解析cell,因为这样可以得到更多声明性和不言自明的合约。 3 N2 k% _3 q( b/ m5 I" y
G% H" h1 D1 P% S- y# o( U4 Y. u X$ N3 |+ Q2 ?( Z4 p0 J' k! b* T W
- // First Struct0 \' G: J$ G3 a9 C
- struct Showcase {
( M/ v: M+ l" j5 j - id: Int as uint8;' M% e7 n0 O! A e1 |# L5 E* N
- someImportantNumber: Int as int8;
* ?* w7 t( n$ a8 R - isThatCool: Bool;0 Y% V* I) `4 k# C
- payload: Slice;
l9 n* ^( B6 z3 i% k" H - nanoToncoins: Int as coins;! i! _' J: y9 g" t9 n; c0 q
- wackyTacky: Address;
6 L/ Z, J. e/ n - jojoRef: Adventure; // another Struct
1 r/ `* a3 a% r4 W, p - }
/ w3 Z; `3 [9 |, D4 B - ) u* E+ g8 j; A& I1 R! U8 J
- // Here it is/ \3 ?7 m5 C9 j& G3 T
- struct Adventure {
% X2 a8 }5 z h. b' A - bizarre: Bool = true;
( V* h% n% }( ?1 ~* A - time: Bool = false;2 r" ~/ l8 g& a, }' J1 x, g
- }
% k* C4 F( ? E- r) P5 V& \2 p R
6 b; M' M0 H0 K- v4 Y0 \( l6 H, G9 M- fun example() {
. M& H% {) a: o# R- P4 n- R - // Basics
% H4 @2 r3 x4 Z8 l - let s = Showcase.fromCell(
# _$ a, X9 _: R - Showcase{, g6 ?% N1 J9 ^0 ~
- id: 7,& ^- c6 t/ u) B/ {5 K
- someImportantNumber: 42,/ a7 `% R" P; C" \2 H$ J
- isThatCool: true,% E0 s5 t( s( u& C5 I" \: `6 u; P! S T
- payload: emptySlice(),( a! g' }% M6 @; Q; e% p* \
- nanoToncoins: 1330 + 7,
) P' ~0 [$ e* O: T1 n, Y - wackyTacky: myAddress(),
: e1 w" H0 y) O5 V' ^4 p) e - jojoRef: Adventure{ bizarre: true, time: false },
+ i2 C1 I- h2 u) g - }.toCell());
9 Z6 f+ ]. ~4 z8 Q: d5 h# k - s.isThatCool; // true2 s' @; [5 ]0 z( b: d, C
- }
复制代码
D+ Y/ t, X* @0 O9 h8 ^( k9 |1 D( @$ w9 T: v! ] g- D
O: b; N) U6 |
|