以太坊 - 简单序列化(SSZ)

  • ethereum
  • 发布于 2025-04-04 17:37
  • 阅读 130

这篇文章详细介绍了简单序列化(SSZ)的实现和功能,包括类型定义、序列化与反序列化过程、Merkle化、以及SSZ与JSON的映射关系。文章结构清晰,逻辑严谨,包含示例代码和表格,适合对区块链数据结构有深入理解的读者。

SimpleSerialize (SSZ)

<!-- 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 每个字节的位数。

类型

基本类型

  • uintNN位无符号整数(其中 N in [8, 16, 32, 64, 128, 256]
  • byte:8位不透明数据容器,在序列化和哈希中等价于 uint8
  • booleanTrueFalse

复合类型

  • container:有序异构值集合
    • python dataclass 语法,包含键-类型对,例如:
      class ContainerExample(Container):
      foo: uint64
      bar: boolean
  • vector:有序固定长度同构集合,包含 N 个值
    • 表示法 Vector[type, N],例如 Vector[uint64, N]
  • list:有序可变长度同构集合,限制为 N 个值
    • 表示法 List[type, N],例如 List[uint64, N]
  • bitvector:有序固定长度的 boolean 值集合,包含 N
    • 表示法 Bitvector[N]
  • bitlist:有序可变长度的 boolean 值集合,限制为 N
    • 表示法 Bitlist[N]
  • union:包含给定子类型之一的联合类型
    • 表示法 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,但前者用于不透明数据,而后者旨在表示数字。

别名

为方便起见,我们为以下内容创建别名:

  • bitboolean
  • BytesNByteVector[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] &lt;&lt; (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] &lt;&lt; (i % 8)
array[len(value) // 8] |= 1 &lt;&lt; (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) &lt; 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

  • 可能具有多个选择器,其中类型相同。
  • 不应使用大于 127 的选择器(即最高位设置),这些保留为向后兼容的扩展。
  • 必须至少有 1 个类型选项。
  • 可以将 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),因为它给我们在偏移数据中的总字节数。
    • 向量/列表中每个对象的大小可以通过两个偏移的差来推断。要获取最后一个对象的大小,需要知道总字节数(通常无法反序列化未知长度的 SSZ 对象)。
  • 容器遵循与向量相同的原则,区别在于容器中可能还有固定大小对象。这意味着 fixed_parts 数据将包含偏移量以及固定大小对象。
  • 在 bitlists 的情况下,不能唯一地从对象中的字节数量推断出位长。因此,在结束时总是有一个设置的位。必须使用此位推断出 bitlist 的位数。
  • 在联合的情况下,反序列化范围的第一个字节将反序列化为类型选择器,范围的其余部分将反序列化为选定的类型。

请注意,反序列化需要防御无效输入。非详尽列表:

  • 偏移量:无序、越界、不匹配的最小元素大小。
  • 范围:额外未使用的字节,没有与元素大小对齐。
  • 超出列表限制的更多元素。部分强制共识。
  • Union 中选择的索引越界。

计算此对象的有效算法可以在 实现 中找到。

Merkle化

我们首先定义辅助函数:

  • 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):给定相同基本类型的有序对象:
    1. values 序列化为字节。
    2. 如果不是按照 BYTES_PER_CHUNK 的倍数对齐,右侧填充零到下一个倍数。
    3. 将字节划分为 BYTES_PER_CHUNK 字节的区块。
    4. 返回这些区块。
  • 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 化,并返回根:
    • Merkle 化依赖于有效输入,必须填充/限制:
    • 如果没有限制:用零 chunk 填充 chunksnext_pow_of_two(len(chunks))(从内存效率上虚拟处理)。
    • 如果 limit >= len(chunks),用零 chunk 填充 chunksnext_pow_of_two(limit)(从内存效率上虚拟处理)。
    • 如果 limit &lt; len(chunks):不进行 merkle 化,输入超出限制。相应引发错误。
    • 然后,merkle 化这些 chunks(空输入会填充 1 个零 chunk):
    • 如果是 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.valueNone,则 mix_in_selector(Bytes32(), 0)

摘要和扩展

A 为通过替换 B 的某些(可能嵌套)值而导出的对象,我们称 AB 的“摘要”,而 BA 的“扩展”。注意 hash_tree_root(A) == hash_tree_root(B)

我们同样定义“摘要类型”和“扩展类型”。例如,BeaconBlockBeaconBlockHeader 的扩展类型。注意,对象最多扩展为一个给定的扩展类型对象。例如,BeaconBlockHeader 对象唯一地扩展为 BeaconBlock 对象。

实现

请参见 https://github.com/ethereum/consensus-specs/issues/2138 以获取当前已知实现的列表。

JSON映射

规范 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 流中出现的一样。

ListVectorbyte (及其别名)被编码为 hex-byte-stringBitlistBitvector 同样将其 SSZ 字节编码映射到 hex-byte-string

Union 被编码为一个对象,包含一个 selectordata 字段,其中 data 的内容根据选择器而变化。

  • 原文链接: github.com/ethereum/cons...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
ethereum
ethereum
江湖只有他的大名,没有他的介绍。