文章探讨了以太坊语义区块分块(Semantic Block Chunking)的架构方案,提议将区块重构为多个独立可验证的语义单元。通过引入独立的区块访问列表(CAL)来传输状态差异,该方案能够实现流式验证和并行执行,旨在应对区块 Gas 限制提升带来的传播延迟和验证压力。
随着区块 Gas 限制(Block gas limits)的增加,以太坊区块的大小不可避免地会增长。我们不能依赖被压缩、传播然后再执行的单体区块(Monolithic blocks)。在某些时刻,我们需要引入区块分块(Block chunking),以减少延迟、平滑传播,并在关键路径上减轻验证者(Validators)的负担。
在之前的一篇文章中,我描述了语义化 Payload 分块及其背后的动力。现在,我想进一步深化这个想法:真正的进步来自于将语义分块(Semantic chunks)视为一等对象(First-class objects),并更进一步,将分块访问列表(Chunk Access Lists, CALs)从分块本身中分离出来。
关于区块分块,有几种截然不同的思考方式:
(1) 在基础层面,可以简单地将 RLP 序列化的区块视为字节流,并将其切割成固定大小的切片。这些碎片易于传播,且非常适合传输协议,但它们没有任何意义:在整个区块重新组装完成之前,验证者无法对其进行任何操作。这种非语义化分块 (non-semantic chunking) 有助于带宽平滑和数据可用性,但并不能改善执行延迟。

(2) 结构上的进步是沿其自然模式边界拆分区块。在这里,Header、交易列表、提款(Withdrawals)、收据(Receipts)和执行输出都将作为独立对象进行传播。这些部分对应于协议定义的字段,并且允许(例如)在获取完整主体之前部分获取 Header,或优先处理证明(Attestations)。然而,这些组件通常只有在一起时才有意义:执行仍然需要完整的 Payload,因此对验证者延迟的改善仍然有限。

(3) 更具雄心的版本是重新定义区块本身,将其视为一系列自包含的 分块 (chunks)。每个分块都携带自己的 Header、交易和后状态根(Post-state root),并且作为一个对象是可以独立验证的。通过将一个分块的后状态连接到下一个分块的前状态(Pre-state),分块链构成了规范区块。这超越了分解,变成了组合式语义分块 (compositional semantic chunking):分块不仅仅是碎片,而是协议的新语义单元。验证者可以在第一个分块到达时立即开始执行,而无需等待完整区块到达。这开启了真正的流式验证(Streaming validation),并为每分块证明(Per-chunk proofs)和并行证明铺平了道路。

值得注意的是,这些方法并不一定相互排斥:我们可以采用 (3),同时将交易、提款、执行输出等从分块中分离出来,并将它们与 (2) 的语义分块方法相结合。此外,分块也可以在序列化之后、压缩之前进行拆分,如 (1) 所述。
我们不再将区块视为单一的执行 Payload,而是将其重构为多个分块。我们今天所熟知的区块在概念上被重新构建,现在仅由一个 Header 和若干分块组成。Header 承诺了一个 Slot 中所有分块的执行,包括最后一个分块的后状态根。
在 CL 上,每个 ExecutionChunk 包含:
class ExecutionChunk(Container):
chunk_header: ExecutionChunkHeader # 带有执行承诺的 Header
transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD]
withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD]
ExecutionChunkHeader 包含了通常在区块 Header 中预期的字段:
class ExecutionChunkHeader(Container):
index: int
txs_root: Root
post_state_root: Root # 可以移除以减少哈希开销
receipts_root: Root
logs_bloom: Bloom
gas_used: uint64
withdrawals_root: Root
正如 proto 在这份关于 Flash BALs 的文档中所描述的,后状态根可以从分块 Header 中移除,以减少哈希开销。这是一个有趣的想法,它使设计更接近于区块现在的样子(即,没有中间状态根)。
协议实施了严格的边界以确保可预测的执行:
CHUNK_GAS_LIMIT = 2^24 (16,777,216) Gas 的硬性限制。MAX_CHUNKS_PER_BLOCK = 16 个分块。
这使分块从一种传播技巧转变为区块结构的语义重新定义。验证者可以通过流式验证过程在分块到达时独立执行它们,但只有在序列中的每个分块都成功执行时,区块才有效。
假设我们将区块拆分为分块,那么我们应该将区块级访问列表(Block-level Access List, BAL)放在哪里?
区块级访问列表 (EIP-7928) 是一个向区块添加状态位置(地址、存储键)+ 交易级状态差异(State Diffs)的提案,旨在实现独立于交易依赖关系的并行执行。
对于 Payload 分块,分块访问列表 (CALs) 作为 Sidecar 与分块分开传播,其中 CALs 包含其对应分块的状态差异:

解决分块之间的依赖关系:
apply_cals(parent_state, [cal_0, cal_1]) -> pre_state_for_chunk_2
chunk[N] 本身CAL[0], CAL[1], ..., CAL[N-1](包含累积的状态差异)这创建了一个累积依赖链,其中早期 CAL 的延迟到达会延迟所有后续分块的执行。然而,由于 CALs 通常很小,它们在网络中的传播速度很快。
设计考虑: 每一个 CAL 都可以携带一个相对于前状态(Chunk 0 之前)的差异,而不是编码相对于前状态(分块 0)或前一个分块(分块 1 到 n)的状态差异。这将允许验证者在不等待 CALs #0–2 的情况下执行(例如)分块 #3,仅需要 CAL #2。权衡之处在于 CAL #2 会变得更大,因为它必须包含自身的状态差异以及从早期分块累积的差异。
CALs(包含状态差异)与分块的分离创造了截然不同的传播路径:
Gossip 主题:
execution_chunk_sidecar_{subnet_id} 上传输,其中 subnet_id = chunk_index % 16。chunk_access_list_sidecar_{subnet_id} 上传输,其中 subnet_id = cal_index % 16。
分叉选择仔细区分了分块可用性和区块有效性:
class Store:
# ... 现有字段 ...
chunks: Dict[Tuple[Root, ChunkIndex], ExecutionChunk]
chunk_access_lists: Dict[Tuple[Root, ChunkIndex], ChunkAccessList]
chunk_validation_status: Dict[Tuple[Root, ChunkIndex], bool] # 第一阶段
payload_chunk_availability: Dict[Root, bool] # 所有分块都存在
block_state_valid: Dict[Root, bool] # 第二阶段 - 最终状态与 Header 匹配
关键区别:分块可以被独立验证(Validated),但只有在验证了完整的状态转换后,区块才是有效的(Valid)。
两阶段验证反映在执行引擎 API 中:
## 第一阶段:独立分块验证
def notify_new_chunk(execution_engine, block_root, chunk, parent_root):
# 内部验证分块 - 交易、Gas、本地状态转换
return execution_engine.validate_chunk(block_root, chunk, parent_root)
def notify_new_chunk_access_list(execution_engine, block_root, index, cal):
# 提供带有状态差异的 CAL 以进行独立执行
execution_engine.new_chunk_access_list(block_root, index, cal)
## 第二阶段:完整的状态转换验证
def finalize_chunked_payload(execution_engine, block_root, expected_chunks, header):
# 验证:chunks[-1].post_state_root == header.state_root
# 这是确定区块有效性的地方
return execution_engine.verify_state_transition(block_root, expected_chunks, header)
协议在证明(Attestation)之前强制执行这两个阶段:
def is_block_valid(store: Store, block_root: Root) -> bool:
block = store.blocks[block_root]
# 第一阶段:所有分块均已单独验证
for i in range(len(block.body.chunk_roots)):
if not store.chunk_validation_status[(block_root, i)]:
return False # 分块验证失败
# 验证分块是否与承诺的根匹配
if hash_tree_root(store.chunks[(block_root, i)]) != block.body.chunk_roots[i]:
return False
# 第二阶段:完整状态转换已验证
if not store.block_state_valid[block_root]:
return False # 最终状态与 Header 不匹配
# 验证最后一个分块的后状态是否与 Header 匹配
final_chunk = store.chunks[(block_root, len(block.body.chunk_roots) - 1)]
if final_chunk.post_state_root != block.header.state_root:
return False # 状态转换无效
return True
验证者绝不能在 is_block_valid() 返回 true 之前进行证明,以确保:
执行层实现流式验证,以便在分块到达时对其进行处理:
在 EL 上,分块是自包含的执行单元:
class ExecutionChunk:
chunk_header: ExecutionChunkHeader # 在区块中的位置
transactions: Tuple[Transaction] # 完整的交易(未拆分)
withdrawals: Tuple[Withdrawal] # 仅在最后一个分块中
EL 采用关键组件进行流式验证:
关键见解在于 CALs 包含了分块执行中的状态差异,从而能够实现独立验证而无需顺序依赖。
根据可用数据,分块可以以两种模式执行:
def validate_chunk(chunk_index, chunk, dependencies):
if chunk_index == 0:
# 第一个分块直接使用父状态
pre_state = parent_state
elif previous_chunk_validated:
# 使用前一个后状态进行顺序执行
pre_state = chunks[chunk_index - 1].post_state
elif all_prior_CALs_available:
# 通过应用 CALs(包含状态差异)进行独立执行
# CALs 包含其分块的所有状态更改
pre_state = apply_CALs(parent_state, CALs[0:chunk_index])
# 执行分块交易
post_state = execute(pre_state, chunk.transactions)
assert post_state.root == chunk.post_state_root
该架构将分块验证与区块验证分开:
当每个分块到达时,验证者立即核实:
post_state_root 匹配。这发生在区块 Header 到达之前,并且不知道最终状态根。可以通过验证提议者在分块 Header 上的签名来验证分块和 CALs。
一旦所有分块都通过验证:
block.header.state_root。## 第一阶段:每个分块独立验证
for chunk in arriving_chunks:
validate_chunk(chunk) # 无需完整区块即可快速失败
## 第二阶段:验证完整的状态转换
if all_chunks_valid:
final_state = chunks[-1].post_state_root
assert final_state == block.header.state_root # 区块有效性
这种分离使分块能够在流式处理期间快速失败,但保留了关键属性,即区块有效性取决于与承诺 Header 匹配的完整状态转换。
每个分块和 CAL Sidecar 都包含一个 Merkle 包含证明(Inclusion Proof):
class ExecutionChunkSidecar(Container):
chunk: ExecutionChunk
chunk_signature: SignedChunkHeader
chunk_root_inclusion_proof: Vector[Bytes32, CHUNK_INCLUSION_PROOF_DEPTH]
这确保了即使分块无序到达或来自不同来源,验证者也可以在处理之前验证它们属于承诺的区块。
协议支持两种截然不同的分块生产流程:
chunk_roots 和 chunk_access_list_roots 的 SignedExecutionPayloadBid。chunk_roots = [hash_tree_root(chunk) for chunk in chunks],分块访问列表同理。与将区块生产外包给试图尽可能晚提交的实体(即提议者时间博弈)相比,在本地构建区块的提议者将从流式处理中获益更多。
其他生态系统以不同的方式探索了并行化和“分块”。
Fuel 和 Solana 都依赖于预先声明其完整读/写集的交易:Fuel 对 UTXO 样式的资源强制执行显式访问列表,以将交易划分为并行的无冲突集合;而 Solana 的 Sealevel 运行时并发调度非重叠的账户写入,其 Turbine 协议纯粹为了传播而将区块数据分块为 Shreds。
Aptos 和 Starknet 使用 Block-STM,它投机性地并行执行所有交易,并回滚冲突,直到达到确定的顺序状态;而 Sui 应用了一种变体,其中非冲突的以对象为中心的交易可以完全绕过共识。
相比之下,NEAR 和 Polkadot 追求状态分片:NEAR 在分片之间拆分账户,并将每个分块聚合到一条链中;而 Polkadot 运行异构平行链,其有效性证明由中继链(Relay Chain)检查并使其可用。
Base 的 Flashblocks 引入了时间分块 (temporal chunking) 而不是分片或状态分区:在每个 2 秒的区块窗口内,排序器以预确认的形式流式传输十个“Flashblocks”(间隔约 200 毫秒),并带有受限的 Gas 预算。这些微区块通过公开子区块排序降低了延迟并改善了用户体验,但所有节点仍然处理一个全局状态,并且 Flashblocks 会合并到最终区块中,没有跨分片的复杂性。
与这些模型相比,语义化 Payload 分块保持了以太坊单一全局状态机的特性,但将分块提升为具有可验证交易后状态差异(CALs)的一等协议对象,从而实现了流式验证和每分块证明,而无需投机执行或跨分片异步。此外,得益于 CALs,我们不必止步于乐观并行化:交易可以实现完美的并行化,且独立于状态依赖关系。
非语义化分块有助于传输,但不会改变区块的本质。相比之下,所描述的语义分块方法:
notify_new_chunk() 立即处理。在 EL 上,我们可以期待具体的性能提升:
CHUNK_GAS_LIMIT 的资源。共识层的语义化分块与执行层的流式验证相结合,将区块处理从单体操作转变为可组合、可流式传输的流水线,该流水线随着网络容量的增长而扩展,而不是被最大的原子单元所阻塞。
- 原文链接: ethresear.ch/t/toward-se...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
作者暂未设置收款二维码