本文探讨了在以太坊Fusaka分叉后,如何通过改进列的传播来提高blob吞吐量。文章提出了几种基于纠删码(如RS和RLNC)和cell为中心的gossip协议,以优化列的传播效率。不同的方案在编码方式和gossip验证机制上有所差异,旨在实现更高效的网络传输和数据重建,同时考虑了计算成本、带宽效率和安全性。
Fusaka 分叉将引入基于 1D 列的数据可用性采样,其中每个样本都是一个由来自所有 blob 的单元格和证明组成的列。在 Fusaka 之后,改进列的传播是进一步提高 blob 吞吐量的主要途径之一,到目前为止,为此提出的工具有从基于推送的 gossip 转向声明和拉取/推送混合,并引入某种形式的编码,如 RS 或 RLNC。在这篇文章中,我们探讨了多种方法来实现后者,同时也使单个单元格成为传播的基本单元。感谢 @asn, @dankrad, @potuz, @MarcoPolo , @vbuterin 的讨论和反馈。
以单元格为中心的 gossip****。使单元格成为传播的单元:
请注意,2. 是一个相当强的约束。如果我们只想改进 CL 传播路径,而不考虑与 EL mempool 和分布式 blob 发布的交互,这将大大简化某些设计,因为我们可以使用更少、更大的块。但是,以下探索将此约束视为既定事实。
在文档的其余部分,除非另有说明,否则我们默认假设总共有 256 列和 128 个 blob。
我们探索的设计主要在两个维度上有所不同:
编码方案: 如何引入冗余。我们始终将单元格视为编码方案的基本单元,并考虑两种可能性,这两种可能性都包括列中单元格的线性组合:
Gossip 验证机制: 节点如何确保在重建之前,在 gossip 期间接收和转发的块的完整性和真实性。例如,我们可以将 DataColumnSidecar
分解成块并对其进行编码,但只有在重建整个消息后才能验证它,而我们需要每个块都是可独立验证的。我们通过以下方式之一确保可验证性:
从总体上看,我们通过 RS 编码(与水平扩展使用的相同)垂直扩展列,并 gossip 单个单元格。我们考虑两种方式,它们在确保传输块的完整性的方式上有所不同:
第二种选择对发布者来说计算成本要低得多,因为发布者不需要进行扩展证明的昂贵计算。
每个 DataColumnSidecar
子网都替换为一个 CellSidecar
子网,其中所有对应于_垂直扩展_列的单元格都进行 gossip。此外,我们有一个单一的、全局的 HeaderSidecar
子网,用于 gossip 带有所有 kzg 承诺(以及包含证明)的签名头。HeaderSidecar
主题将具有高度的网格,例如 30,以确保它尽可能快地传播。我们可以在这里接受高重复因子,因为消息只有几 KB(大部分大小是由于 kzg_commitments
,对于 128 个 blob 约为 6 KB)
class CellSidecar(Container)
cell: Cell
row_index: RowIndex
column_index: ColumnIndex
kzg_proof: KZGProof
class HeaderSidecar(Container):
signed_block_header: SignedBeaconBlockHeader
kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK]
kzg_commitments_inclusion_proof: Vector[Bytes32, KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH]
cell_sidecar_{subnet_id}
subnet_id
与 compute_subnet_for_cell_sidecar(column_index)
不匹配,则拒绝CellSidecar
验证暂停,直到收到 BeaconBlock
或 HeaderSidecar
。row_index
的 kzg_commitment
,如果 kzg 证明验证失败,则拒绝在等待匹配的 BeaconBlock
或 HeaderSidecar
时,既不能进行消息传递也不能进行转发。因此,每当对等方发送我们无法验证的单元格时,我们会向他们请求 HeaderSidecar
(这些非常小,我们可以限制发送的并行请求数量)。我们还可以降低在超时后不响应此请求的对等方的分数,以惩罚发送不可验证的单元格。
收到 HeaderSidecar
后,可以灵活地安排所有 kzg 证明验证,以最大限度地利用批量验证的优势。
header_sidecar
block_header.parent_root
的祖先,则拒绝(block_header.slot, block_header.proposer_index, sidecar.index)
的第一个边车,则忽略block_header.parent_root
定义的当前混洗的上下文中 block_header.slot
的预期提议者,则拒绝。HeaderSidecar
仅包含原始单元格的 kzg 承诺。验证扩展单元格需要我们插入原始承诺,即计算 C' = M_{RS} \cdot C 其中 C = (C_1, \dots, C_{128}),原始承诺,而 M_{RS} 是 RS 矩阵。这需要 128 个(每个承诺一个)G1 中的 128 点 MSM,大约 ~130 毫秒多线程(在 zka.lc 上每个 128 点 MSM 约 1 毫秒)。请注意,该计算每个节点只需要完成一次,即在首次收到 HeaderSidecar
时。
原则上,我们可以避免在每个节点上重复计算。HeaderSidecar
中的 kzg_commitments
可以包含_所有承诺,包括扩展承诺。需要验证这些承诺与原始承诺的一致性,但这可以通过选择随机标量向量 r 并检查 r^TC' = (r^T M\{RS}) \cdot C 来更快地完成,而不是重新计算。这只需要两个 G1 中的 128 点 MSM,约 2 毫秒。当然,权衡在于带宽,因为这将意味着 HeaderSidecar
中另外 ~6 KB。
最昂贵的操作是插值原始证明以计算扩展证明。对于每列,这与我们讨论的 KZG 承诺扩展的操作完全相同,即 128 个(每个证明一个)G1 中的 128 点 MSM,以计算 \pi_j' = M_{RS} \cdot \pi_j 其中 \pi_j = (\pi_{1j}, \dots, \pi_{128j}),列 j的原始证明向量。提议者必须_在关键路径中为每一列_执行此操作,或者换句话说,它必须_为每个证明执行一个 128 点 MSM。这似乎非常昂贵,因为它必须计算 128 \times 256 = 32768 个证明。使用来自 zka.lc 的数字,即使是多线程,也需要 ~34 秒。通过用 2 个 FFT 替换与 M\{RS} 的乘法可以进一步加速 ~3 倍,但这仍然使我们远离可接受的成本。此外请注意,即使我们放弃将 CellSidecar
放入单个数据包的想法,我们也不能低于 128 列,这只会使成本减半。
在发布步骤中消除如此高的计算成本正是原始 blobs 的证明计算已通过将其外包给 tx 发送者而从关键路径中移出的原因。另一方面,垂直扩展的证明计算既不能外包,也不能像我们从 mempool 获取 blobs 那样预先计算,因为我们需要所有将进入块的 blobs 才能执行此操作。
让我们考虑一种不同的方法,通过使用一种更简单的方式来确保单元格的完整性,从而避免了计算扩展证明的高成本。纠删码仅限于单元格,因此 gossip 验证不需要 KZG 证明验证(甚至对于原始单元格也不需要)。相反,它依赖于针对 cell_roots
列表进行检查,该列表作为 ColumnSidecar
的一部分在同一主题中单独发送。反过来,这是针对 HeaderSidecar
中包含的列边车根列表进行验证,并在块中提交。
## 列主题
## 没有 kzg 证明
class CellSidecar(Container)
cell: Cell
row_index: RowIndex
column_index: ColumnIndex
## 具有所有单元格承诺并携带证明的新边车
class ColumnSidecar(Container):
column_index: ColumnIndex
cell_roots: List[Root, 2 * MAX_BLOB_COMMITMENTS_PER_BLOCK]
kzg_proofs: List[KZGProof, MAX_BLOB_COMMITMENTS_PER_BLOCK]
## 全局主题
## 添加对列边车的承诺
class HeaderSidecar(Container):
signed_block_header: SignedBeaconBlockHeader
column_sidecar_roots: List[Root, NUMBER_OF_COLUMNS]
column_sidecar_roots_inclusion_proof: Vector[Bytes32, COLUMN_SIDECAR_ROOTS_INCLUSION_PROOF_DEPTH]
kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK]
kzg_commitments_inclusion_proof: Vector[Bytes32, KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH]
class BeaconBlockBody(Container):
...
# 需要一个更好的名字
column_sidecar_roots_root: Root
与之前一样,我们在全局主题中传播 HeaderSidecar
。除了之前的内容外,它现在还包含一个根列表 column_sidecar_roots
,以及针对 header.body_root
的包含证明 column_sidecar_roots_inclusion_proof
。BeaconBlockBody
本身仅包含对 column_sidecar_roots
的承诺,即 column_sidecar_roots_root = hash_tree_root(column_sidecar_roots)
。
在给定列的子网中,我们传播:
column_sidecar
,包含 cell_roots
,提交到给定列的所有单元格,包括扩展单元格的单元格,并且还包含所有 kzg_proofs
。对于 gossip 验证,我们检查 hash_tree_root(column_sidecar) == header_sidecar.column_sidecar_roots[column_sidecar.column_index]
。请注意,kzg_proofs
_未_作为 gossip 验证的一部分进行验证。cell_sidecar
。对于 gossip 验证,我们检查 column_sidecar.cell_roots[cell_sidecar.row_index] == hash_tree_root(cell_sidecar.cell)
。一些注意事项:
cell_roots
验证单元格边车(本身又针对 column_sidecar_roots
验证),因此我们不需要验证 kzg_proof
作为转发的一部分,因此 kzg 验证不会减慢传播速度。ColumnSidecar
中,并且可以在收到相应的单元格后立即进行验证。由于 kzg 证明验证与转发分离,因此可以在最大限度地利用批处理的同时完成此操作。getBlobs
的交互是理想的:当我们从 mempool 检索 blob 时,我们会为每列获得一个完整的 CellSidecar
,我们可以立即传播在 RS + KZG 案例以及 Fusaka 协议中,_以某种方式_知道与 blob txs 列表关联的 blobs 可用,但没有下载它们的提议者只需包含这些 txs,以及相应的 KZG 承诺,并完全依赖分布式 blob 发布来在 CL 上传播实际的 blob 数据(无论是单元格还是数据列边车)。例如,如果一个委员会证明了 mempool txs 的可用性,情况可能就是这样。
另一方面,此协议不允许提议者这样做,因为为了创建 column_sidecar
,需要具有单元格(或更准确地说,cell_roots
)和 kzg_proofs
,而这些对于cell_sidecar
传播的发生是必需的。请注意,column_sidecar
也必须在块中提交,因此提议者确实需要在块创建时拥有它们。
在这里,我们考虑两种方法来采用这个提案来实现我们列传播的目标。在这两种情况下,我们都使用单元格级别的块,主要的区别在于我们如何在传播期间验证块:一种选择是重用 KZG 证明,另一种选择是坚持使用 Pedersen 承诺,如帖子中所述。
我们可以利用 KZG 承诺和证明的线性性质来为列传播启用 RLNC,同时保持与原始 KZG 行承诺相关的重组块的可验证性:
RLNC 编码(重新编码): 拥有列 j 的一组输入块 (v_k, \pi_k, s_k)_k 的节点(其中每个 s_k = (s_{ki})_i 因此 v_k = \sum_i s_{ki} v_{ij} 并且 \pi_k = \sum_i s_{ki} \pi_{ij}) 可以创建一个新的编码块。它选择一组随机标量 (r_k)_k 并计算:
CellSidecar
,其中 cell
保持 v,kzg_proof
保持 \pi,新的 rlnc_scalars
字段保持 s。保留 column_index
,但 row_index
被 rlnc_scalars
替换,因为数据包表示行的组合。
class CellSidecar(Container):
cell: Cell
column_index: ColumnIndex
kzg_proof: KZGProof
rlnc_scalars: List[Bytes32, MAX_BLOB_COMMITMENTS_PER_BLOCK]
HeaderSidecar
中获得原始行承诺 C_i(与 RS + KZG 方法相同)。column_index
)的评估点,针对编码数据 v 和组合承诺 C 验证 KZG 证明 \pi。RLNC 的一个问题是传输标量向量 s = (s_i)_i(rlnc_scalars)
的开销。让 N 为基本块的数量(每列的原始行/单元格,例如 128)。
我们还可以尝试不同的策略来减少膨胀,例如:
只有发布者重新编码,以便具有初始冗余,但始终保持相同的 1 字节标量。换句话说,我们将纯粹将随机线性编码用作纠删码的替代品,唯一的优点是我们可以权衡 RS 编码的 FFT 优化及其更强的保证(知道任何 50% 的块_保证_能够重建)以使用更小的标量。特别是,我们得到:
HeaderSidecar
,就可以只计算一次组合承诺。此外,由于代码是提前知道的,我们不需要在消息中包含标量,这避免了每个单元格 128 B 的开销。接收者: 要验证收到的数据包 (v, \pi, s),其中 s 具有 m 个非零条目,大部分成本是:
请注意,即使我们使用 1 字节标量,这里的 s_i 也不是新生成的标量,因此会受到膨胀的影响,而发送者完成的证明的线性组合直接使用新生成的小标量,因此始终保持相同的加速。但是,即使有一些膨胀,配对也将在此处占据主导地位。
使用后一种策略,提议者的成本与 RS + KZG 中的成本相同,这很有意义,因为我们本质上也在做一个 1/2 速率的块纠删码,但代码是随机的。与 RS + KZG 不同,我们甚至无法从 FFT 中受益,因为代码不是以这种方式结构的。但是,如果我们使用 1 字节标量而不是完整(32 字节)标量,则会有一个很大的优势,因为这应该会导致 MSM 大约加速 ~32 倍,从而使成本降至 ~1 秒。
现在让我们讨论来自 RLNC ethresearch 帖子的方法,该方法使用 Pedersen 承诺来保证重新编码块的完整性。以下很多内容都是对帖子中的内容的重新陈述,针对传播列的特定情况,并推理出具体的开销。
cell_roots
一样,分布在 ColumnSidecar
中,该 ColumnSidecar
本身在块中提交。
class ColumnSidecar(Container):
column_index: ColumnIndex
pedersen_commitments: List[PedersenCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK]
kzg_proofs: List[KZGProof, MAX_BLOB_COMMITMENTS_PER_BLOCK]
RLNC 编码(重新编码): 一个节点接收输入块 (v_k, s_k)_k(其中 s_k = (s_{ki})_i 定义 v_k = \sum_i s_{ki} v_{ij})。它选择随机标量 (r_k)_k,并计算:
class CellSidecar(Container):
cell: Cell
column_index: ColumnIndex
rlnc_scalars: List[Bytes32, MAX_BLOB_COMMITMENTS_PER_BLOCK]
验证: 节点接收 (v, s')。
ColumnSidecar
中获取列 j 的原始基本 Pedersen 承诺 C_{ij}。几个评论:
让我们简要讨论一下这类设计的优点,而不关注由于 RLNC 与 RS 等等造成的任何差异。
要发布 blob 数据,提议者/构建者可以执行以下操作:
HeaderSidecar
这只需要提议者发送原始数据的 4 倍(由于 DAS 的水平扩展,原始数据的 2 倍,然后传播中的冗余,又增加了 2 倍),而不是当前的 16 倍(同样的,由于 blobs 的水平扩展,原始数据的 2 倍,然后每列发送给 8 个对等方)。
请注意,发布步骤不能仅通过从推送转换为声明和拉取来改进,因为来自发布者的所有消息都将被拉取。一个更有趣的比较将是与当前系统进行比较,通过推送发送完整的列,并通过 批量发布 来增强,即优先在每个子网中发布每列的第一个副本。即使这样,为每列至少提供一些冗余也需要发布每列的至少 2 个副本,因此发送 4 倍的数据,就像使用纠删码一样。以相同的带宽成本,对列进行纠删码更加健壮,并且更不容易被一些故障减慢速度。
总的来说,纠删码使我们能够拥有高水平的冗余和高分支因子( | 方法 | 发布者计算成本 | 每跳工作量 | 每个单元格的额外字节 |
---|---|---|---|---|
RS + KZG | 非常高 ≈ 34s,RS 编码 proofs (32 768 × 128-pt G1 MSMs 或 512 G1 FFTs) | 发送:仅转发 <br> 接收:≈ 1.5ms,验证 KZG proof (2 pairings) | 低: +48 B (KZG proof), ~5% | |
带有 1 字节标量的随机线性码 + KZG | 中等 ≈ 1s (32× 更快的 MSMs) | 发送:仅转发 <br> 接收:≈ 1.5ms,验证 KZG proof (2 pairings) | 低: +48 B (KZG proof), ~5% (固定码,不传输标量) | |
RS + 单元格根 | 低 (RS 编码单元格,无 group MSMs) | 可忽略不计 | 低: 在单元格 sidecar 中: 0 <br> 带有列 sidecar 的总计: ~11% | |
RLNC + KZG | 非常高 ≈ 68s (65 536 × 128-pt G1 MSMs) | 发送:128-pt G1 MSM (≈ 1ms) <br> 接收:128-pt G1 MSM + 2 pairings (≈ 2.5ms) | 非常高: +4.1 kB (标量向量) +48 B (proof), ~4x 开销 | |
RLNC + KZG (1 字节标量) | 中等 ≈ 1s (32× 更快的 MSMs) | 发送:低 (带有 1 字节标量的 MSM) <br> 接收:即使有标量膨胀,成本主要由 2 pairings 主导 (≈ 2.5ms) | 高: 最后一个跃点 +640 B,高达 ~60% | |
RLNC + Pedersen (1 字节标量) | 非常高 ≈ 22s (curve25519 中的 32 768 × 32-pt MSMs) | 发送:可忽略不计 <br> 接收:≈ 3ms curve25519 中的一个 128-pt + 一个 32-pt MSM | 高 最后一个跃点 +640 B,高达 ~60% | |
RLNC + Pedersen (1 字节标量,txs 包括 Pedersen 承诺) | 低 (无 group MSMs) | 发送:可忽略不计 <br> 接收:≈ 3ms curve25519 中的一个 128-pt + 一个 32-pt MSM | 高 最后一个跃点 +640 B,高达 ~60% |
KZG 承诺和 proofs 具有非常有用的线性性质,我们在 RS + KZG 和 RLNC + KZG 中都使用了它,以便为扩展/重编码单元格生成 proof:
考虑评估点 (x_j)_j 和 (值, 承诺, proof) 三元组 ((y_{ij})_j, C_i, \pi_i),证明对于所有 j,p_i(x_j) = y_{ij},对于多项式 p_i 使得 [p_i(s)] = C_i。给定标量 (r_i)_i,三元组 ((y_j)_j, C, \pi) = ((\sum_i r_i y_{ij})_j, \sum_i r_i C_i, \sum_i r_i \pi_i) 使得 C = [p(s)] 是对多项式 p(x) = \sum_i r_i p_i(x) 的承诺,并且 \pi 是一个有效的 proof,证明对于所有 j,p(x_j) = y_j。
对于感兴趣的读者,这里有一个简短的解释,主要基于 这个很棒的参考:
> 令 I_i(x) = \sum_j y_{ij} \prod_{k\neq j} \frac{x-x_k}{x_j - x_k} 为 (x_j, y_{ij})_j 的插值多项式,Z(x) = \prod_j{(x - x_j)} 为 (x_j)_j 上的消失多项式。p_i(x) 在 (x_j)_j 的 proof 为 \pi_i = [q_i(s)],其中 q_i(x) = \frac{p(x) - I_i(x)}{Z(x)}。
>
> I(x) = \sum_i r_i I_i(x) 是 p(x) = \sum_i r_i p_i(x) 在 (x_j, y_{ij})_j 上的插值多项式,因为 I(x_j) = \sum_i r_i I_i(x_j) = \sum_i r_i p_i(x_j) = p(x_j)。
>
> 那么,生成 p(x) 在 (x_j)_j 的 proof 的商多项式为 q(x) = \frac{p(x) - I(x)}{Z(x)} = \frac{\sum_i r_i p_i(x) - \sum_i r_i I_i(x)}{Z(x)} = \sum_i r_i\frac{p_i(x) - I_i(x)}{Z(x)} = \sum_i r_i q_i(x)。proof 为 [q(x)] = [\sum_i r_iq_i(x)] = \sum_i r_i[q_i(x)] = \sum_i r_i\pi_i。
乍一看,单元格 sidecars 似乎很危险,因为它们可以是完全任意的数据片段,不与任何区块或 slot 或提议者相关联。但是,首先请注意,恶意节点不会获得将其 mesh peers 之外传播坏单元格(未与任何区块关联)的能力,因为转发需要等待 HeaderSidecar
,并且可能还需要 ColumnSidecar
,具体取决于哪个设计,并执行验证以证明该单元格确实与区块相关联。恶意节点可以做的只是向 mesh peer 发送一个坏的单元格 sidecar。
似乎即使这种较弱形式的恶意行为也不可能通过 DataColumnSidecar
实现,至少在不被降分的情况下,因为列 sidecar 包含与 HeaderSidecar
等效的内容,因此可以独立验证。事实上,情况并非如此:列 sidecar 的验证取决于对其包含的已签名信标区块头的验证,而这又依赖于其链:如果你不知道标头所属链的 shuffle,则无法验证它。
具体来说,peer 总是可以向你发送带有有效签名但编造的父根的标头,这样你就永远无法检索其链并确定提议者是否是正确的。因此,你也不能确定它是无效的,并且任何降分最多都必须取决于时间,例如,如果消息长时间无法验证,则降分,因为你无法检索其依赖项。这与节点接收到单元格 sidecar 且无法检索相应的 HeaderSidecar
并且可能无法检索 ColumnSidecar
的情况完全相同。
- 原文链接: ethresear.ch/t/improving...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!