简单序列化
简单序列化 (SSZ) 是信标链上使用的序列化方法。它在整个共识层中取代了执行层上使用的 RLP 序列化,但对等节点发现协议除外。要了解有关 RLP 序列化的更多信息,请参阅递归长度前缀 (RLP)。SSZ 旨在具有确定性,并能高效地进行默克尔化。可以认为 SSZ 包含两个组件:一个序列化方案,以及一个旨在与序列化数据结构高效配合的默克尔化方案。
SSZ 是如何工作的?
序列化
SSZ 是一种非自描述的序列化方案——相反,它依赖于必须提前知晓的模式 (schema)。SSZ 序列化的目标是将任意复杂度的对象表示为字节串。对于“基本类型”,这是一个非常简单的过程。元素只需转换为十六进制字节。基本类型包括:
- 无符号整数
- 布尔值
对于复杂的“复合”类型,序列化会更加复杂,因为复合类型包含多个可能具有不同类型或不同大小(或两者兼有)的元素。如果这些对象都具有固定长度(即无论其实际值如何,元素的大小始终保持不变),序列化只需将复合类型中的每个元素按顺序转换为小端字节串。然后将这些字节串拼接在一起。序列化对象中固定长度元素的字节列表表示顺序,与其在反序列化对象中出现的顺序相同。
对于可变长度的类型,实际数据在序列化对象中该元素的位置会被替换为“偏移量 (offset)”值。实际数据会被添加到序列化对象末尾的堆 (heap) 中。偏移量值是实际数据在堆中起始位置的索引,充当指向相关字节的指针。
下面的示例说明了偏移量如何应用于同时包含固定长度和可变长度元素的容器:
struct Dummy {
number1: u64,
number2: u64,
vector: Vec<u8>,
number3: u64
}
dummy = Dummy{
number1: 37,
number2: 55,
vector: vec![1,2,3,4],
number3: 22,
}
serialized = ssz.serialize(dummy)
serialized 将具有以下结构(此处仅填充为 4 位,实际中填充为 32 位,并保留 int 表示形式以求清晰):
[37, 0, 0, 0, 55, 0, 0, 0, 16, 0, 0, 0, 22, 0, 0, 0, 1, 2, 3, 4]
------------ ----------- ----------- ----------- ----------
| | | | |
数字1 数字2 向量的偏移量 数字3 向量的值
为了清晰起见,分行显示:
[
37, 0, 0, 0, # `number1` 的小端编码。
55, 0, 0, 0, # `number2` 的小端编码。
16, 0, 0, 0, # 指示 `vector` 的值从何处开始的“偏移量”(小端编码的 16)。
22, 0, 0, 0, # `number3` 的小端编码。
1, 2, 3, 4, # `vector` 中的实际值。
]
这仍然是一种简化——上述示意图中的整数和零实际上将存储为字节列表,如下所示:
[
10100101000000000000000000000000 # `number1` 的小端编码
10110111000000000000000000000000 # `number2` 的小端编码。
10010000000000000000000000000000 # 指示 `vector` 的值从何处开始的“偏移量”(小端编码的 16)。
10010110000000000000000000000000 # `number3` 的小端编码。
10000001100000101000001110000100 # `bytes` 字段的实际值。
]
因此,可变长度类型的实际值存储在序列化对象末尾的堆中,而它们的偏移量则存储在有序字段列表中各自正确的位置。
还有一些需要特殊处理的特殊情况,例如 BitList 类型,它要求在序列化期间添加长度上限,并在反序列化期间将其移除。完整的详细信息可在 SSZ 规范 (opens in a new tab)中找到。
反序列化此对象需要 模式。模式定义了序列化数据的精确布局,以便每个特定元素都能从字节斑点反序列化为有意义的对象,并且其中的元素具有正确的类型、值、大小和位置。正是模式告诉反序列化器哪些值是实际值,哪些是偏移量。当对象被序列化时,所有字段名称都会消失,但在反序列化时会根据模式重新实例化。
默克尔化
然后可以对这个 SSZ 序列化对象进行默克尔化——即转换为相同数据的默克尔树表示形式。首先,确定序列化对象中 32 字节块的数量。这些就是树的“叶子”。叶子的总数必须是 2 的幂,以便将叶子一起进行哈希处理最终生成单个哈希树根 (hash-tree-root)。如果自然情况下并非如此,则会添加包含 32 字节零的额外叶子。如图所示:
哈希树根
/ \
/ \
/ \
/ \
叶子哈希 叶子哈希
1 和 2 3 和 4
/ \ / \
/ \ / \
/ \ / \
叶子1 叶子2 叶子3 叶子4
也有一些情况,树的叶子并不像上例中那样自然均匀地分布。例如,叶子 4 可能是一个包含多个元素的容器,需要为默克尔树添加额外的“深度”,从而创建一棵不均匀的树。
我们不将这些树元素称为叶子 X、节点 X 等,而是可以赋予它们广义索引 (generalized indices),从根 = 1 开始,沿每一层从左到右计数。这就是上面解释的广义索引。序列化列表中的每个元素都有一个等于 2**depth + idx 的广义索引,其中 idx 是其在序列化对象中从零开始的索引位置,depth 是默克尔树中的层数,可以确定为元素(叶子)数量的以 2 为底的对数。
广义索引
广义索引是一个整数,表示二叉默克尔树中的一个节点,其中每个节点都有一个广义索引 2 ** depth + index in row。
1 --深度 = 0 2**0 + 0 = 1
2 3 --深度 = 1 2**1 + 0 = 2, 2**1+1 = 3
4 5 6 7 --深度 = 2 2**2 + 0 = 4, 2**2 + 1 = 5...
这种表示法为默克尔树中的每条数据生成一个节点索引。
多重证明
提供表示特定元素的广义索引列表,使我们能够根据哈希树根对其进行验证。这个根是我们公认的现实版本。我们获得的任何数据都可以通过将其插入默克尔树中的正确位置(由其广义索引确定)并观察根是否保持不变,来根据该现实进行验证。规范中此处 (opens in a new tab)的函数展示了如何计算验证特定广义索引集内容所需的最小节点集。
例如,要验证下树中索引 9 处的数据,我们需要索引 8、9、5、3、1 处数据的哈希。 (8,9) 的哈希应等于哈希 (4),它与 5 进行哈希处理生成 2,2 与 3 进行哈希处理生成树根 1。如果为 9 提供了不正确的数据,根就会改变——我们会检测到这一点,并导致分支验证失败。
* = 生成证明所需的数据
1*
2 3*
4 5* 6 7
8* 9* 10 11 12 13 14 15