文章详细介绍了 Block-in-Blobs (BiB) 提案,旨在通过将交易数据以 RLP 格式编码进 Blob 中,使以太坊验证者在 zkEVM 时代无需下载完整交易即可通过采样确保数据可用性。同时,文章还提出了“统一数据 Gas”模型,建议将执行 Gas 和 Blob Gas 合并为单一的数据维度,以简化资源核算并优化费用市场。
感谢 Kev、Francesco、soispoke、Anders 和 Jihoon 的反馈和审阅。
摘要: Block in Blobs (BiB) 采用 RLP 格式的交易,并将其编码为 Blob(类似于 type-3-txs)。这在 zkEVM 世界中非常有益,因为不再需要每个人都下载每笔交易——从而降低了带宽需求。
随着 zkEVM 的到来,验证者将不再需要执行交易来验证区块:一个简洁的证明就足够了。但这改变了数据可用性 (DA) 的运作方式。
今天,DA 是隐式强制执行的:如果不下载并执行其中的交易,你就无法验证一个区块。在 zkEVM 下,验证者验证的是证明,而不是直接验证交易。构建者可能会发布一个有效的区块和证明,同时扣留底层的交易数据。该区块将通过共识,但没有人能够重新执行它、为其建立索引或可靠地在其基础上进行构建。
Block-in-Blobs (BiB) 通过将交易数据编码到 Blob 中来解决这个问题,使 DA 成为共识层面的要求。验证者随后可以进行 采样 (sample) 而不是下载:同样的保证,更少的带宽,更强的扩展性。
在接下来的内容中,我想梳理一下设计空间以及帮助我理解这一点的相关内容。
要理解 BiB,我们首先需要理清一些术语。以太坊分为两层:共识层 (CL) 和 执行层 (EL)。这种分离是有意为之且强大的,但它引入了概念上的摩擦,特别是关于“区块”或“有效负载 (Payload)”到底是什么。
从共识层的角度来看,规范对象是 信标区块 (beacon block)。信标区块可能包含一个 ExecutionPayload,但它本身并不是执行区块。
ExecutionPayload 是 CL 和 EL 之间通过 Engine API 交换的对象:
ExecutionPayload 并将其交给 CL。ExecutionPayload 不是 EL 的原生区块格式。相反,它包含了 EL 在内部重构和验证执行区块所需的确切信息。
ExecutionPayload 的大小随着区块 Gas 限制的增加而增长,但也取决于 Calldata 定价和其他因素。随着区块 Gas 限制的提高,区块大小也会增加,带宽需求也随之增加。幸运的是,有两种解决方案。
首先,效仿 Flashblocks 的例子,我们可以将有效负载拆分为固定大小的片段,使其能够独立处理——从而有效地隐藏延迟。
其次,我们可以考虑让验证者不下载所有交易,但仍能确定这些交易已被发布的方法——即 Block in Blobs (BiB)。虽然这篇文章描述了第一种解决方案,但在接下来的内容中,我想重点关注第二种。
BiB 提议将区块(可能还包括 区块级访问列表 (BAL))编码为固定大小的 Blob。类似于 type-3-tx Blob,验证者届时将不再需要下载完整的 ExecutionPayload,而只需下载他们负责托管的列。这释放了扩展效益,因为并非每个验证者都需要下载所有内容,在不削弱安全/可用性保证的情况下降低了带宽需求。
所以当人们说“将区块放入 Blob 中”时,他们的真实意思是将 CL 的 执行有效负载 放入 Blob 中……即便如此,这也不是全部事实。
尽管名字如此,BiB 并没有将整个区块,甚至整个执行有效负载都放入 Blob 中。在其最简单的形式中,该协议只需要确保 交易数据的可用性。
ExecutionPayload 内部的交易已经具备了正确的形式:
BiB 获取序列化的 RLP 编码交易,将其分块并打包到 Blob 中。
其他所有与执行相关的内容(EL 区块头字段、执行输出根等)要么移入信标区块,要么带有一个在信标区块中包含承诺的单独 Sidecar。BiB 保证了 重新执行所需的信息是可用的。
区块级访问列表 (BAL) 可能会与交易面临相同的命运。由于 BAL 的使用者可能与交易的使用者不同,将 BAL 也放入 Blob 中但与交易分开可能是有意义的。
在确立了“是什么”和“为什么”之后,让我们看看“如何做”。Blob 编码遵循 EIP-4844 模型:
payload.transactions(每个已经是 RLP 编码的)0x00,以便该元素保持在 BLS 模数之下ExecutionPayloadEnvelope,有效负载 Blob 以 Sidecar 的形式在网络中传输ExecutionPayloadEnvelope)并从中本地构建 ExecutionPayload关键见解:在 zkEVM 下,验证者不需要交易来验证承诺。他们只需要承诺来进行共识,而 DA 采样则确保 Blob 内容在网络上可用。
为了让 BiB 对 不下载所有有效负载 Blob 的 zk 证明者起作用,证明必须保证三件事:
$pre\_state$ 到 $post\_state$ 的状态转换是有效的。这些要求共同将数据可用性从“通过重新执行实现的隐式”转变为“通过 Blob 承诺 + DAS 实现的显式”。
注意:BiB 和 zkEVM/强制证明是正交的:
BiB 可以迭代推出,首先将交易(可能还有 BAL)放入 Blob 中,而暂不引入执行和 Blob 编码的强制 zk 证明。此外,部分数据托管也不需要从一开始就执行。BiB 在其最小形式下,只需获取 RLP 编码的交易,并将其编码为 Blob。从 EL 的角度来看,没有任何变化——在收到后,CL 从收到的 Blob 中构建 ExecutionPayload,并从中构建 EL 区块——就像今天一样。在这一点上,这些 Blob 已经可以被消耗可选证明的 zk 证明者使用,这些证明计划在针对有限的一组证明者推出强制证明之前推出。
BiB 不会将整个执行有效负载移入 Blob:只移动交易数据。
在 ePBS 的基础上,信标区块携带 执行区块头(或承诺/元数据),而 有效负载 则通过 Blob 单独提供。
Blob 承载沉重的字节;出价 (Bids) 承载紧凑的承诺。
这种设计会使未来转向 Slot 拍卖变得复杂。如果我们想保留该选项,我们可能仍然需要在 Sidecar 之外保留 ePBS 的
ExecutionPayloadEnvelope,或者也将 EL 请求移入有效负载 Blob 中。
最后,让我们立足于具体的数字。如今 60M Gas 限制下的执行有效负载平均约为 ~150 KiB(约等于 1-2 个有效负载 Blob),最大大小约为 ~5.7 MiB(取决于 EL 数据定价)。
有效负载 Blob 的最大数量取决于 Gas 限制和 Calldata 定价。观察最近的区块,60M Gas 限制偶尔会产生 3-4 个 Blob 的区块:
在最坏情况下,假设 EIP-7976(针对 Calldata 密集型交易,每字节 64 Gas),我们将需要约 7 个填满 Calldata 的 Blob。按照今天的 Calldata 定价(10/40 Gas),这个数字将跃升至 45 个 Blob,进一步推广开来,我们得到以下公式来确定所需的最大 Blob 计数:
$$N = \left\lceil \frac{L}{c \cdot B} \right\rceil$$
其中
$L$ = 区块 Gas 限制
$c$ = Calldata Gas 成本 (Gas/字节)
$B$ = Blob 大小 (字节)
BiB 回答了 如何 通过 Blob 使交易数据可用。但它引出了一个更根本的问题:我们如何核算所有这些数据?
今天,以太坊在数据方面有两个独立的资源维度:
这些维度具有 独立的限制。区块 Gas 限制上限了执行 Gas;MAX_BLOB_GAS_PER_BLOCK 上限了 Blob Gas。一个区块可以同时达到这 两者 的最大值。
在 BiB 下,Calldata 变成了 Blob。但 type-3 交易 也 携带 Blob。如果不改变资源核算方式,我们将面临叠加的最坏情况:
$$N{worst} = N{calldata} + N_{type3} = \left\lceil \frac{L}{c \cdot B} \right\rceil + \frac{\text{MAX_BLOB_GAS}}{\text{GAS_PER_BLOB}}$$
在 60M Gas 限制、EIP-7976 定价(64 Gas/字节)以及 21 个 type-3 Blob 的情况下:
$$N_{worst} = 7 + 21 = 28 \text{ 个 Blob}$$
按照今天的 Calldata 定价(10/40 Gas),这等于 $11 + 21 = 32$ 个 Blob,且全部不可压缩。
优雅的解决方案是将来自 Calldata 和 Blob 数据的数据压缩成一个单一的 数据维度。核心原则是:
所有需要被提供的数据都应计入同一个限制。
碎片化仅存在于 EL,在那里我们有:
## 当前:两个独立的检查
assert tx.gas <= block_gas_limit - block.gas_used # 执行 Gas (execution gas)
assert tx.blob_gas <= MAX_BLOB_GAS - block.blob_gas_used # Blob Gas (blob gas)
在 BiB 下,交易和 type-3 Blob 都变成了同一种东西:Blob,核算方式也应反映这一点。
将二维模型替换为单一的 数据字节 (data bytes) 维度:
BYTES_PER_BLOB = 131_072 # 128 KiB
MAX_DATA_BYTES = max_blobs * BYTES_PER_BLOB # 例如:28 个 Blob ≈ 3.5 MiB
def data_bytes(tx):
# 完整编码的交易进入有效负载 Blob
tx_bytes = len(rlp_encode(tx))
# type-3 Blob Sidecar 增加了更多 DA
if tx.blob_versioned_hashes:
tx_bytes += len(tx.blob_versioned_hashes) * BYTES_PER_BLOB
return tx_bytes
区块级强制执行变为一次统一检查:
## 一次统一检查(取代原本独立的 Gas + Blob_Gas 检查)
if data_bytes(tx) > MAX_DATA_BYTES - block.data_bytes_used:
invalid("data limit exceeded")
不再有叠加的最坏情况。一个区块可以拥有 28 个 Blob 的数据量,无论是 28 个 type-3 Blob、28 个编码交易的 Blob,还是两者的任何混合。
Calldata 目前支付的执行 Gas 隐式地涵盖了计算 和 数据可用性。通过统一数据 Gas,我们可以清晰地分离这些关注点:
def intrinsic_cost(tx):
# 执行 Gas:纯计算(不含 Calldata 成本!)
exec_gas = TX_BASE_COST + access_list_cost + create_cost + auth_cost
# 数据字节:完整交易 + Blob Sidecar
data = len(rlp_encode(tx))
if tx.blob_versioned_hashes:
data += len(tx.blob_versioned_hashes) * BYTES_PER_BLOB
return exec_gas, data
注意缺失了什么:没有 Calldata Gas 成本,没有 EIP-7623 底线价格。
EIP-7623 引入了 Calldata 底线价格,以防止包含大量 Calldata 但没有执行的庞大区块,确保 Calldata 密集型交易不会因为它们带来的带宽负担而支付过低的费用,同时不消耗任何其他资源。但在统一的数据费用市场中,这种保护是 免费 提供的:
| 关注点 | EIP-7623 解决方案 | 统一数据 Gas 解决方案 |
|---|---|---|
| 防止垃圾信息 | 底线 Gas 成本(每字节 10/40) | 数据费用市场(上涨的 $data\_base\_fee$) |
| 价格信号 | 隐含在执行 Gas 中 | 显式的每字节 $data\_base\_fee$ |
| 适应性 | 固定底线 | 动态(EIP-1559 式调整) |
如果有人试图用 Calldata 填满区块,$data\_base\_fee$ 就会上涨,就像区块填满时 $base\_fee\_per\_gas$ 会上涨一样。市场会自动处理垃圾信息防范。
更简洁的模型:
TX_BASE_COST + 访问列表 + auth + create + 执行期间的操作码)没有重复计算。没有底线。一份计算费,一份数据费。
正如 Blob Gas 拥有自己的 EIP-1559 式费用市场一样,统一数据字节也将拥有:
## 数据层面的 EIP-1559 式定价(机制与当今的 Blob Gas 相同)
data_base_fee = fake_exponential(MIN_DATA_PRICE, excess_data_bytes, UPDATE_FRACTION)
## 超额追踪(与 Blob Gas 相同)
excess = max(0, parent.excess_data_bytes + parent.data_bytes_used - TARGET_DATA_BYTES)
一种新的交易类型引入了显式的数据费用上限,让用户可以更精细地控制他们愿意为数据支付多少费用:
## 新交易类型字段(类似于 type-3 对 Blob 的处理)
tx.max_fee_per_data_byte # 显式数据费用上限
tx.blob_versioned_hashes # 用于 Blob Sidecar
完整的验证流程如下所示:
def validate(tx, block):
exec_gas, data = intrinsic_cost(tx)
if has_explicit_data_fee(tx):
# 新交易类型:显式的各维度上限
max_cost = tx.gas * tx.max_fee_per_gas + data * tx.max_fee_per_data_byte
assert tx.max_fee_per_data_byte >= data_base_fee
else:
# 传统类型:单一的总预算 (EIP-7999 风格)
max_cost = get_max_fee(tx) # gas_price × gas_limit
required = exec_gas * base_fee_per_gas + data * data_base_fee
assert required <= max_cost
assert exec_gas <= tx.gas
assert data <= MAX_DATA_BYTES - block.data_bytes_used
assert sender.balance >= max_cost + tx.value
$max\_fee\_per\_data\_byte$ 字段在新的交易类型中引入。但现有的交易通过 Gas 限制分区 (gas limit partitioning)(详见 EIP-7999)保持完全兼容:
def partition_gas_limit(tx):
if has_explicit_data_fee(tx): # 新交易类型
return tx.gas, data_bytes(tx)
else: # 传统类型
calldata_tokens = zero_bytes + 4 * non_zero_bytes
execution_gas = tx.gas - STANDARD_TOKEN_COST * calldata_tokens
return execution_gas, data_bytes(tx)
关键见解:传统交易的 $gas\_price \times gas\_limit$ 已经提供了一个总预算。这个预算现在涵盖了执行 Gas 和数据 Gas:Calldata 成本只是从一个维度移到了另一个维度。不需要额外的资金。
区块头追踪的是数据使用情况,而不是 Blob Gas:
## 替换这些字段:
blob_gas_used → data_bytes_used # 此区块中的总 DA
excess_blob_gas → excess_data_bytes # 用于 EIP-1559 定价
统一数据 Gas 规范的初稿可以在这里找到。
关键见解如下:BiB 和 统一数据 Gas (Unified Data Gas) 解决的是不同的问题。
| BiB | 统一数据 Gas | |
|---|---|---|
| 解决的问题 | 如何使 Calldata 可进行 DA 采样 | 如何限制总 DA 需求 |
| 机制 | 编码(交易 → Blob) | 核算(所有数据统一限制) |
| 影响层级 | CL(Blob Sidecar、承诺) | EL(Gas 计量、固有成本) |
| 能独立存在吗? | 能 | 能 |
没有统一数据 Gas 的 BiB 仍然可行:你只需接受叠加的最坏情况或引入临时限制。这与当今协议的运作方式并无不同。
没有 BiB 的统一数据 Gas 同样可行:它限制了总数据量,但并没有为交易开启 DAS。
两者结合会产生协同效应:BiB 提供“编码”,统一数据 Gas 提供“核算”。
- 原文链接: ethresear.ch/t/blocks-ar...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!