有效载荷分块
简而言之:将一个 EL 区块( =payload )拆分成多个小区块(“ chunk ”),每个区块的 Gas 预算固定(例如2**24 = 16.77M
),并以 Sidecar 的形式独立传播。每个小区块都承载着无状态执行所需的预状态,并提交到其后状态 diff。小区块按顺序排列,但可以完全独立地并行执行。CL 提交到一组区块头;Sidecar 则承载区块主体和包含证明。
验证变得更加连续。
动机
如今,区块已经是巨大的、单片化的对象,未来规模还会更大。验证需要先收到完整的区块才能开始执行。这会导致区块传播和执行的延迟瓶颈。
通过 P2P 网络接收The Block后,交易将按顺序执行。我们无法在下载时开始验证,也无法并行执行。
P2P 层上的消息通常使用 Snappy 进行压缩。以太坊上使用的 Snappy区块格式无法进行流式传输。因此,我们需要先将The Block切分成多个块,然后再进行压缩。
有了EIP-7928:块级访问列表,情况有所改善,但我们仍在等待下载完成才能开始区块验证。使用 4 个核心,我们得到了以下甘特图:
相反,我们可以将块作为块进行流式传输:
- 每个块包含≤2
2**24
gas的交易。- 也可以让块大小以 gas 为单位呈几何级数增长(
2**22
2**23
、……、2**25
)。这样可以调整块的延迟,从而实现更好的并行化——但我不确定这样做是否值得,因为这会增加复杂性。
- 也可以让块大小以 gas 为单位呈几何级数增长(
- 交易保持有序。区块被索引并排序,但彼此独立,因此可以并行验证。然而,区块 0 的后状态是区块 1 的前状态。
- (可选)每个块都带有无状态执行所需的状态。
这将验证从“下载完整区块,然后处理”转变为“在接收剩余部分的同时进行处理”。
执行层变化
我们扩展了 EL 块格式以支持分块:
class ELHeader :parent_hash: Hash32fee_recipient: Addressblock_number: uint64gas_limit: uint64timestamp: uint64extra_data: ByteList[MAX_EXTRA_DATA_BYTES]prev_randao: Bytes32base_fee_per_gas: uint256parent_beacon_block_root: Rootblob_gas_used: uint64excess_blob_gas: uint64transactions_root: Rootstate_root: Rootreceipts_root: Rootlogs_bloom: Bloomgas_used: Uintwithdrawals_root: Rootblock_access_list_hash: Bytes32 # New fields chunk_count: int # >= 0
EL 标头中对各个块没有任何承诺。我们只会将块计数添加到其中。执行输出( state_root
、 logs_bloom
、 receipts_root
、 gas_used
)必须与最后一个块中的值相同(适用于状态根和提款根),或者与汇总块值后的根相同(适用于交易、收据、日志、已用 gas 和The Block访问列表)。
执行块
区块永远不会被放到链上;只有它们的根被提交。
区块包含我们通常在 EL 区块主体中期望的字段。交易被拆分成多个区块,每个区块的 Gas 消耗上限为2**24
。提现操作只能包含在最后一个区块中。与区块级访问列表类似,每个区块都拥有自己的区块访问列表,并且还可以为区块添加预状态值,从而解锁无状态性。
class Chunk :header: ChunkHeadertransactions: List [Tx]withdrawals: List [Withdrawal] # only in chunk at index -1 chunk_access_list: List [ChunkAccessList]pre_state_values: List [(Key, Value)] # optional
每个块都带有一个包含块索引的头。交易按照chunk.header.index
及其在块中的索引进行排序。每个块的执行输出的承诺也包含在头中。
class ChunkHeader :index: int txs_root: Rootpost_state_root: Rootreceipts_root: Rootlogs_bloom: Bloomgas_used: uint64withdrawals_root: Rootchunk_access_list_root: Rootpre_state_values_root: Root # optional
为了防止提议者将其区块分割成过多的块,协议可以强制要求块必须至少半满( \geq\frac{chunk\_gas\_limit}{2} ≥ c h u n k _ g a s _ l i m i t 2 ) 或chunk.header.index == len(beaconBlock.chunk_roots)
( = 该块中的最后一个块)。
共识层变化
信标块使用新字段来跟踪块:
class BeaconBlockBody :...chunk_roots: List [ChunkRoot, MAX_CHUNKS_PER_BLOCK] # SSZ roots of chunks class ExecutionPayloadHeader :...chunk_count: int
CL 通过一个新的ChunkBundle
容器从 EL 接收执行块,该容器包含 EL 头和块(类似于 blob )。
CL 使用 SSZ 的hash_tree_root
计算块根并将其放入信标块主体中。
边车设计
块由边车运送:
class ExecutionChunkSidecar :index: uint64 # chunk index chunk: ByteList[MAX_CHUNK_SIZE] # Opaque chunk data signed_block_header: SignedBeaconBlockHeaderchunk_root_inclusion_proof: Vector[Bytes32, PROOF_DEPTH]
共识层确保所有块可用,并通过针对chunk_roots
(类似于 blob )的 Merkle 证明正确链接到信标块主体。
联网
提议者在普通的beacon_block
主题上仅传播具有承诺( chunk_count
、 chunk_headers_root
)的轻量级信标块,而重度执行数据则作为ExecutionChunkSidecar
分别在X 个并行子网( beacon_chunk_sidecar_{0..X}
)上传输,并按(block_root, index)
进行重复数据删除。
最初,所有节点必须订阅所有子网并托管所有区块。虽然这暂时不会降低带宽/存储需求,但它能够带来并行化的直接好处。一旦基本机制得到验证和/或零知识证明变得可行,可以在未来的升级中添加部分托管功能。
叉子选择
分叉选择要求所有 Sidecar 都可用且已成功验证,区块才会被视为有效。带有chunk_roots
的信标区块会快速传播,但只有当所有区块都已接收并针对根区块进行了包含验证后,The Block才有资格进行分叉选择。信标区块仍然包含 EL 区块头,其中包含所有必要的承诺(=对父区块的承诺和执行输出)。在本设计中,EL 上的区块主体部分保持为空。
好处
- 流式验证:当The Block的其他部分仍在下载或忙于从磁盘加载时,即可开始执行。区块之间是独立的(如果提供了预状态),或者依赖于区块访问列表(包含区块级状态差异)和区块预状态;多个 CPU/核心可以同时验证区块;带宽使用情况在时隙内分配,而不是在时隙开始时突发。
- 流线型证明:ZK 证明器可以同时并行证明多个块,从而受益于块的独立性。
- 无状态友好性:由于单个块小于一个区块,我们可以考虑添加预状态值,这样就无需访问本地状态。一个实用的折中方案是仅在块
0
中包含预状态值,以保证在节点将其他块所需的状态从磁盘加载到缓存时,始终至少有一个块可以执行。 - 未来的可扩展性:在块上集成 zk-proofs 或进行分片执行的明确路径。
设计空间
区块大小
2**24
gas(~16.7M)成为自然的块大小:
- 最大交易规模:截至 Fusaka(EIP-7825),
2**24
是最大可能交易规模。 - 当前区块: 45M gas 区块自然分成约 3 个区块,提供即时并行
- 未来区块:规模化发展——1 亿个 gas 区块将包含约 6 个区块
验证器
- 执行引擎在内部将The Block拆分为块(对 CL 不透明),并通过
ExecutionChunkBundle
将它们传递给 CL。 - 提议者将每个块封装在带有包含证明的 Sidecar 中。提议者还计算每个块的哈希树根,并将其放入信标块主体中。
- 所有子网中并行发布
- 证明者等待所有区块并验证它们后再进行投票
建造者
提议者可以在区块构建完成后发布它们,而验证者甚至可以在收到信标区块之前就开始验证它们。由于区块包含已签名的信标区块头及其包含证明,因此人们可以在区块提交时进行验证( =执行),并信任其来源( =提议者)。
未解决的问题和未来工作
渐进式块大小?
几何级数增加块大小( 2**22
2**23
、……、 2**25
)的想法看似有益,但会增加复杂性。第一个块可以较小(5M gas),并保留预状态值以便立即执行,而后面的块可以较大。这仍然是一个有待实验的领域。
部分监护路径
虽然初始实施需要完全托管,但该架构自然支持部分托管:
- 节点只能保管 X 个子网中的 Y 个
- 重建机制(类似于 DAS)可以恢复丢失的块
兼容 ePBS 和延迟执行
乍一看,提议的设计似乎与EIP-7732和EIP-7886兼容。在 ePBS 下,块根可能会移至ExecutionPayloadEnvelope
中,并且我们会在块根之上添加一个额外的根,并将其放入ExecutionPayloadHeader
中。PTC 不仅需要检查单个 EL 有效载荷是否可用,还需要检查所有块是否可用。这与 blob 并没有太大区别。
块分块和独立验证的优势在于可以随着更高的 gas 限制而扩展,并且可能有助于减少节点带宽消耗的峰值。