EIP-8146 深度解析:将区块访问列表(BAL)作为独立 Sidecar 提前传播,重塑 ePBS 关键路径
【免费下载链接】EIPsThe Ethereum Improvement Proposal repository项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs
导读
EIP-8146(Block Access List Sidecars,Draft 状态,Standards Track / Core)是对 EIP-7928(区块级访问列表 BAL)与 EIP-7732(内嵌式提议者-构建者分离 ePBS)的一次架构级重构:它把原本内嵌在执行载荷(ExecutionPayload)中的区块访问列表剥离出来,改经一条专属 gossip topic 作为独立 sidecar 提前传播,并在ExecutionPayloadBid中仅携带keccak256(rlp(BAL))这一 32 字节承诺。读完本文,你将掌握 EIP-8146 的完整规范——包括共识层容器与 fork choice 变更、两类新的 Req/Resp 协议、Builder 与 PTC 成员的新职责,以及engine_notifyBlockAccessListV1等 Engine API 方法如何为执行客户端创造至少 1 秒的"预热窗口",让状态预取与 post-state root 计算先于执行开始。
一、背景:BAL 从何而来,又为何需要独立传播
1.1 BAL 是什么(EIP-7928 的遗产)
EIP-7928 引入了区块级访问列表(Block Access List,BAL):一份记录区块执行期间所有被访问账户、存储槽及其 post-execution 值的完整清单,编码为 RLP,结构遵循address -> field -> block_access_index -> change的嵌套模式(BlockAccessList = List[AccountChanges])。它带来的核心收益包括:
- 并行磁盘读取与并行交易验证;
- 并行 post-state root 计算;
- 无需重放交易即可重建状态(executionless state updates)。
关键约束在于:BAL 大小与区块本体相当。仓库中的实测分析 assets/eip-7928/bal_size_analysis_60m.md 显示,在 60M gas 上限下,1000 个区块样本的完整 BAL 平均压缩后约 72.4 KiB,而压缩后的平均区块大小约为 71.7 KiB——也就是说,BAL 与区块几乎等大,其中存储写入占 40.3%、存储读取占 25.8%。早期分析 assets/eip-7928/bal_size_analysis.md 也佐证了这一量级(100 区块样本下含 reads 配置平均压缩约 42.7 KB)。
1.2 现状问题:BAL 卡在关键路径上
在 EIP-7732 的 ePBS 架构下,执行载荷不再随信标区块体传播,而是由 Builder 在 slot 进行期间通过SignedExecutionPayloadEnvelope揭示。EIP-7928 把 BAL 作为ExecutionPayload的一个字段,因此BAL 与交易绑定在同一个 topic、同一个截止时间下到达——执行客户端只有等载荷整体到达后才能拿到 BAL,预热窗口为零。
EIP-8146 正是针对这一耦合提出的解耦方案:把 BAL 从载荷中移出,作为独立 sidecar 提前至少 1 秒(由 PTC 计时委员会强制执行)到达。它在原文档中列出了四大收益:
| 收益 | 说明 |
|---|---|
| 分离关注点 | BAL 是状态差异,载荷是产生它的交易集合;只推进状态的节点(如不做重放的同步客户端)只需要 diff,不需要交易 |
| 关键路径块体积减半 | 全量 BAL ≈ 72.4 KiB,与压缩区块 ≈ 71.7 KiB 相当;从信封中移除后对象尺寸减半,可用更低的网络上限做 DoS 防护 |
| 更早的 BAL 投递 | 通过提前的观察截止时间,BAL 至少领先载荷 1 秒(PTC 强制执行),执行客户端可以带声明的状态预取进入执行,post-state root 计算先行启动 |
| 更好的包含列表 | EIP-7805 的 slot N 包含列表委员会在 slot N 载荷揭示前就冻结列表;提前拿到 BAL 的 post-values 后,包含列表构建者无需执行载荷即可推进其状态视图 |
二、共识层规范:容器、fork choice 与网络协议
2.1 新常量
| 名称 | 值 | 说明 |
|---|---|---|
MAX_BLOCK_ACCESS_LIST_SIZE | uint64(2**23)(= 8 MiB) | 编码后 BAL 的 SSZ 上限 |
MIN_EPOCHS_FOR_BLOCK_ACCESS_LIST_SIDECARS_REQUESTS | uint64(3533) | sidecar 必须被服务的 epoch 数,与 EIP-7928 的 BAL 保留窗口一致 |
BLOCK_ACCESS_LIST_LEAD_TIME | uint64(1) | sidecar 需至少提前载荷证明截止时间多少秒,才算"可用" |
2.2 新类型与容器变更
新增 SSZ 类型:BlockAccessList = ByteList[MAX_BLOCK_ACCESS_LIST_SIZE]——RLP 编码的区块访问列表,对共识层而言是不透明字节(consensus layer 不做 RLP 解码)。
ExecutionPayload(移除字段):EIP-7928 加入的block_access_list字段被移除:
class ExecutionPayload(Container): # ... fields unchanged from EIP-7732 ... blob_gas_used: uint64 excess_blob_gas: uint64 # [Removed in EIP-8146] # block_access_list: BlockAccessListExecutionPayloadBid(新增字段):
class ExecutionPayloadBid(Container): parent_block_hash: Hash32 parent_block_root: Root block_hash: Hash32 prev_randao: Bytes32 fee_recipient: ExecutionAddress gas_limit: uint64 builder_index: BuilderIndex slot: Slot value: Gwei execution_payment: Gwei blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] # [New in EIP-8146] block_access_list_hash: Bytes32其中block_access_list_hash = keccak256(rlp(BlockAccessList)),与 EIP-7928 写入执行区块头的block_access_list_hash字段值完全一致。注意 EIP-7928 规定:无状态变化时该字段为keccak256(rlp.encode([])),即空 RLP 列表哈希0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347。
PayloadAttestationData(新增字段):
class PayloadAttestationData(Container): beacon_block_root: Root slot: Slot payload_present: boolean blob_data_available: boolean # [New in EIP-8146] block_access_list_present: boolean新增BlockAccessListSidecar容器:
class BlockAccessListSidecar(Container): beacon_block_root: Root slot: Slot block_access_list: BlockAccessList2.3 Fork Choice 变更
Store新增两个字段:
@dataclass class Store(object): # ... existing fields ... # [New in EIP-8146] block_access_lists: Dict[Root, BlockAccessList] = field(default_factory=dict) block_access_list_availability_vote: Dict[Root, List[Optional[boolean]]] = field( default_factory=dict )on_block:在既有ptc_vote初始化旁,为新区块根初始化PTC_SIZE(EIP-7732 中定义为 512)个投票槽位:
store.block_access_list_availability_vote[block_root] = [None] * PTC_SIZEon_payload_attestation_message:在记录payload_present的同时记录 BAL 可用性投票;notify_ptc_messages在从信标区块中的聚合证明提取PayloadAttestationMessage对象时,也会透传block_access_list_present:
store.block_access_list_availability_vote[data.beacon_block_root][ptc_index] = ( data.block_access_list_present )on_block_access_list_sidecar:对每个通过 gossip 校验的 sidecar 调用。注意其中两个关键设计:共识层把 BAL 视为不透明字节、不做 RLP 解码;并在载荷到达之前就把 BAL 交付给执行层:
def on_block_access_list_sidecar(store: Store, sidecar: BlockAccessListSidecar) -> None: assert sidecar.beacon_block_root in store.blocks block = store.blocks[sidecar.beacon_block_root] assert sidecar.slot == block.slot # The consensus layer treats the BAL as opaque bytes; no RLP decoding bid = block.body.signed_execution_payload_bid.message assert keccak256(sidecar.block_access_list) == bid.block_access_list_hash store.block_access_lists[sidecar.beacon_block_root] = sidecar.block_access_list # Deliver to the execution layer without waiting for the payload EXECUTION_ENGINE.notify_block_access_list(sidecar.block_access_list, bid.block_hash)on_execution_payload_envelope:载荷不得先于同区块的 BAL 交给执行层;若信封先于 sidecar 到达,客户端不得丢弃,而是缓存起来,待on_block_access_list_sidecar存入匹配的 BAL 后重跑该处理器:
def on_execution_payload_envelope( store: Store, signed_envelope: SignedExecutionPayloadEnvelope ) -> None: envelope = signed_envelope.message assert envelope.beacon_block_root in store.block_states assert is_data_available(envelope.beacon_block_root) # [New in EIP-8146] The BAL must be available locally assert envelope.beacon_block_root in store.block_access_lists state = store.block_states[envelope.beacon_block_root] verify_execution_payload_envelope(state, signed_envelope, EXECUTION_ENGINE) store.payloads[envelope.beacon_block_root] = envelopeverify_execution_payload_envelope本身保持不变。
2.4 网络层:新 Gossip Topic 与两类 Req/Resp 协议
Gossip topicblock_access_list_sidecar:携带BlockAccessListSidecar对象的全局 topic。转发前必须通过以下校验:
- [IGNORE]
sidecar.slot <= current_slot(允许MAXIMUM_GOSSIP_CLOCK_DISPARITY偏差); - [IGNORE]
sidecar.slot >= compute_start_slot_at_epoch(store.finalized_checkpoint.epoch); - [IGNORE]未见过针对
sidecar.beacon_block_root的有效 sidecar; - [IGNORE]根为
sidecar.beacon_block_root的信标区块已被看到(客户端 MAY 将 sidecar 排队等待区块到达)。
设block为该信标区块,bid为其body.signed_execution_payload_bid.message:
- [REJECT]
block通过校验; - [REJECT]
sidecar.slot == block.slot; - [REJECT]
keccak256(sidecar.block_access_list) == bid.block_access_list_hash。
Req/Resp:BlockAccessListSidecarsByRoot v1,协议 ID/eth2/beacon_chain/req/block_access_list_sidecars_by_root/1/:
| Request | Response |
|---|---|
List[Root, MAX_REQUEST_PAYLOADS] | List[BlockAccessListSidecar, MAX_REQUEST_PAYLOADS] |
按请求的信标区块根返回对应 sidecar。
Req/Resp:BlockAccessListSidecarsByRange v1,协议 ID/eth2/beacon_chain/req/block_access_list_sidecars_by_range/1/:
| Request | Response |
|---|---|
(start_slot: Slot, count: uint64) | List[BlockAccessListSidecar, MAX_REQUEST_PAYLOADS] |
返回 slot 区间[start_slot, start_slot + count)内的 sidecar,按 slot 排序,至多MAX_REQUEST_PAYLOADS条。
客户端必须通过两种方法服务最近MIN_EPOCHS_FOR_BLOCK_ACCESS_LIST_SIDECARS_REQUESTS(3533)个 epoch 的 sidecar,更早的 MAY 被裁剪。
2.5 验证者职责:Builder 与 PTC 成员
Builder 的四步流程:
- 从
engine_getPayloadV6获取载荷与 BAL; - 在
ExecutionPayloadBid中设置block_access_list_hash = keccak256(blockAccessList); - 携带 bid 的信标区块发布后,在
block_access_list_sidecartopic 上广播BlockAccessListSidecar; - 广播不再携带 BAL 的
SignedExecutionPayloadEnvelope。
Builder 应尽可能早地广播 sidecar,且MUST NOT把步骤 3 推迟到步骤 4 的载荷信封揭示之后。
PTC 成员:仅当某个区块的 sidecar 在本地通过 gossip 校验、且至少早于载荷证明截止时间BLOCK_ACCESS_LIST_LEAD_TIME(1 秒)时,才将block_access_list_present置为True。payload_present与blob_data_available仍按 EIP-7732 独立设置,与block_access_list_present互不影响。
三、Engine API 变更:专用的 BAL 交付通道
3.1engine_getPayloadV6
响应结构变化:blockAccessList(RLP 编码的 BAL)成为响应顶层字段,而非载荷结构内的字段。
3.2engine_notifyBlockAccessListV1
将 BAL 独立于载荷交付给执行层。参数:
blockAccessList:RLP 编码的 BAL 字节;blockHash:32 字节,将 BAL 与某个载荷绑定。
执行层收到后:
- 在
blockHash下存储该 BAL; - 开始预取 BAL 声明的账户与存储槽;
- MAY 开始利用 BAL 的 post-values 计算 post-state root。
注意:该调用不是有效性检查——BAL 是在执行载荷时对照区块头校验的,此方法仅确认收到。共识客户端必须在on_block_access_list_sidecar验证完 sidecar 后立即调用此方法,不得等待载荷信封。对一个给定的blockHash,共识层 MUST 先调用engine_notifyBlockAccessListV1再调用engine_newPayloadV5。
3.3engine_newPayloadV5
EIP-7928 为 engine API 载荷结构添加的blockAccessList字段被移除,其余行为不变。执行层将载荷与先前为同一blockHash交付的 BAL 配对,并按 EIP-7928 校验。
对照 EIP-7928 的原始设计可以更清楚这次改动的含义:EIP-7928 中ExecutionPayloadV4扩展了blockAccessList字段,engine_newPayloadV5校验计算出的访问列表与提供的blockAccessList一致、不一致则返回INVALID;EIP-8146 只是把这份数据的送达路径从"载荷内"改成了"独立方法",BAL 的构造、校验与block_access_list_hash头部字段语义均原样保留。
四、设计理由:为什么这样设计
4.1 头部承诺复用,杜绝 Builder 双花(equivocate)
EIP-7928 已经在执行区块头里用keccak256(rlp(BAL))承诺了 BAL。在 bid 中携带这相同的 32 字节,共识层就有了认证 sidecar 的全部依据——无需第二套哈希方案,也无需在执行层之外做 RLP 解码。Builder 无法双花:bid.block_hash传递性地承诺了区块头,因此若bid.block_access_list_hash与揭示的载荷不一致,要么payload.block_hash != bid.block_hash导致信封被拒,要么执行层重算出的 BAL 与自身头部不符、按 EIP-7928 拒绝载荷。
此外,把 BAL 移出ExecutionPayload容器,也消除了 EIP-7928 为修剪后的 BAL 描述的 hash tree root 替换(SSZ 哈希树根替换)需求——sidecar 保留策略不再影响载荷的 Merkle 承诺。
4.2 不给 sidecar 单独签名
Builder 已经签署了 bid,验证即keccak256(sidecar.block_access_list) == bid.block_access_list_hash。再给 sidecar 加 BLS 签名只会增加验证成本、不增加任何约束力。这与 blob 数据用 bid 中携带的承诺来验证的方式如出一辙。
4.3 独立的 PTC 字段
block_access_list_present沿用了blob_data_available的模式:payload_present信号信封是否及时、blob_data_available信号 blob 可用性、block_access_list_present信号 BAL 可用性——缺失的对象可以归因到具体环节。该投票仅是信号,本 EIP 不附加惩罚;可用性在本地强制执行,因为on_execution_payload_envelope无论 PTC 结果如何都以 BAL 存在为前提。
4.4 发布时机:为什么 BAL 可以提前、载荷不能
在 ePBS 中,Builder 会把执行载荷信封推迟到接近证明截止时间才发布——过早揭示会让交易暴露给同 slot unbundling(恶意提议者插入对抗性 bundle 来剥削 Builder)。但 BAL不暴露这个风险:它只携带访问列表和 post-state 值,不携带已签名交易。提前收到 BAL 的提议者虽然能知道哪些合约被触碰、哪些余额变动,但要窃取 MEV 就必须冒被罚没(slash)的风险。
BLOCK_ACCESS_LIST_LEAD_TIME覆盖了发布较晚的 Builder:若 BAL 与载荷同时或更晚到达,就会失去可用性投票,这也保证了至少 1 秒的预取与 post-state root 计算窗口——没有它,这个提前量就完全取决于 Builder 的行为。
4.5 专用 Engine API 方法创造时间差
分两次调用交付 BAL 与载荷,正是提前量的来源:
t0 sidecar received --engine_notifyBlockAccessListV1--> EL prefetches state, MAY start post-state root t1 envelope received --engine_newPayloadV5-------------> EL executes against warm state t2 <--------- {status: VALID}--------t1 - t0是本 EIP 创造的头启动窗口;若 BAL 仍在信封内,这一窗口为零。排序是共识层的责任:先到的信封会被缓存,直到 sidecar 到达,因此执行层绝不会在没有 BAL 的情况下收到载荷,也就不需要回退路径。
4.6 Sidecar 保留期对齐弱主观性
EIP-7928 要求执行层在弱主观性周期(= 3533 epochs)内保留 BAL,以便离线短于该周期的节点可通过重执行同步。MIN_EPOCHS_FOR_BLOCK_ACCESS_LIST_SIDECARS_REQUESTS使用相同窗口,于是弱主观性周期内同步的节点可以从任一层取得 BAL;超出该窗口后,BAL 可通过执行区块重新生成。
五、向后兼容性
本提案修改了 EIP-7732 的ExecutionPayload、ExecutionPayloadBid、PayloadAttestationData容器以及 EIP-7928 的 engine API 方法。这些变更不向后兼容,需要一次硬分叉。分叉前区块的 BAL 仍保留在执行载荷内部、按 EIP-7928 的方式可取回;sidecar topic 与两类 Req/Resp 方法自分叉起生效。
六、安全考量
6.1 扣留(Withholding)
Builder 可以扣留 BAL sidecar。此时 PTC 成员投票block_access_list_present = False,让失败全网可见;由于on_execution_payload_envelope以本地 BAL 可用为前提,任何节点都无法验证该载荷,无法验证的载荷不会被继续构建——扣留的代价是 Builder 丢掉自己的区块。EIP-7732 的支付属性不变。
6.2 早期 BAL 暴露
提前的 sidecar 会在交易之前暴露状态变化。但没有独立签名的对象被暴露,因此没有任何东西可以被 unbundling。泄露的是粗粒度的 slot 内活动,而且这些信息也会在数秒后随载荷泄露给观察同一变化的各方。
6.3 网络开销
每 slot 的字节总数不变——原本在信封内传输的 BAL 现在走自己的 topic。负载只是从"载荷揭示与证明截止时间之间"的集中窗口,重新分配到了不同 topic、摊开到更宽的时间窗。
6.4 验证成本
sidecar 验证只需一次对至多MAX_BLOCK_ACCESS_LIST_SIZE(8 MiB)字节的keccak256,相对载荷验证可忽略。共识客户端因此新增一个 keccak 依赖,可由久经考验的现成库满足。
6.5 无匹配的 BAL
执行层可能持有永远不会到达的载荷所对应的 BAL。实现 SHOULD 限制该缓存(例如按 slot 距离或在 finalization 时清理)。流入量受 gossip 规则约束为每个有效信标区块根最多一个 sidecar。
七、小结
EIP-8146 是 ePBS(EIP-7732)与区块级访问列表(EIP-7928)组合下的一次传输层重构:它不改变 BAL 的构造、语义与校验规则,而是改变其送达路径与时机。通过将约 72 KiB 的 BAL 移出执行载荷信封、以keccak256(rlp(BAL))承诺在 bid 中完成认证、经独立 gossip topic 与两类 Req/Resp 协议传播、并由engine_notifyBlockAccessListV1提前注入执行层,该提案为执行客户端打开了状态预取与 post-state root 预计算的确定性窗口,同时也为 EIP-7805 的包含列表构建者提供了不执行交易即可推进状态视图的能力。完整规范以 EIPS/eip-8146.md 为准,相关实证数据可参考 assets/eip-7928/bal_size_analysis_60m.md 与 assets/eip-7928/bal_size_analysis.md。
【免费下载链接】EIPsThe Ethereum Improvement Proposal repository项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考