这篇文章详细介绍了简单序列化(SSZ)的实现和功能,包括类型定义、序列化与反序列化过程、Merkle化、以及SSZ与JSON的映射关系。文章结构清晰,逻辑严谨,包含示例代码和表格,适合对区块链数据结构有深入理解的读者。
<!-- mdformat-toc start --slug=github --no-anchors --maxlevel=6 --minlevel=2 -->
<!-- mdformat-toc end -->
| 名称 | 值 | 描述 |
|---|---|---|
BYTES_PER_CHUNK |
32 |
每个区块的字节数。 |
BYTES_PER_LENGTH_OFFSET |
4 |
每个序列化长度偏移所占的字节数。 |
BITS_PER_BYTE |
8 |
每个字节的位数。 |
uintN:N位无符号整数(其中 N in [8, 16, 32, 64, 128, 256])byte:8位不透明数据容器,在序列化和哈希中等价于 uint8boolean:True 或 Falseclass ContainerExample(Container):
foo: uint64
bar: booleanN 个值
Vector[type, N],例如 Vector[uint64, N]N 个值
List[type, N],例如 List[uint64, N]boolean 值集合,包含 N 位
Bitvector[N]boolean 值集合,限制为 N 位
Bitlist[N]Union[type_0, type_1, ...],例如 union[None, uint64, uint32]注意:Vector[boolean, N] 和 Bitvector[N] 都是有效的,但由于它们的序列化要求不同,因此是不同的。同样,List[boolean, N] 和 Bitlist[N] 也是有效的,但不同。通常因为它们的序列化效率,Bitvector[N]/Bitlist[N] 是首选。
我们递归定义“可变大小”类型为列表、联合、Bitlist 和所有包含可变大小类型的类型。所有其他类型称为“固定大小”。
尽管 byte 的 SSZ 序列化等同于 uint8,但前者用于不透明数据,而后者旨在表示数字。
为方便起见,我们为以下内容创建别名:
bit 指 booleanBytesN 和 ByteVector[N] 指 Vector[byte, N](这不是基本类型)ByteList[N] 指 List[byte, N]别名在语义上与其基础类型等价,因此在 SSZ 和相关格式中共享规范表示。
假设有一个辅助函数 default(type) 返回 type 的默认值,我们可以递归定义所有类型的默认值。
| 类型 | 默认值 |
|---|---|
uintN |
0 |
boolean |
False |
Container |
[default(type) for type in container] |
Vector[type, N] |
[default(type)] * N |
Bitvector[N] |
[False] * N |
List[type, N] |
[] |
Bitlist[N] |
[] |
Union[type_0, type_1, ...] |
default(type_0) |
is_zero如果一个 SSZ 对象等于该类型的默认值,则称其为零值(因此,is_zero(object) 返回 true)。
Vector[type, 0], Bitvector[0]) 是非法的。Union 类型中的 None 类型选项仅在第一个选项中合法(即索引为零)。我们递归定义 serialize 函数,该函数接收一个对象 value(指定类型),并返回类型为 bytes 的字节串。
注意:在下面的函数定义中 (serialize, hash_tree_root, is_variable_size 等) 对象隐式携带其类型。
uintNassert N in [8, 16, 32, 64, 128, 256]
return value.to_bytes(N // BITS_PER_BYTE, "little")
booleanassert value in (True, False)
return b"\x01" if value is True else b"\x00"
Bitvector[N]array = [0] * ((N + 7) // 8)
for i in range(N):
array[i // 8] |= value[i] << (i % 8)
return bytes(array)
Bitlist[N]请注意,从偏移编码中,已知 bitlist 的长度(以字节为单位)。在末尾添加一个额外的1位,位于索引 e,其中 e 是 bitlist 的长度(不是限制),以便位数也可知。
array = [0] * ((len(value) // 8) + 1)
for i in range(len(value)):
array[i // 8] |= value[i] << (i % 8)
array[len(value) // 8] |= 1 << (len(value) % 8)
return bytes(array)
## 递归序列化
fixed_parts = [serialize(element) if not is_variable_size(element) else None for element in value]
variable_parts = [serialize(element) if is_variable_size(element) else b"" for element in value]
## 计算并检查长度
fixed_lengths = [len(part) if part != None else BYTES_PER_LENGTH_OFFSET for part in fixed_parts]
variable_lengths = [len(part) for part in variable_parts]
assert sum(fixed_lengths + variable_lengths) < 2**(BYTES_PER_LENGTH_OFFSET * BITS_PER_BYTE)
## 将可变大小部分的偏移与固定大小部分交错
variable_offsets = [serialize(uint32(sum(fixed_lengths + variable_lengths[:i]))) for i in range(len(value))]
fixed_parts = [part if part != None else variable_offsets[i] for i, part in enumerate(fixed_parts)]
## 返回将固定大小部分(交错的偏移)与可变大小部分连接的结果
return b"".join(fixed_parts + variable_parts)
一个类型为 Union[T...] 的 value 具有属性 value.value,包含值,以及 value.selector,用于索引选定的 Union 类型选项 T。
一个 Union:
None 作为第一个类型选项,即 selector == 0None,则必须至少有 2 个类型选项。if value.value is None:
assert value.selector == 0
return b"\x00"
else:
serialized_bytes = serialize(value.value)
serialized_selector_index = value.selector.to_bytes(1, "little")
return serialized_selector_index + serialized_bytes
由于序列化是单射函数(即两个不同的同类型对象将序列化为不同的值),任何字节串至多有一个对象可反序列化。
反序列化可以使用递归算法来实现。基本对象的反序列化很简单,从那里我们可以找到所有固定大小对象的简单递归算法。对于可变大小对象,我们必须根据对象的种类执行以下操作之一:
BYTES_PER_LENGTH_OFFSET 字节每个)。
BYTES_PER_LENGTH_OFFSET),因为它给我们在偏移数据中的总字节数。fixed_parts 数据将包含偏移量以及固定大小对象。请注意,反序列化需要防御无效输入。非详尽列表:
Union 中选择的索引越界。计算此对象的有效算法可以在 实现 中找到。
我们首先定义辅助函数:
size_of(B),其中 B 是基本类型:基本类型序列化形式的长度(以字节为单位)。chunk_count(type):计算类型的 Merkle 化的叶数量。
1Bitlist[N] 和 Bitvector[N]:(N + 255) // 256(按区块大小除,向上取整)List[B, N] 和 Vector[B, N],其中 B 是基本类型:(N * size_of(B) + 31) // 32(按区块大小除,向上取整)List[C, N] 和 Vector[C, N],其中 C 是复合类型:Nlen(fields)pack(values):给定相同基本类型的有序对象:
values 序列化为字节。BYTES_PER_CHUNK 的倍数对齐,右侧填充零到下一个倍数。BYTES_PER_CHUNK 字节的区块。pack_bits(bits):给定 bitlist 或 bitvector 的位,获取 bitfield_bytes,通过将其打包到字节中并对齐到开始进行处理。bitlist 的长度限定位被排除。然后返回 pack(bitfield_bytes)。next_pow_of_two(i):获取 i 的下一个 2 的幂,如果不是已经是 2 的幂,0 映射到 1。示例:0->1, 1->1, 2->2, 3->4, 4->4, 6->8, 9->16merkleize(chunks, limit=None):给定有序的 BYTES_PER_CHUNK 字节的 chunks,对其进行 merkle 化,并返回根:
chunks 到 next_pow_of_two(len(chunks))(从内存效率上虚拟处理)。limit >= len(chunks),用零 chunk 填充 chunks 到 next_pow_of_two(limit)(从内存效率上虚拟处理)。limit < len(chunks):不进行 merkle 化,输入超出限制。相应引发错误。1 chunk:根就是 chunk 本身。> 1 chunks:作为二叉树进行 merkle 化。mix_in_length:给定 Merkle 根 root 和长度 length("uint256" 小端序列化),返回 hash(root + length)。mix_in_selector:给定 Merkle 根 root 和类型选择器 selector("uint256" 小端序列化),返回 hash(root + selector)。现在我们定义对象 value 的 Merkle 化 hash_tree_root(value) 递归地进行:
value 是基本对象或基本对象的向量,则 merkleize(pack(value))。value 是 bitvector,则 merkleize(pack_bits(value), limit=chunk_count(type))。value 是基本对象的列表,则 mix_in_length(merkleize(pack(value), limit=chunk_count(type)), len(value))。value 是 bitlist,则 mix_in_length(merkleize(pack_bits(value), limit=chunk_count(type)), len(value))。value 是复合对象的向量或容器,则 merkleize([hash_tree_root(element) for element in value])。value 是复合对象的列表,则 mix_in_length(merkleize([hash_tree_root(element) for element in value], limit=chunk_count(type)), len(value))。value 是联合类型,并且 value.value 不是 None,则 mix_in_selector(hash_tree_root(value.value), value.selector)。value 是联合类型,并且 value.value 是 None,则 mix_in_selector(Bytes32(), 0)。令 A 为通过替换 B 的某些(可能嵌套)值而导出的对象,我们称 A 是 B 的“摘要”,而 B 是 A 的“扩展”。注意 hash_tree_root(A) == hash_tree_root(B)。
我们同样定义“摘要类型”和“扩展类型”。例如,BeaconBlock 是 BeaconBlockHeader 的扩展类型。注意,对象最多扩展为一个给定的扩展类型对象。例如,BeaconBlockHeader 对象唯一地扩展为 BeaconBlock 对象。
请参见 https://github.com/ethereum/consensus-specs/issues/2138 以获取当前已知实现的列表。
规范 JSON 映射为每个 SSZ 类型分配对应的 JSON 编码,使 SSZ 架构也能定义 JSON 编码。
在解码 JSON 数据时,SSZ 架构中的所有字段必须存在且有值。解析器可以忽略额外的 JSON 字段。
| SSZ | JSON | 示例 |
|---|---|---|
uintN |
字符串 | "0" |
byte |
十六进制字节字符串 | "0x00" |
boolean |
布尔值 | false |
Container |
对象 | { "field": ... } |
Vector[type, N] |
数组 | [element, ...] |
Vector[byte, N] |
十六进制字节字符串 | "0x1122" |
Bitvector[N] |
十六进制字节字符串 | "0x1122" |
List[type, N] |
数组 | [element, ...] |
List[byte, N] |
十六进制字节字符串 | "0x1122" |
Bitlist[N] |
十六进制字节字符串 | "0x1122" |
Union[type_0, type_1, ...] |
选择器对象 | { "selector": number, "data": type_N } |
整数被编码为字符串,以避免在 64 位值中的精度损失。
别名被编码为其基础类型。
hex-byte-string 是十六进制编码的字节数据,前面带有 0x,就像在 SSZ 流中出现的一样。
List 和 Vector 的 byte (及其别名)被编码为 hex-byte-string。Bitlist 和 Bitvector 同样将其 SSZ 字节编码映射到 hex-byte-string。
Union 被编码为一个对象,包含一个 selector 和 data 字段,其中 data 的内容根据选择器而变化。
- 原文链接: github.com/ethereum/cons...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!