简单序列化(SSZ) - 深入探讨以太坊中的SSZ

  • thogiti
  • 发布于 2024-05-03 15:47
  • 阅读 82

本文深入探讨了简单序列化(SSZ)在以太坊信标链中的应用,以及其与RLP序列化的比较。SSZ旨在提高以太坊共识层的效率、安全性和可扩展性,详细介绍了SSZ的基本类型、向量、列表、位向量、容器等序列化和反序列化过程,并提供了相关示例代码和图示,以帮助读者更好地理解SSZ的操作及其在以太坊中的重要性。

概述

Simple Serialize (SSZ) 是一个序列化和 Merkle化 方案,专为以太坊的 Beacon Chain 设计。SSZ 替代了执行层 (EL) 中使用的 RLP 序列化,在共识层 (CL) 的各个地方使用,除了 对等发现协议。它的开发和应用旨在增强以太坊 CL 的效率、安全性和可扩展性。

本文档是关于 SSZ 序列化的。你可以通过 merkleization 了解更多关于 SSZ Merkle 化的信息。

SSZ 工具

有许多工具可用于 SSZ。以下是 SSZ 工具的完整列表。下面是一些流行的工具:

SSZ 与 RLP 序列化的比较

标准 紧凑 表达能力 哈希化 索引
RLP 灵活 可行
SSZ 较差

表格:根据 Piper Merriam 的 SSZ 与 RLP 比较。

  1. 表达能力

    • SSZ:直接支持所有必要的数据类型,无需额外的抽象层。这使得 SSZ 本质上更直接,更适合处理以太坊 PoS 中使用的复杂数据结构。
    • RLP:仅限于动态长度字节字符串和列表。其他数据类型只能通过抽象层来支持,这可能会引入复杂性和潜在的低效。
  2. 哈希化

    • SSZ:方便高效的对象哈希和重新哈希,尤其适用于频繁更新数据状态的操作,例如分片和无状态客户端中的操作。这种效率对于维护区块链的完整性和性能至关重要。
    • RLP:虽然可以进行哈希处理,但并未提供相同的性能优化,特别是在数据结构进行小幅修改时。
  3. 索引

    • SSZ:虽然索引被描述为“较差”,但 SSZ 支持在不进行完全反序列化的情况下对序列化数据进行某种程度的直接访问,这对区块链内某些操作是有益的。
    • RLP:不支持高效索引,可能导致访问内部数据时复杂度为 O(N),这对大型网络的性能可能是显著的缺陷。
  4. 数据类型兼容性

    • SSZ:设计为与以太坊协议中的数据类型和结构完全兼容,提高了其在共识机制和网络操作中的实用性。
    • RLP:虽然灵活,但需要附加层来支持各种数据类型,这可能导致效率降低和实现复杂性增加。
  5. 确定性序列化

    • SSZ:提供确定性的序列化结果,确保相同的数据结构每次序列化为完全相同的字节序列,这对于共识可靠性至关重要。

基于这些原因,以太坊在全力推动全面迁移到 SSZ 序列化并停止使用 RLP 序列化。

SSZ 的工作原理 - 基本类型

以下是 SSZ 如何处理基本类型的序列化和反序列化:

flowchart TD
    A[开始序列化] --> B[选择数据类型]
    B --> C[无符号整数]
    B --> D[布尔值]

    C --> E[将整数转换为\n小端字节数组]
    E --> F[整数的序列化输出]

    D --> G["将布尔值转换为字节\n(True 转为 0x01, False 转为 0x00)"]
    G --> H[布尔值的序列化输出]

    classDef startEnd fill:#f9f,stroke:#333,stroke-width:4px;
    class A startEnd;
    classDef process fill:#ccf,stroke:#f66,stroke-width:2px;
    class B,C,D,E,G process;
    classDef output fill:#cfc,stroke:#393,stroke-width:2px;
    class F,H output;

图:基本类型的序列化过程。

flowchart TD
    A[开始反序列化] --> B[确定数据类型]
    B --> C[无符号整数]
    B --> D[布尔值]

    C --> E[读入小端字节数组]
    E --> F[重建原始整数值]
    F --> G[反序列化的整数输出]

    D --> H[读入字节]
    H --> I["将字节转为布尔值\n(0x01 转为 True, 0x00 转为 False)"]
    I --> J[反序列化的布尔值输出]

    classDef startEnd fill:#f9f,stroke:#333,stroke-width:4px;
    class A startEnd;
    classDef process fill:#ccf,stroke:#f66,stroke-width:2px;
    class B,C,D,E,H,I process;
    classDef output fill:#cfc,stroke:#393,stroke-width:2px;
    class G,J output;

图:基本类型的反序列化过程。

无符号整数

无符号整数(uintN)在 SSZ 中表示为 N 可以是 8、16、32、64、128 或 256 位。这些整数直接序列化为其小端字节表示形式,适用于大多数现代计算机架构并便于按字节级别进行操作。

无符号整数的序列化过程

  1. 输入:获取 uintN 类型的无符号整数。
  2. 转换为字节:将整数转换为长度为 N/8 的字节数组。例如,uint16 表示 2 个字节。
  3. 应用小端格式:以小端顺序排列字节,最低有效字节优先存储。
  4. 输出:得到的字节数组即为整数的序列化形式。

示例

  • 整数 1025 作为 uint16 将序列化为 01 04(十六进制)。先将 1025 转换为十六进制,得到 0x0401。在小端格式中,最低有效字节 (LSB) 优先。因此,0x0401 在小端格式为 01 04。字节数组 [01, 04] 就是序列化的输出。

无符号整数的反序列化过程

  1. 输入:读取表示序列化 uintN 的字节数组。
  2. 读取小端字节:以小端顺序解释字节,从而重建整数值。
  3. 输出:将字节数组转换回整数。

示例

  • 字节数组 01 04(十六进制)被反序列化为整数 1025。读取第一个字节 01 为整数的低位部分,04 为高位部分。重组时以大端格式可读,再转化为十六进制为 0401,即十进制的 1025

布尔值

SSZ 中的布尔值非常简单,每个布尔值用一个字节表示。

布尔值的序列化过程

  1. 输入:获取布尔值 (TrueFalse)。
  2. 转换为字节
    • 如果布尔值为 True,序列化为 01(十六进制)。
    • 如果布尔值为 False,序列化为 00
  3. 输出:得到的单字节即为布尔值的序列化形式。

示例

  • True 变为 01
  • False 变为 00

布尔值的反序列化过程

  1. 输入:读取一个字节。
  2. 解释字节
    • 字节为 01 表示 True
    • 字节为 00 表示 False
  3. 输出:与字节对应的布尔值。

示例

  • 字节 01 被反序列化为 True
  • 字节 00 被反序列化为 False

我们可以使用指定的 python Eth2 规范运行 SSZ 序列化和反序列化命令,如下所示,并验证上述字节数组。

>>> from eth2spec.utils.ssz.ssz_typing import uint64, boolean
## 序列化 
>>> uint64(1025).encode_bytes().hex()
'0104000000000000'
>>> boolean(True).encode_bytes().hex()
'01'
>>> boolean(False).encode_bytes().hex()
'00' 

## 反序列化 
>>> print(uint64.decode_bytes(bytes.fromhex('0104000000000000')))
1025
>>> print(boolean.decode_bytes(bytes.fromhex('01')))
1
>>> print(boolean.decode_bytes(bytes.fromhex('00')))
0

SSZ 在复合类型上的工作原理

向量

SSZ 中的向量用于处理固定长度的同质元素集合。以下是 SSZ 如何处理向量的序列化和反序列化的详细分解。

SSZ 向量的序列化

flowchart TD
    A[开始序列化] --> B[定义类型和长度的向量]
    B --> C[序列化每个元素]
    C --> D["将每个元素转换为\n字节数组(小端)"]
    D --> E[将所有字节数组连接]
    E --> F[输出序列化向量]

    classDef startEnd fill:#f9f,stroke:#333,stroke-width:4px;
    class A startEnd;
    classDef process fill:#ccf,stroke:#f66,stroke-width:2px;
    class B,C,D,E process;
    classDef output fill:#cfc,stroke:#393,stroke-width:2px;
    class F output;

图:SSZ 向量的序列化。

  1. 固定长度定义:向量在定义时指定可以包含的元素的具体长度和类型,例如 Vector[uint64, 4] 表示包含四个 64 位无符号整数的向量。

  2. 元素序列化

    • 向量中的每个元素按其类型独立序列化。
    • 对于基本类型如整数或布尔值,这意味着将每个元素转换为其字节表示。
    • 如果元素是复合类型,则根据其具体序列化规则序列化每个元素。
  3. 拼接

    • 每个元素的序列化输出按出现顺序拼接在一起。
    • 由于向量的长度和每个元素的大小是已知且固定的,因此在序列化输出中不需要额外的元数据(如长度前缀)。

示例: 对于具有元素 [256, 512, 768]Vector[uint64, 3],每个元素占 64 位或 8 个字节。序列化过程如下:

  1. 将每个整数转换为小端字节数组

    • 256 作为 uint64 变为 00 01 00 00 00 00 00 00
    • 512 作为 uint64 变为 00 02 00 00 00 00 00 00
    • 768 作为 uint64 变为 00 03 00 00 00 00 00 00
  2. 将这些字节数组连接

    • 结果连接的字节数组为 00 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 03 00 00 00 00 00 00

序列化输出

  • 00 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 03 00 00 00 00 00 00

SSZ 向量的反序列化

flowchart TD
    A[开始反序列化] --> B[接收序列化字节流]
    B --> C[根据元素大小识别并拆分字节流]
    C --> D[将每个字节段反序列化为其原始类型]
    D --> E[将元素重新组合为向量]
    E --> F[输出反序列化向量]

    classDef startEnd fill:#f9f,stroke:#333,stroke-width:4px;
    class A startEnd;
    classDef process fill:#ccf,stroke:#f66,stroke-width:2px;
    class B,C,D,E process;
    classDef output fill:#cfc,stroke:#393,stroke-width:2px;
    class F output;

图:SSZ 向量的反序列化。

  1. 固定长度利用

    • 反序列化器利用预定义的长度和类型来解析序列化数据。
    • 它知道每个元素所需的字节数和向量中元素的数量。
  2. 元素反序列化

    • 字节流根据每个元素的大小分为多个段。
    • 每个段根据向量中元素的类型独立进行反序列化。
  3. 重建

    • 将元素重新构建为原始形式(例如,将字节数组转换回整数或其他指定类型)。
    • 然后将这些元素聚合以重新形成原始的向量。

示例: 给定 Vector[uint64, 3] 的序列化数据:

  • 序列化字节数组:00 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 03 00 00 00 00 00 00
  1. 将数据解析为段

    • 每个段包含 8 个字节。
    • 第一个段:00 01 00 00 00 00 00 00 → 代表整数 256。
    • 第二个段:00 02 00 00 00 00 00 00 → 代表整数 512。
    • 第三个段:00 03 00 00 00 00 00 00 → 代表整数 768。
  2. 将每个段从小端字节数组转换回整数

    • 使用小端格式,读取并转换每个字节数组回其各自的 uint64 整数。
  3. 重建

    • 重建的向量为 [256, 512, 768]

我们可以在 python 中运行并验证如下示例:

>>> from eth2spec.utils.ssz.ssz_typing import uint8, uint16, Vector
>>> Vector[uint16, 3](256, 512, 768).encode_bytes().hex()
'000100000000000000020000000000000003000000000000'
>>> print(Vector[uint64, 3].decode_bytes(bytes.fromhex('000100000000000000020000000000000003000000000000')))
Vector[uint64, 3]<<len=3>>(256, 512, 768)
>>> 

列表

SSZ 中的列表对于管理具有指定最大长度 (N) 的变长同质元素集合至关重要。这种灵活性允许动态管理数据结构,如交易集合或变更状态组件,以适应网络的变化需求。

SSZ 列表的序列化

flowchart TD
    A[开始序列化] --> B[定义类型和最大长度的列表]
    B --> C[序列化每个元素]
    C --> D[将每个元素转换为字节数组 - 小端]
    D --> E[将所有字节数组连接]
    E --> F[可选: 包含长度元数据]
    F --> G[输出序列化列表]

    classDef startEnd fill:#f9f,stroke:#333,stroke-width:4px;
    class A startEnd;
    classDef process fill:#ccf,stroke:#f66,stroke-width:2px;
    class B,C,D,E,F process;
    classDef output fill:#cfc,stroke:#393,stroke-width:2px;
    class G output;

图:SSZ 列表的序列化。

  1. 定义列表:SSZ 中的列表以特定的元素类型和最大长度进行定义,记为 List[type, N]。此定义不仅限制了列表的最大容量,还告知如何进行序列化操作。

  2. 元素序列化

    • 列表中的每个元素根据其类型进行序列化。对于 uint64 元素,序列化过程包含将每个整数转换为字节数组。
  3. 拼接序列化元素

    • 序列化出的元素的输出顺序拼接在一起。序列化数据的总长度会根据在序列化时的元素数量而变化。
  4. 包含长度元数据(可选)

    • 根据实现要求,列表的长度可能在序列化数据的开头明确包含,以帮助解析和反验证在反序列化过程中的有效性。

示例: 对于包含元素 [1024, 2048, 3072]List[uint64, 5],序列化过程将包括:

  • 将每个整数转换为小端格式的字节数组:00 04 00 00 00 00 00 0000 08 00 00 00 00 00 0000 0C 00 00 00 00 00 00
  • 连接这些数组,得到 00 04 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 0C 00 00 00 00 00 00

SSZ 列表的反序列化

flowchart TD
    A[开始反序列化] --> B[接收序列化字节流]
    B --> C["识别并根据元素大小分割字节流"]
    C --> D[将每个字节段反序列化为 uint64]
    D --> E[将元素重新组合为列表]
    E --> F[输出反序列化列表]

    classDef startEnd fill:#f9f,stroke:#333,stroke-width:4px;
    class A startEnd;
    classDef process fill:#ccf,stroke:#f66,stroke-width:2px;
    class B,C,D,E process;
    classDef output fill:#cfc,stroke:#393,stroke-width:2px;
    class F output;

图:SSZ 列表的反序列化。

  1. 接收序列化数据:序列化字节流作为输入,包含为每个元素编码的字节数组。

  2. 解析并反序列化每个元素

    • 基于元素类型(比如 uint64),将序列化流按 8 字节的段进行解析。
    • 将每个字节数组反序列化为原始类型。
  3. 重建列表

    • 反序列化的元素被重新组合为原始列表。

示例: 给定序列化数据 00 04 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 0C 00 00 00 00 00 00 表示的 List[uint64, 5]

  • 将数据分为段:00 04 00 00 00 00 00 0000 08 00 00 00 00 00 0000 0C 00 00 00 00 00 00
  • 將每個段從小端轉回整数: 102420483072
  • 重建的列表为 [1024, 2048, 3072]

我们可以在上面示例中这样运行并验证:

>>> from eth2spec.utils.ssz.ssz_typing import uint8, List, Vector
>>> List[uint64, 5](1024, 2048, 3072).encode_bytes().hex()
'00040000000000000008000000000000000c000000000000'
>>> print(List[uint64, 5].decode_bytes(bytes.fromhex('00040000000000000008000000000000000c000000000000')))
List[uint64, 5]<<len=3>>(1024, 2048, 3072)
>>> 

列表是在 SSZ 中的变长对象,它们在包含另一对象时以不同的方式编码。因此,有一些小的开销。例如,下面的 AliceBob 对象有不同的编码。

>>> from eth2spec.utils.ssz.ssz_typing import uint8, Vector, List, Container
>>> class Alice(Container):
...     x: List[uint8, 3] # 变长
>>> class Bob(Container):
...     x: Vector[uint8, 3] # 固定长度
>>> Alice(x = [1, 2, 3]).encode_bytes().hex()
'04000000010203'
>>> Bob(x = [1, 2, 3]).encode_bytes().hex()
'010203'
>>> 

位向量

在 SSZ 中,位向量被用于管理固定长度的布尔值序列,通常表示为位。该数据结构对于紧凑存储二进制数据或标志非常高效,这是以太坊应用中常见的用于指示状态条件、权限或其他二进制设置的应用。

SSZ 位向量的序列化

flowchart TD
    A[开始序列化] --> B[定义大小为 N 的位向量]
    B --> C[将位打包到字节中]
    C --> D[从 LSB 到 MSB 的位顺序]
    D --> E[如果  N % 8 != 0 添加填充]
    E --> F[输出序列化字节数组]

    classDef startEnd fill:#f9f,stroke:#333,stroke-width:4px;
    class A startEnd;
    classDef process fill:#ccf,stroke:#f66,stroke-width:2px;
    class B,C,D,E process;
    classDef output fill:#cfc,stroke:#393,stroke-width:2px;
    class F output;

图:SSZ 位向量的序列化。

  1. 定义位向量:SSZ 中的位向量由它的长度 N 定义,指定可包含的位数。例如,Bitvector[256] 表示包含 256 位的位向量。

  2. 将位转换为字节

    • 位向量中的每个位都表示一个布尔值,0 对应 False1 对应 True
    • 这些位依照位的顺序被压缩到字节中,每个字节内部最低有效位 (LSB) 在前。这意味着位向量中的第一个位对应第一个字节的最低有效位。
  3. 字节数组形成

    • 将位序列化为字节数组,并将每 8 个位打包到一个字节内,直到所有位均被处理完毕。
    • 如果 N 不是 8 的倍数,最后一个字节中会包含少于 8 位的数据,并在最高有效位位置填充零。

示例: 对于 Bitvector[10] 的模式 1011010010

  • 前 8 位 (10110100) 形成第一个字节。
  • 剩余 2 位 (10) 用六个零填充形成第二个字节:10000000
  • 序列化输出为 B4 80(十六进制)。

SSZ 位向量的反序列化

flowchart TD
    A[开始反序列化] --> B[接收序列化字节数组]
    B --> C[读取每个字节]
    C --> D[将字节转换为位]
    D --> E[尊重字节中的 LSB 到 MSB 的顺序]
    E --> F[去掉任何填充位]
    F --> G[重构位向量]
    G --> H[输出反序列化的位向量]

    classDef startEnd fill:#f9f,stroke:#333,stroke-width:4px;
    class A startEnd;
    classDef process fill:#ccf,stroke:#f66,stroke-width:2px;
    class B,C,D,E,F,G process;
    classDef output fill:#cfc,stroke:#393,stroke-width:2px;
    class H output;

图:SSZ 位向量的反序列化。

  1. 读取序列化字节数组:以作为输入的字节数组开始。

  2. 从字节提取位

    • 将每个字节转换回位。请注意,位在每个字节中以 LSB 优先存储。
    • 如果位向量的长度 N 不是 8 的倍数,则去除最后一个字节中的多余填充的位。
  3. 重建位向量

    • 重新组合提取的位形成原始的位向量格式,遵从特定的长度 N

示例: 给定序列化数据 B4 80 表示 Bitvector[10]

  • B4(二进制10110100)和 80(二进制10000000)转换回位。
  • 从位序列中提取前 10 位:1011010010
  • 重构的位向量为 1011010010

你可以像下面这样在 python 中运行并验证:

>>> from eth2spec.utils.ssz.ssz_typing import Bitvector
>>> Bitvector[8](0,0,1,0,1,1,0,1).encode_bytes().hex()
'b4'
>>> Bitvector[8](0,0,0,0,0,0,0,1).encode_bytes().hex()
'80'

实际上,我们可以使用Vector[boolean, N]Bitvector[N]来表示布尔值的列表。然而,后者在实际使用中序列化占用的字节上最大减少了八倍,因为前者将会对每个位一整字节。

>>> from eth2spec.utils.ssz.ssz_typing import Vector, Bitvector, boolean
>>> Bitvector[5](1,0,1,0,1).encode_bytes().hex()
'15'
>>> Vector[boolean,5](1,0,1,0,1).encode_bytes().hex()
'0100010001'

位列表

位列表在 SSZ 中类似于位向量,但它们旨在处理带有指定最大长度 (N) 的变长布尔值序列。

SSZ 位列表的序列化

flowchart TD
    A[开始序列化] --> B[定义大小为 N 的位列表]
    B --> C[将位打包为字节]
    C --> D[添加哨兵位]
    D --> E[如有必要对最后一个字节填充]
    E --> F[输出序列化字节数组]

    classDef startEnd fill:#f9f,stroke:#333,stroke-width:4px;
    class A startEnd;
    classDef process fill:#ccf,stroke:#f66,stroke-width:2px;
    class B,C,D,E process;
    classDef output fill:#cfc,stroke:#393,stroke-width:2px;
    class F output;

图:SSZ 位列表的序列化。

  1. 定义位列表:位列表通过其最大长度 N 定义,该长度指定可以包含的位的最大数量。实际的位数可以小于 N

  2. 已位打包为字节

    • 位列表中的每位表示布尔值,0 对应 False1 对应 True
    • 这些位被序列化为字节数组,从 LSB 到 MSB 的顺序进行打包,类似于位向量。
  3. 添加哨兵位

    • 为了标记位列表的结束和区分其实际长度与最大容量,最后添加一个哨兵位(1)。这一点对于确保反序列化过程准确识别位列表的长度很重要。
  4. 字节数组形成和填充

    • 加入哨兵位后,位被压入字节,其中在最后一个字节填充必要的填充位以确保总位数(包括哨兵)能够整除 8。

SSZ 位列表的反序列化

flowchart TD
    A[开始反序列化] --> B[接收序列化字节数组]
    B --> C[将字节转换为位]
    C --> D[识别并移除哨兵位]
    D --> E[去掉填充位]
    E --> F[重建原始位列表]
    F --> G[输出反序列化位列表]

    classDef startEnd fill:#f9f,stroke:#333,stroke-width:4px;
    class A startEnd;
    classDef process fill:#ccf,stroke:#f66,stroke-width:2px;
    class B,C,D,E,F process;
    classDef output fill:#cfc,stroke:#393,stroke-width:2px;
    class G output;

图:SSZ 位列表的反序列化。

  1. 接收序列化字节数组:开始时以包含哨兵位的字节数组为输入。

  2. 从字节提取位

    • 将每个字节转换为位,请注意在每个字节内按照 LSB 优先存储位。
    • 继续处理每个字节。
  3. 识别并移除哨兵位

    • 在提取的位序列中找到第一个 1(哨兵位)以确定位列表数据的实际结束。
    • 哨兵位之后的所有位被视为填充位。
  4. 重建位列表

    • 重新将提取的位(不包括哨兵位和任何填充位)组合成原始的位列表格式。

可以像下面这样运行位列表的编码:

>>> from eth2spec.utils.ssz.ssz_typing import Bitlist
>>> Bitlist[100](0,0,0).encode_bytes().hex()
'08'

由于哨兵对于数量的要求,如果其实际长度是 8 的整数倍,则我们需要一个额外的字节来序列化一个位列表(与最大长度 N 无关)。固定长度的位向量则不会这样。

>>> Bitlist[8](0,0,0,0,0,0,0,0).encode_bytes().hex()
'0001'
>>> Bitvector[8](0,0,0,0,0,0,0,0).encode_bytes().hex()
'00'

容器

在 SSZ 中,容器是将多个字段分组为单个复合类型的基本结构。容器内的每个字段可以是任何 SSZ 支持的类型,包括基本类型如 uint64、其他容器、向量或列表等复合类型。容器类似于编程语言中的结构或对象,使它们在以太坊中表示复杂和嵌套数据结构方面至关重要。

SSZ 容器的序列化

flowchart TD
    A[开始序列化] --> B[定义容器模式]
    B --> C[根据类型序列化每个字段]
    C --> D["序列化基本类型\n(uint64、布尔值等)"]
    C --> E["序列化复合类型\n(其他容器、列表、向量)"]
    D --> F[拼接字段的序列化输出]
    E --> F
    F --> G[输出序列化容器]

    classDef startEnd fill:#f9f,stroke:#333,stroke-width:4px;
    class A startEnd;
    classDef process fill:#ccf,stroke:#f66,stroke-width:2px;
    class B,C,D,E,F process;
    classDef output fill:#cfc,stroke:#393,stroke-width:2px;
    class G output;

图:SSZ 容器的序列化。

  1. 定义容器:SSZ 中的容器由其模式定义,模式指定字段的类型和顺序。该模式至关重要,因为它决定了如何序列化和反序列化数据。

  2. 序列化每个字段

    • 容器中的每个字段按模式定义的顺序进行序列化。
    • 每个字段的序列化方法取决于其类型:
      • 基本类型 直接转换为其字节表示。
      • 复合类型 (其他容器、列表、向量)按照各自的规则进行递归序列化。
  3. 拼接序列化字段

    • 所有字段的序列化输出被拼接在一起以形成容器的完整序列化数据。
    • 如果字段是变长的(例如,包含变长列表或向量),那么其序列化数据会根据实现细节包含长度前缀或使用偏移量指示数据的起始位置。

SSZ 容器的反序列化

flowchart TD
    A[开始反序列化] --> B[接收序列化的容器数据]
    B --> C[根据容器模式解析数据]
    C --> D[根据类型反序列化字段]
    D --> E[反序列化基本类型]
    D --> F[反序列化复合类型]
    E --> G[用反序列化的字段重建容器]
    F --> G
    G --> H[输出反序列化的容器]

    classDef startEnd fill:#f9f,stroke:#333,stroke-width:4px;
    class A startEnd;
    classDef process fill:#ccf,stroke:#f66,stroke-width:2px;
    class B,C,D,E,F,G process;
    classDef output fill:#cfc,stroke:#393,stroke-width:2px;
    class H output;

图:SSZ 容器的反序列化。

  1. 读取序列化数据:以表示容器的序列化字节流作为输入。

  2. 根据模式解析序列化数据

    • 根据容器的模式,将序列化数据解析为其组成字段。
    • 这要求了解每个字段的类型和大小,以正确地提取和反序列化每个字段。
  3. 反序列化每个字段

    • 每个字段的数据根据其类型反序列化。
    • 反序列化可能涉及将字节数组转换回整数、解码嵌套容器,或从其序列化形式重建列表和向量。
  4. 重建容器

    • 当每个字段反序列化完成后,将每个字段填回到其定义的位置。

示例

让我们通过以太坊 Beacon Chain 中的 IndexedAttestation 容器深入了解 SSZ 序列化和反序列化过程。此示例将概述如何处理复杂的嵌套容器,尤其是那些涉及固定大小和变长数据类型的容器。

IndexedAttestation 容器的结构如下:

class IndexedAttestation(Container):
    attesting_indices: List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE]
    data: AttestationData
    signature: BLSSignature

它包含一个 AttestationData 容器:

class AttestationData(Container):
    slot: Slot
    index: CommitteeIndex
    beacon_block_root: Root
    source: Checkpoint
    target: Checkpoint

Checkpoint 容器则有两个:

class Checkpoint(Container):
    epoch: Epoch
    root: Root

IndexedAttestation 容器结构

IndexedAttestation 容器包括多个字段,其中一些字段为固定大小的基本类型,另一些为复合类型,包括另一个容器(AttestationData)和列表(如 attesting_indices)。

结构如下:

  • attesting_indicesList[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE] (变长)
  • dataAttestationData (复合容器)
  • signatureBLSSignature (固定大小) ``- **插槽**:Slot`(固定大小)
  • 索引: CommitteeIndex(固定大小)
  • 信标区块根: Root(固定大小)
  • : Checkpoint(复合容器)
  • 目标: Checkpoint(复合容器)

检查点容器结构

  • 纪元: Epoch(固定大小)
  • : Root(固定大小)

序列化过程

  • 序列化固定和可变组件 IndexedAttestation 的序列化涉及根据其类型序列化每个组件:
  1. 序列化固定大小元素

    • 每个固定大小元素(SlotCommitteeIndexEpochRootBLSSignature)被序列化为其对应的字节格式,通常对数字类型使用小端格式。
  2. 序列化可变大小元素

    • List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE] 通过先记录列表的长度,再序列化每个索引的形式进行序列化。
    • 如果一个列表或其它可变大小的元素为空或未达到最大容量,它仅消耗实际存在的数据所需的空间,以及可能的一些长度或偏移元数据。
  • 连接序列化数据
    1. 所有序列化的字节按容器结构指定的顺序进行连接。固定大小的字段按顺序直接放置,而可变大小的字段可能作为序列化的一部分包含偏移或长度。

示例序列化输出

from eth2spec.utils.ssz.ssz_typing import *
from eth2spec.capella import mainnet
from eth2spec.capella.mainnet import *

attestation = IndexedAttestation(
    attesting_indices = [33652, 59750, 92360],
    data = AttestationData(
        slot = 3080829,
        index = 9,
        beacon_block_root = '0x4f4250c05956f5c2b87129cf7372f14dd576fc152543bf7042e963196b843fe6',
        source = Checkpoint (
            epoch = 96274,
            root = '0xd24639f2e661bc1adcbe7157280776cf76670fff0fee0691f146ab827f4f1ade'
        ),
        target = Checkpoint(
            epoch = 96275,
            root = '0x9bcd31881817ddeab686f878c8619d664e8bfa4f8948707cba5bc25c8d74915d'
        )
    ),
    signature = '0xaaf504503ff15ae86723c906b4b6bac91ad728e4431aea3be2e8e3acc888d8af'
                + '5dffbbcf53b234ea8e3fde67fbb09120027335ec63cf23f0213cc439e8d1b856'
                + 'c2ddfc1a78ed3326fb9b4fe333af4ad3702159dbf9caeb1a4633b752991ac437'
)

print(attestation.encode_bytes().hex())

表示该 IndexedAttestation 对象的序列化数据 blob 为(十六进制):

e40000007d022f000000000009000000000000004f4250c05956f5c2b87129cf7372f14dd576fc15
2543bf7042e963196b843fe61278010000000000d24639f2e661bc1adcbe7157280776cf76670fff
0fee0691f146ab827f4f1ade13780100000000009bcd31881817ddeab686f878c8619d664e8bfa4f
8948707cba5bc25c8d74915daaf504503ff15ae86723c906b4b6bac91ad728e4431aea3be2e8e3ac
c888d8af5dffbbcf53b234ea8e3fde67fbb09120027335ec63cf23f0213cc439e8d1b856c2ddfc1a
78ed3326fb9b4fe333af4ad3702159dbf9caeb1a4633b752991ac437748300000000000066e90000
00000000c868010000000000

序列化输出的分解

为了清晰地解释序列化过程和示例中 IndexedAttestation 容器的序列化数据结构,我们将序列化分解为其各个组成部分,并理解每个部分如何在字节流中表示。这种拆解有助于说明 SSZ 格式如何管理复杂的数据结构。

第1部分:固定大小元素

  1. 可变大小列表 (attesting_indices) 的 4 字节偏移

    • 字节偏移: 00
    • : e4000000
    • 解释: 这标识 attesting_indices 列表在序列化字节流中的开始位置。十六进制值 e4 转换为十进制是 228,意味着列表从字节 228 开始。
  2. 插槽 (uint64)

    • 字节偏移: 04
    • : 7d022f0000000000
    • 解释: 表示 slot 字段序列化为一个 64 位无符号整数。十六进制 7d022f00 在小端格式下转化为十进制的 3080829,即插槽编号。
  3. 委员会索引 (uint64)

    • 字节偏移: 0c
    • : 0900000000000000
    • 解释: 这是 index 字段,表示一个委员会索引,作为 64 位无符号整数。值 09 表示委员会索引 9
  4. 信标区块根 (Bytes32)

    • 字节偏移: 14
    • : 4f4250c05956f5c2b87129cf7372f14dd576fc152543bf7042e963196b843fe6
    • 解释: 这是一个以 Bytes32 存储的 256 位哈希,表示信标区块的根哈希。
  5. 源检查点纪元 (uint64) 和根 (Bytes32)

    • 纪元字节偏移: 34
    • 纪元值: 1278010000000000
    • 根字节偏移: 3c
    • 根值: d24639f2e661bc1adcbe7157280776cf76670fff0fee0691f146ab827f4f1ade
    • 解释: 源检查点包含一个 epoch (96274) 和一个 root。根是另一个 256 位哈希。
  6. 目标检查点纪元 (uint64) 和根 (Bytes32)

    • 纪元字节偏移: 5c
    • 纪元值: 1378010000000000
    • 根字节偏移: 64
    • 根值: 9bcd31881817ddeab686f878c8619d664e8bfa4f8948707cba5bc25c8d74915d
    • 解释: 与源相似,目标检查点包括一个 epoch (96275) 和一个 root,详细说明了之前的状态。
  7. 签名 (BLSSignature/Bytes96)

    • 字节偏移: 84
    • : 由于长度而跨多行连接(总共 96 字节)。
    • 解释: 这是对该证词的加密签名,以验证其真实性。

第2部分:可变大小元素

  1. 见证指标 (List[uint64, MAX_VALIDATORS_PER_COMMITTEE])
    • 字节偏移: e4
    • : 748300000000000066e9000000000000c868010000000000
    • 解释: 这表示为证明方块的验证者索引列表。它从偏移量 228 开始,并包含索引 3365259750,和 92360

资源

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

0 条评论

请先 登录 后评论
thogiti
thogiti
https://thogiti.github.io/