有效负载分块 - 分片

文章提出将以太坊执行层区块拆分为独立传播的固定 Gas 分块(Chunks),以实现流式下载和并行验证。该方案通过在共识层引入 Sidecars 机制,旨在降低区块传播延迟,优化带宽利用率,并为未来的 ZK 证明并行化和无状态性提供技术基础。

tl;dr: 将一个 EL 区块 ($= \text{payload}$) 拆分为多个具有固定 Gas 预算(例如 $2^{24} = 16.77\text{M}$)的迷你区块(“chunks”),这些小块作为 sidecar 独立传播。每个 chunk 都携带其无状态执行所需的预状态(pre-state),并提交其后置状态差异(post-state diff)。Chunk 是有序的,但可以完全独立地并行执行。CL 提交 chunk 头的集合;sidecar 携带主体和包含证明(inclusion proofs)。

验证过程变得更像是一种连续的流。

动机

目前的区块是大型的单体对象,未来还会变得更大。验证要求在执行开始前接收到完整的区块。这在区块传播和执行中造成了延迟瓶颈。

在通过 p2p 网络接收到区块后,交易是顺序执行的。我们无法在下载的同时开始验证,也无法并行执行。

显示当今区块验证瓶颈的时间线:先下载完整区块,然后顺序执行

p2p 层上的消息通常使用 Snappy 进行压缩。以太坊上使用的 Snappy 的 block-format 无法进行流式传输。因此,我们需要在压缩之前将区块切分成 chunk。

有了 EIP-7928: Block-level Access Lists,情况有所改善,但我们仍然需要等待下载完成后才能开始区块验证。使用 $4$ 个核心,我们会得到以下甘特图:

EIP-7928 下的时间线:执行可以使用访问列表,但仍必须等待区块下载完成

相反,我们可以将区块作为 chunk 进行流式传输

  • 每个 chunk 包含 $\le 2^{24}$ Gas 的交易。
    • 也可以让 chunk 的 Gas 大小按几何级数增加($2^{22}, 2^{23}, \dots, 2^{25}$)。这将为 chunk 提供不同的延迟,从而实现更好的并行化——但我不确定这是否值得增加复杂性。
  • 交易保持有序。Chunk 被索引且有序,但彼此独立,因此可以并行验证。尽管如此,chunk 0 的后置状态仍然是 chunk 1 的预状态。
  • (可选)每个 chunk 携带其执行所需的 statelessly(无状态)状态。

有效载荷分块后的时间线:在继续下载的同时,chunk 以流的形式进入并并行执行。

这使验证从“下载完整区块,然后处理”转变为“在接收剩余部分的同时进行处理”。


执行层变更

我们扩展 EL 区块格式以支持分块:

class ELHeader:
    parent_hash: Hash32
    fee_recipient: Address
    block_number: uint64
    gas_limit: uint64
    timestamp: uint64
    extra_data: ByteList[MAX_EXTRA_DATA_BYTES]
    prev_randao: Bytes32
    base_fee_per_gas: uint256
    parent_beacon_block_root: Root
    blob_gas_used: uint64
    excess_blob_gas: uint64
    transactions_root: Root
    state_root: Root
    receipts_root: Root
    logs_bloom: Bloom
    gas_used: Uint
    withdrawals_root: Root
    block_access_list_hash: Bytes32
    # 新字段
    chunk_count: int  # >= 0

EL 区块头中没有对单个 chunk 的承诺。我们只向其中添加了 chunk 计数。执行输出(state_rootlogs_bloomreceipts_rootgas_used)必须与最后一个 chunk 中的值相同(适用于状态根和提款根),或者是聚合 chunk 值后的根(适用于交易、收据、日志、已用 Gas 和区块访问列表)。

执行 Chunk

Chunk 永远不会上链;仅提交它们的根。

Chunk 包含我们通常在 EL 区块主体中预期的字段。交易被拆分到多个 chunk 中,每个 chunk 的限制为 $2^{24}$ Gas。提款必须仅包含在最后一个 chunk 中。镜像区块级访问列表,chunk 带有自己的 chunk 访问列表,并且还可以向 chunk 添加预状态值,从而实现无状态化。

class Chunk:
    header: ChunkHeader
    transactions: List[Tx]
    withdrawals: List[Withdrawal]  # 仅在索引为 -1 的 chunk 中
    chunk_access_list: List[ChunkAccessList]
    pre_state_values: List[(Key, Value)] # 可选

每个 chunk 都有一个包含 chunk 索引的头。交易按 chunk.header.index 及其在 chunk 中的索引排序。每个 chunk 执行输出的承诺都包含在头中。

class ChunkHeader:
    index: int
    txs_root: Root
    post_state_root: Root
    receipts_root: Root
    logs_bloom: Bloom
    gas_used: uint64
    withdrawals_root: Root
    chunk_access_list_root: Root
    pre_state_values_root: Root  # 可选

为了防止提议者将其区块拆分为过多的 chunk,协议可以强制要求 chunk 必须至少达到半满($\ge \frac{\text{chunk_gas_limit}}{2}$)或者满足 chunk.header.index == len(beaconBlock.chunk_roots)= 该区块中的最后一个 chunk)。


共识层变更

共识变更图示:信标区块跟踪 chunk 根,而执行 chunk 通过 sidecar 传播。

信标区块通过新字段跟踪 chunk:

class BeaconBlockBody:
    ...
    chunk_roots: List[ChunkRoot, MAX_CHUNKS_PER_BLOCK]  # chunk 的 SSZ 根

class ExecutionPayloadHeader:
    ...
    chunk_count: int

CL 通过一个新的 ChunkBundle 容器从 EL 接收执行 chunk,该容器包括 EL 区块头和 chunk(=类似于 blob)。

CL 使用 SSZ 的 hash_tree_root 计算 chunk 根,并将其放入信标区块主体中。

Sidecar 设计

Chunk 携带在 sidecar 中:

class ExecutionChunkSidecar:
    index: uint64  # chunk 索引
    chunk: ByteList[MAX_CHUNK_SIZE]  # 不透明的 chunk 数据
    signed_block_header: SignedBeaconBlockHeader
    chunk_root_inclusion_proof: Vector[Bytes32, PROOF_DEPTH]

共识层确保所有 chunk 都是可用的,并通过针对 chunk_roots 的 Merkle 证明正确地链接到信标区块主体(=类似于 blob)。

网络

提议者仅在正常的 beacon_block 主题上广播带有承诺(chunk_countchunk_headers_root)的轻量级信标区块,而庞大的执行数据则作为 ExecutionChunkSidecar 通过 $X$ 个并行子网beacon_chunk_sidecar_{0..X})分别流式传输,并通过 (block_root, index) 进行去重。

最初,所有节点必须订阅所有子网并托管所有 chunk。虽然这目前还不会减少带宽/存储要求,但它能立即带来并行化的好处。一旦基本机制得到验证且/或 zk 证明变得可行,就可以在未来的升级中添加部分托管。

网络视图:轻量级信标区块传播速度快,而庞大的执行 chunk 通过并行子网流式传输。

分叉选择 (Fork Choice)

分叉选择要求在区块被视为有效之前,所有 sidecar 都必须可用且成功通过验证。带有 chunk_roots 的信标区块传播速度很快,但只有在接收到每个 chunk 并针对根进行了包含证明后,该区块才具有分叉选择资格。信标区块仍然包含带有所有必要承诺的 EL 区块头(=提交到父区块和执行输出)。在 EL 上我们所熟知的“区块主体”在此设计中保持为空。


收益

  • 流式验证:执行可以在区块的其他部分仍在下载或正忙于从磁盘加载时开始。Chunk 是独立的(如果提供了预状态),或者依赖于 chunk 访问列表(带有 chunk 级的状态差异)和区块预状态;多个 CPU/核心可以同时验证 chunk;将带宽使用分散到 slot 中,而不是在 slot 开始时爆发。
  • 流线化证明:ZK 证明者可以并行化同时证明多个 chunk,从而受益于 chunk 的独立性。
  • 无状态友好性:由于单个 chunk 比一个区块小,我们可以考虑添加预状态值,从而无需本地状态访问。一个切合实际的折中方案是仅在 chunk 0 中包含预状态值,确保在节点从磁盘向缓存加载其他 chunk 所需的状态时,至少有一个 chunk 始终可以执行。
  • 未来扩展性:有清晰的路径来集成针对 chunk 的 zk 证明或进行分片执行。

设计空间

Chunk 大小

$2^{24}$ Gas($\approx 16.7\text{M}$)成为了一个自然的 chunk 大小:

  • 最大交易大小:截至 Fusaka (EIP-7825),$2^{24}$ 是可能的最大交易大小。
  • 当前区块:$45\text{M}$ Gas 的区块自然地拆分为约 $3$ 个 chunk,提供了即时的并行性
  • 未来区块:扩展性良好——$100\text{M}$ Gas 的区块将有约 $6$ 个 chunk

验证者

  1. 执行引擎在内部将区块拆分为 chunk(对 CL 不透明),并通过 ExecutionChunkBundle 将其传递给 CL。
  2. 提议者将每个 chunk 封装在带有包含证明的 sidecar 中。提议者还计算每个 chunk 的哈希树根(hash tree root),并将其放入信标区块主体中。
  3. 发布在所有子网上并行发生
  4. 证明者在投票前等待所有 chunk 并对其进行验证

构建者

构建者可以在完成构建 chunk 时立即发布,验证者甚至可以在收到信标区块之前就开始验证它们。由于 chunk 包含已签名的信标区块头和针对它的包含证明,人们可以在 chunk 到达时验证(=执行)它们,并信任它们的来源(=提议者)。

开放问题与未来工作

渐进式 Chunk 大小?

几何级数增加 chunk 大小($2^{22}, 2^{23}, \dots, 2^{25}$)的想法似乎有益,但增加了复杂性。第一个 chunk 可以较小($5\text{M}$ Gas)并带有预状态值以便立即执行,而后面的 chunk 较大。这仍是一个待实验的领域。

部分托管路径

虽然最初的实现要求完全托管,但该架构自然支持部分托管:

  • 节点可以仅托管 $X$ 个子网中的 $Y$ 个
  • 重建机制(类似于 DAS)可以恢复缺失的 chunk

兼容 ePBS 与延迟执行

初步看来,所提议的设计似乎与 EIP-7732EIP-7886 都兼容。在 ePBS 下,chunk 根可能会移动到 ExecutionPayloadEnvelope 中,我们会在 ExecutionPayloadHeader 中针对 chunk 根放置一个额外的根。PTC 不仅需要检查单个 EL 有效载荷是否可用,还需要检查所有 chunk 是否可用。这与 blob 没有太大区别。

区块分块和独立验证的优势随着 Gas 限制的提高而扩大,并可能有助于减少节点带宽消耗的峰值。

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

0 条评论

请先 登录 后评论
以太坊中文
以太坊中文
以太坊中文, 用中文传播以太坊的最新进展