这篇文章详细介绍了简单序列化(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位不透明数据容器,在序列化和哈希中等价于 uint8
boolean
:True
或 False
class ContainerExample(Container):
foo: uint64
bar: boolean
N
个值
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
指 boolean
BytesN
和 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
等) 对象隐式携带其类型。
uintN
assert N in [8, 16, 32, 64, 128, 256]
return value.to_bytes(N // BITS_PER_BYTE, "little")
boolean
assert 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 == 0
None
,则必须至少有 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 化的叶数量。
1
Bitlist[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
是复合类型:N
len(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->16
merkleize(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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!