本文档详细介绍了Solana区块链中用于在点对点网络中传播交易数据的分片包(Shred packets)的结构、协议和处理流程。内容涵盖数据分片的创建、不同版本的修订、数据布局、通用头部、数据头部、代码头部以及分片负载的构建过程,同时还包括纠删码、签名和Merkle证明等关键环节,为理解Solana网络中的数据传播机制提供了全面的技术细节。
分片数据包是区块(交易数据)的片段,有助于在点对点网络中传播。
数据分片通过拆分区块数据创建。 代码分片通过对分组到前向纠错 (FEC) 集合中的数据分片构建纠删码来创建。
所有分片都由区块生产者签名。
存在分片结构的多个修订版本。 分片修订版本未显式编码。
Solana 主网 beta 版在创世时启动,支持具有传统身份验证机制的代码和数据分片。
数据分片标头的重大更改,在偏移量 0x56
处添加了新的大小字段。
分片有效负载从偏移量 0x56
移动到 0x58
。
引入了两种具有 Merkle 身份验证方案的额外分片变体。
分片按顺序包含以下部分:
每个字段都按字节对齐。整数字节顺序为小端。
SHRED_SZ_MAX
常量定义为 1228。
如果使用传统身份验证,
则每个分片在序列化时占用 SHRED_SZ_MAX
字节。
如果使用 Merkle 身份验证,则编码分片占用 SHRED_SZ_MAX
字节,
数据分片占用 1203 字节。
这源于 IPv6 的最小链路 MTU 为 1280 字节,减去为 IPv6 和 UDP 标头保留的 48 字节。 另外保留 4 个字节用于可选的 nonce 字段。
通用标头的大小为 0x53
(83 字节)。
偏移量 | 大小 | 类型 | 名称 | 目的 |
---|---|---|---|---|
0x00 |
64B | Ed25519 签名 | signature |
区块生产者签名 |
0x40 |
1B | u8 |
variant |
分片变体 |
0x41 |
8B | u64 |
slot |
Slot 编号 |
0x49 |
4B | u32 |
shred_index |
分片索引 |
0x4d |
2B | u16 |
shred_version |
分片版本 |
0x4f |
4B | u32 |
fec_set_index |
FEC 集合索引 |
该签名验证了分片源自该 slot 的当前区块生产者。
区块生产者的公钥从外部获取。
用于创建签名的已签名消息的内容取决于所使用的身份验证方案:
在传统身份验证方案中,区块生产者对每个分片的内容进行签名。 要签名的消息从通用分片标头开始的 64 字节之后开始(即跳过签名字段),并跨越分片的其余部分,包括任何零填充。 生成的签名放置在 区块生产者签名 字段中。
在 Merkle 身份验证方案中,区块生产者对分片 FEC 集合的 Merkle 根进行签名。有关更多详细信息,请参见 Merkle 证明。 因此,签名字段对同一 FEC 集合中的所有分片都具有相同的内容。 Merkle 节点大小截断为 20 字节。
分片变体标识分片类型(数据、代码)和身份验证机制(传统、Merkle)。
该字段编码为两个 4 位无符号整数。
高 4 位字段位于位范围 4:8
。
低 4 位字段位于位范围 0:4
。
高 4 位 | 低 4 位 | 分片类型 | 身份验证 |
---|---|---|---|
0x5 |
0xa |
代码 | 传统 |
0xa |
0x5 |
数据 | 传统 |
0x4 |
任何 | 代码 | Merkle |
0x8 |
任何 | 数据 | Merkle |
使用 Merkle 身份验证时, 低 4 位表示 Merkle 树的高度。 此数字在下面定义为 $h$。
设置为此分片所属的区块的 slot 编号。
对于数据分片,设置为此分片在 slot 内的所有数据分片中的索引。 对于编码分片,设置为此分片在 slot 内的所有编码分片中的索引。
标识此分片所属区块的网络分叉。
设置为与此分片位于同一 FEC 集合中的第一个分片的分片索引。 所有具有相同 FEC 集合索引的分片都属于同一 FEC 集合。
数据分片标头
偏移量相对于通用标头的开始。
偏移量 | 大小 | 类型 | 名称 | 目的 |
---|---|---|---|---|
0x53 |
2B | u16 |
parent_offset |
到父区块的 Slot 距离 |
0x55 |
1B | u8 |
data_flags |
数据标志 |
0x56 |
2B | u16 |
size |
总大小 |
数据有效负载从通用标头的偏移量 0x58
处开始。
数据标志包含以下字段。(从 LSB 0 开始编号)
位数 | 类型 | 名称 | 目的 |
---|---|---|---|
7 |
bool | block_complete |
区块完成位 |
6 |
bool | batch_complete |
批处理完成位 |
0:6 |
u6 |
batch_tick |
批处理 tick 编号 |
如果此数据分片是此区块的最后一个分片,则设置为 1。 否则,设置为零。
区块中的最后一个分片也是其相应批处理中的最后一个分片, 因此,如果区块完成位设置为 1,则还必须将批处理完成位设置为 1。
如果此数据分片是此条目批处理的最后一个分片,则设置为 1。 否则,设置为零以指示下一个数据分片是同一条目批处理的一部分。
此数据包的大小,包括通用分片标头、数据分片标头和数据有效负载。 大小不包括零填充(如果有) 和 Merkle 证明(如果使用 Merkle 身份验证)。
修订版 v1 数据分片。
总大小字段假定为 1228 字节,因此未包含在数据包中。
偏移量 | 大小 | 类型 | 名称 | 目的 |
---|---|---|---|---|
0x53 |
2B | u16 |
parent_offset |
到父区块的 Slot 距离 |
0x55 |
1B | u8 |
data_flags |
数据标志 |
数据有效负载从通用标头的偏移量 0x56
处开始。
偏移量 | 大小 | 类型 | 名称 | 目的 |
---|---|---|---|---|
0x53 |
2B | u16 |
num_data_shreds |
数据分片数 |
0x55 |
2B | u16 |
num_coding_shreds |
编码分片数 |
0x57 |
2B | u16 |
position |
此分片在 FEC 集合中的位置 |
Reed-Solomon 编码有效负载从通用标头的偏移量 0x59
处开始。
设置为此数据包所属的 FEC 集合中的数据分片数。 集合中的每个编码分片在此字段中必须具有相同的值。
设置为此数据包所属的 FEC 集合中的编码分片数。 集合中的每个编码分片在此字段中必须具有相同的值。
标识此数据包包含哪个 Reed-Solomon 分片。 必须在范围 $[0, \texttt{num}\textunderscore\texttt{coding}\textunderscore\texttt{shreds})$ 内。 在 https://github.com/solana-labs/solana/pull/27136 之前,此字段不存在且设置为 0。 集合中的每个编码分片在此字段中必须具有唯一的值。
区块生产者在处于活动状态时创建并广播分片到验证器网络。
这作为一个流式处理过程发生,其中分片在最早的可能机会中构建。
分片的构造需要以下步骤:
批处理创建区块条目的子组,每个子组都序列化为一个字节数组。
批处理的序列化是所有序列化条目的连接,以条目计数作为 u64
整数(8 字节)作为前缀。
分片过程将序列化的条目批处理转换为数据分片向量。 在本节中,序列化的条目批处理被拆分为固定大小的块,这些块形成每个数据分片的有效负载, 并且计算相关的元数据。
首先,我们必须计算 $S$,即每个数据分片的有效负载的大小。
设 $\ell$ 是序列化条目批处理的字节长度,并且 $x_0, x1, \ldots, x{\ell-1}$ 是序列化条目批处理字节。
然后,第 $i$ 个数据分片的有效负载是 $x{iS}, x{iS+1}, \ldots, x{(i+1)S-1}$ 对于 $0\le i < \lfloor{\ell/S}\rfloor$。 如果 $\ell$ 不能被 $S$ 整除,则最终有效负载将 0 填充到 $S$ 字节,即 $x{\lfloor{\ell/S}\rfloor S}, x{\lfloor{\ell/S}\rfloor S+1}, \ldots, x{\ell-1}, \underbrace{0, 0, \; \ldots\;, 0}_{\ell-S\lfloor{\ell/S}\rfloor bytes}$。
区块中第一个批处理的第一个分片的分片索引为 0。 每个后续分片的分片索引比前一个分片的分片索引大 1。 后续批处理中的第一个分片的分片索引比前一个批处理中的最后一个分片的分片索引大 1。 也就是说,分片索引单调递增,而不会在每个批处理中重置。
每个批处理的最后一个数据分片都设置了“批处理完成”位。
可以使用数据标志位 0x40
提取此字段。
区块的最后一个数据分片设置了“区块完成”位。
可以使用数据标志位 0x80
提取此字段。
由于区块包含整数个条目批处理,
因此区块的最后一个数据分片也必须是批处理的最后一个数据分片。
批处理中所有分片的“批处理 tick 编号”设置为
自 slot 开始以来经过的 PoH tick 数
对于批处理中的第一个条目。
由于 Solana 每个 slot 有 64 个 tick,因此此字段不会溢出。
可以使用掩码 0x3f
的数据标志位字段提取此字段。
当“区块完成”标志设置为 1 时,“批处理 tick 编号”可以设置为 0。
分片过程(反分片)的逆过程从数据分片的流中重建序列化的批处理。
数据分片被分组在一起以形成前向纠错 (FEC) 集合。 区块生产者可以选择 $N$, 要包含在 FEC 集合中的连续数据分片的数量。 但是,FEC 集合必须至少有 1 个且不超过 67 个数据分片, 建议使用 $N=32$。
给定选择的 $N$ 值, 合规的区块生产者必须生成 $K$ 个由下表给出的代码分片:
$N$ | $K$ | 总分片数 ($N+K$) | $N$ | $K$ | 总分片数 ($N+K$) | ||
---|---|---|---|---|---|---|---|
1 | 17 | 18 | 17 | 26 | 43 | ||
2 | 18 | 20 | 18 | 27 | 45 | ||
3 | 19 | 22 | 19 | 27 | 46 | ||
4 | 19 | 23 | 20 | 28 | 48 | ||
5 | 20 | 25 | 21 | 28 | 49 | ||
6 | 21 | 27 | 22 | 29 | 51 | ||
7 | 21 | 28 | 23 | 29 | 52 | ||
8 | 22 | 30 | 24 | 29 | 53 | ||
9 | 23 | 32 | 25 | 30 | 55 | ||
10 | 23 | 33 | 26 | 30 | 56 | ||
11 | 24 | 35 | 27 | 31 | 58 | ||
12 | 24 | 36 | 28 | 31 | 59 | ||
13 | 25 | 38 | 29 | 31 | 60 | ||
14 | 25 | 39 | 30 | 32 | 62 | ||
15 | 26 | 41 | 31 | 32 | 63 | ||
16 | 26 | 42 | 32 | 32 | 64 |
对于 $N>32$,使用 $K=N$。
但是,合规的实现也可以接受具有不同数量的代码分片的 FEC 集合,只要 $1\le K, N \le 67$。
使用 Reed-Solomon 编码从数据分片生成代码分片。 使用传统身份验证时, 用于纠删码的“数据分片”的解释 从数据分片的通用标头的第一个字节开始, 并包括签名字段。 使用 Merkle 身份验证时, 用于纠删码的“数据分片”的解释在签名字段之后立即开始, 并在 Merkle 证明部分之前立即结束。
设 $x_{i,b}$ 为 FEC 集合的第 $i$ 个数据分片(编号为 $0, 1, \ldots, N-1$)的第 $b$ 个字节,解释为有限域 $GF(2^8)$ 的元素(即 $\mathbb{F}_2[\gamma] / (\gamma^8 + \gamma^4 + \gamma^3 + \gamma^2 + 1)$ )。
一次取一个 $b$,定义阶数小于 $N$ 的多项式 $P_b(x)$,使得对于所有 $0\le i < N$,$P_b(i) = x_i$(将 $i$ 的字节值解释为 $GF(2^8)$ 的元素)。 该多项式是唯一的。
然后,每个代码分片的第 $b$ 个字节来自评估 $Pb$ 作为后续点。 更准确地说,设 $y{j,b}$ 是 $0\le j < K$ 的第 $j$ 个代码分片的第 $b$ 个字节。 然后 $y_{j,b} = P_b(N+j)$,其中 $N+j$ 计算为整数,然后解释为 $GF(2^8)$ 的元素。
等效地,这是一个线性运算,因此它也可以描述为 $GF(2^8)$ 上的矩阵向量积:
$$ M \left( \begin{array}{c} x{0,b} \ x{1,b} \ \vdots \ x{N-1,b} \end{array} \right) = \left( \begin{array}{c} y{0,b} \ y{1,b} \ \vdots \ y{K-1,b} \end{array} \right).$$
矩阵 $M$ 仅取决于 $N$ 和 $K$。 有多种计算 $M$ 的方法,但一种描述是
$$ M = \left( \begin{array}{c} N^0 & N^1 & \cdots & N^{N-1} \ (N+1)^0 & (N+1)^1 & \cdots & (N+1)^{N-1} \ \vdots & \vdots & \ddots & \vdots \ (N+K-1)^0 & (N+K-1)^1 & \cdots & (N+K-1)^{N-1} \end{array} \right) * \left( \begin{array}{ccccc} 1 & 0 & 0 & \cdots & 0 \ 1^0 & 1^1 & 1^2 & \cdots & 1^{N-1} \ 2^0 & 2^1 & 2^2 & \cdots & 2^{N-1} \ \vdots & \vdots & \vdots & \ddots & \vdots \ (N-1)^0 & (N-1)^1 & (N-1)^2 & \cdots & (N-1)^{N-1} \end{array} \right)^{-1} $$
其中基数和指数计算是整数算术, 但是指数运算、矩阵逆和矩阵乘法是有限域运算。 也就是说,$M$ 是 Vandermonde 矩阵的一部分与另一个 Vandermonde 矩阵的逆的乘积。
当分片过程产生的数据 没有填充有效负载字段时, 必须在分片数据之后插入额外的零字节。 这确保了所有数据分片都具有相同的长度。
Reed-Solomon 编码过程自然会生成相同大小的编码分片, 因此编码分片不需要零填充。
当使用传统身份验证方法时, 区块生产者使用数据包中签名字段后面的字节的 Ed25519 签名填充数据分片和代码分片的签名字段, 包括任何零填充。
使用 Merkle 身份验证方法时,
区块生产者从 FEC 集合中的每个分片构建 规范 Merkle 树,
数据分片按顺序排列,然后是依次排列的编码分片。
两种分片类型的叶节点都是字节
从签名字段之后立即开始
到 Merkle 证明部分之前立即结束。
所有哈希都截断为 20 字节,
并立即丢弃每个 SHA256 哈希的最后 12 个字节。
叶节点使用 \x00SOLANA_MERKLE_SHREDS_LEAF
的前缀,内部节点使用 \x01SOLANA_MERKLE_SHREDS_NODE
的前缀。
两个前缀都是 26 字节,而不是 \0
终止的。
区块生产者计算 Merkle 树的根的 Ed25519 签名 并将签名存储在通用标头的签名字段中。 由于 FEC 集合中的所有数据包都是同一 Merkle 树的一部分 因此具有相同的 Merkle 根, 因此,在此方案中,同一 FEC 集合中的所有分片(代码和数据)都具有相同的签名。
使用 Merkle 身份验证时, 数据包的最后一个字节包含 Merkle 证明,证明有效负载属于签名字段覆盖的 Merkle 树。
设 $h=\lceil \log_2 (N+K) \rceil$ 是 FEC 集合的 Merkle 树的高度 (包括叶节点,但不包括根)。 Merkle 证明部分由以下内容组成:
偏移量 | 大小 | 类型 | 描述 |
---|---|---|---|
end- $20h$ | 20B | 截断的 Merkle 哈希 | 兄弟叶节点的 Merkle 哈希 |
end- $20h$ + 20 | 20B | 截断的 Merkle 哈希 | 叶节点的父节点的兄弟节点的 Merkle 哈希 |
... | ... | ... | ... |
end-20 | 20B | 截断的 Merkle 哈希 | 根的子节点的 Merkle 哈希 |
Merkle 证明包含计算从数据包中的叶到根的完整分支所需的其他信息。
例如,在规范 Merkle 树图 2中,
L0
的证明包含哈希 L1
(兄弟叶节点),
Iβ
和 Iε
。
它不包括 Iα
、Iδ
或 Iζ
,因为这些可以从包含的信息中计算出来。
L3
的证明包含 L2
、Iα
和 Iε
。
如前所述,两种分片类型的叶节点都是字节 从签名字段之后立即开始 到 Merkle 证明部分之前立即结束。
合规的实现必须验证 该签名是根哈希的有效签名 并且 Merkle 树在 FEC 集合中的所有分片中是一致的。
- 原文链接: github.com/solana-founda...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!