Block-in-Blobs (BiB) 是一个以太坊改进提案 (EIP),旨在解决 zkEVM 时代的数据可用性问题。
PR: https://github.com/ethereum/EIPs/pull/11212
zkEVM 允许验证者使用证明来验证执行负载的正确性,而无需下载或执行负载本身。然而,取消下载执行负载的要求,也同时取消了隐式的数据可用性保证;区块生产者可以发布有效证明并隐藏执行负载数据,因为验证者不再需要它来进行共识。
本 EIP 引入了“块中数据块”(BiB)机制,该机制要求执行负载数据必须作为 blob 数据发布,且与携带相应执行负载头部的信标区块在同一区块中。这确保了即使验证者不再需要执行负载来验证状态转换函数 (STF),执行负载也始终可用。
简而言之,BiB 的工作原理是:区块生产者将执行负载数据编码为 blob,作为执行层 STF 的一部分,并要求信标区块的 blob KZG 承诺对这些负载 blob 进行承诺。
通过重执行进行验证
当前,验证者通过以下方式验证执行负载:
这隐式地保证了执行负载的可用性,因为除非节点下载了负载,否则无法验证。
使用 zkEVM 进行验证
使用 zkEVM 时,验证者改为:
在这种模型中,验证者不再需要访问完整的执行负载数据本身即可验证其正确性。
数据可用性问题
在共识中取消重执行要求,同时也取消了负载必须可用的隐式要求。
恶意或理性的构建者可以:
构建者:由于构建者始终需要重执行以构建区块,恶意构建者不会发布执行负载,从而确保他们自己是唯一能够在当前链上构建的人。
RPC 和索引器:许多节点,如 RPC 提供者和索引器,不能完全依赖执行证明,必须重执行执行负载。
BiB 通过让执行负载以 blob 形式可用来解决此问题。
执行负载数据指的是 ExecutionPayload 中必须通过 blob 可用的子集。包括:
transactionswithdrawalsrequests请参阅理性分析中的 执行负载数据包含哪些内容? 以了解为什么包含这些字段而不包含其他字段。
BiB 确保已证明的负载被发布:
负载可用性不变量: 一个有效区块意味着存在一个有序的 blob 列表,其字节解码为规范执行负载数据,并且这些 blob 的 KZG 承诺与区块引用的前 payload_blob_count 个 blob 承诺匹配。现有的 DAS 机制将确保这些 blob 可用。
| 名称 | 值 | 描述 |
|---|---|---|
MAX_PAYLOAD_BLOBS_PER_BLOCK |
待定 | 可用于编码执行负载的最大 blob 数量 |
这些参数在 EIP-4844 及相关规范中定义:
| 名称 | 值 | 来源 |
|---|---|---|
FIELD_ELEMENTS_PER_BLOB |
4096 |
EIP-4844 |
BYTES_PER_FIELD_ELEMENT |
32 |
EIP-4844 |
GAS_PER_BLOB |
2**17 |
EIP-4844 |
MAX_BLOBS_PER_BLOCK |
因分叉而异 | PeerDAS/BPO |
| 名称 | 值 | 描述 |
|---|---|---|
USABLE_BYTES_PER_FIELD_ELEMENT |
BYTES_PER_FIELD_ELEMENT - 1 (31) |
每个域元素的可使用字节数(最后一个字节必须为零,以保持在 BLS 模数以下) |
USABLE_BYTES_PER_BLOB |
FIELD_ELEMENTS_PER_BLOB * USABLE_BYTES_PER_FIELD_ELEMENT |
每个 blob 的总可使用字节数 |
在本提案中,我们使用相应共识 4844/7594 规范中定义的方法和类。
具体来说,我们使用 polynomial-commitments.md 中的以下方法:
以及 beacon-chain.md 中的以下方法:
def bytes_to_blobs(data: bytes) -> List[Blob]:
"""
将任意字节打包成一个或多个 blob。
前 4 个字节以小端序编码原始数据的长度。
最后一个 blob 中的剩余空间用零填充。
注意:4 字节长度前缀(最大约 4GB)是足够的,因为假设执行负载不会那么大。
"""
length_prefix = len(data).to_bytes(4, 'little')
prefixed_data = length_prefix + data
# 填充到 USABLE_BYTES_PER_BLOB 的整数倍
padding_needed = (USABLE_BYTES_PER_BLOB - (len(prefixed_data) % USABLE_BYTES_PER_BLOB)) % USABLE_BYTES_PER_BLOB
prefixed_data = prefixed_data + bytes(padding_needed)
blobs = []
offset = 0
while offset < len(prefixed_data):
chunk = prefixed_data[offset : offset + USABLE_BYTES_PER_BLOB]
blob = bytes_to_blob(chunk)
blobs.append(blob)
offset += USABLE_BYTES_PER_BLOB
return blobs
def bytes_to_blob(data: bytes) -> Blob:
"""
将恰好 USABLE_BYTES_PER_BLOB 个字节打包成一个 blob。
每个 31 字节的块存储在域元素的字节 [0:31] 中,
字节 [31](最后一个字节)设置为零,以确保值 < BLS 模数。
"""
assert len(data) == USABLE_BYTES_PER_BLOB
blob = bytearray(FIELD_ELEMENTS_PER_BLOB * BYTES_PER_FIELD_ELEMENT)
for i in range(FIELD_ELEMENTS_PER_BLOB):
chunk_start = i * USABLE_BYTES_PER_FIELD_ELEMENT
chunk = data[chunk_start : chunk_start + USABLE_BYTES_PER_FIELD_ELEMENT]
# 在 [0:31] 中存储 31 个数据字节,最后一个字节 [31] 保持为零
blob[i * 32 : i * 32 + 31] = chunk
return Blob(blob)
def blobs_to_bytes(blobs: List[Blob]) -> bytes:
"""
将 blob 解包回字节。
读取长度前缀(小端序)以确定实际数据大小。
"""
raw = bytearray()
for blob in blobs:
raw.extend(blob_to_bytes(blob))
length = int.from_bytes(raw[0:4], 'little')
return bytes(raw[4 : 4 + length])
def blob_to_bytes(blob: Blob) -> bytes:
"""
从每个域元素中提取 31 个可用字节。
验证每个域元素的最后一个字节是否为零。
"""
result = bytearray()
for i in range(FIELD_ELEMENTS_PER_BLOB):
# 验证最后一个字节是否为零
assert blob[i * 32 + 31] == 0x00, "无效的 blob:最后一个字节必须为零"
# 提取 31 个数据字节
result.extend(blob[i * 32 : i * 32 + 31])
return bytes(result)
TODO:未引用 MAX_TRANSACTIONS_PER_PAYLOAD、MAX_WITHDRAWALS_PER_PAYLOAD、MAX_REQUESTS_PER_PAYLOAD
class ExecutionPayloadData(Container):
transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD]
withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD]
requests: List[Request, MAX_REQUESTS_PER_PAYLOAD]
def get_execution_payload_data(payload: ExecutionPayload) -> ExecutionPayloadData:
"""
从 ExecutionPayload 中提取必须通过 blob 可用的数据。
"""
return ExecutionPayloadData(
transactions=payload.transactions,
withdrawals=payload.withdrawals,
requests=payload.requests,
)
def execution_payload_data_to_blobs(data: ExecutionPayloadData) -> List[Blob]:
"""
将执行负载数据规范编码为一个有序的 blob 列表。
编码步骤:
1. payload_bytes = SSZ.serialize(data)
2. 返回 bytes_to_blobs(payload_bytes)
注意:这使用 SSZ 序列化 ExecutionPayloadData 容器。
单个交易仍为不透明的字节数组(其内部的 RLP 编码保持不变,不会重新编码)。
"""
payload_bytes = SSZ.serialize(data)
return bytes_to_blobs(payload_bytes)
def blobs_to_execution_payload_data(blobs: List[Blob]) -> ExecutionPayloadData:
"""
将一个有序的 blob 列表规范解码为执行负载数据。
解码步骤:
1. payload_bytes = blobs_to_bytes(blobs)
2. 返回 SSZ.deserialize(payload_bytes, ExecutionPayloadData)
"""
payload_bytes = blobs_to_bytes(blobs)
return SSZ.deserialize(payload_bytes, ExecutionPayloadData)
可逆性不变量: execution_payload_data_to_blobs 和 blobs_to_execution_payload_data 在有效的执行负载数据上是互逆的。
摘要: 执行层在两个方面进行了修改:
payload_blob_count 字段,以便我们可以准确计算总的 blob_gas_used。我们在计算中不仅包含类型 3 交易,还包含负载 blob,以便 blob_gas_used 准确反映 CL 使用了多少个 blob。payload_blob_count 值,并像以前一样检查预期的版本哈希是否匹配。本 EIP 向 ExecutionPayloadHeader 添加了一个新字段:
语义:
blob_kzg_commitments 为信标区块引用的有序 kzg 承诺列表blob_kzg_commitments 的前 payload_blob_count 个条目是负载 blob 承诺(即对应负载数据的 blob 的承诺)在执行层,区块验证规则修改如下:
def validate_block(block: Block):
blob_gas_used = block.payload_blob_count * GAS_PER_BLOB
# ...
换句话说,不再是 blob_gas_used = 0,而是根据负载 blob 消耗的 blob 数量来初始化 blob_gas_used。单独的 EL STF 假定 payload_blob_count 是正确的,因为它无法在 validate_block 中检查。payload_blob_count 的正确性在 Engine API 边界处通过重新计算负载 blob 并检查与信标区块的 blob 承诺的一致性来强制执行。
此更改不会影响共识层的 blob 记账规则;它仅确保执行负载中的 blob_gas_used 准确反映总 blob 使用量,包括负载 blob。
本节指定了 new_payload 的两种等效形式。实现者根据其执行上下文选择一种:
blob_to_kzg_commitment。适用于预强制性证明的实现。verify_blob_kzg_proof_batch 的多项式打开。避免了在 zkEVM 电路中证明代价高昂的多标量乘法 (MSM)。两种变体都强制执行相同的有效性条件。一个变体下有效的区块在另一个变体下也有效。
fn new_payload(
payload: ExecutionPayload,
expected_blob_versioned_hashes: List[VersionedHash],
...
) -> PayloadStatus:
# 1. 导出负载 blob
payload_data = get_execution_payload_data(payload)
payload_blobs = execution_payload_data_to_blobs(payload_data)
payload_blob_count = len(payload_blobs)
payload_versioned_hashes = [blob_to_versioned_hash(b) for b in payload_blobs]
# 2. 验证 payload_blob_count 与头部匹配
assert payload_blob_count == payload.payload_blob_count
assert payload_blob_count <= MAX_PAYLOAD_BLOBS_PER_BLOCK
# 3. 提取类型 3 交易的版本哈希
type3_versioned_hashes = []
for tx in payload.transactions:
if tx.type == BLOB_TX_TYPE:
type3_versioned_hashes.extend(tx.blob_versioned_hashes)
# 4. 验证版本哈希:先是负载 blob,然后是类型 3
assert expected_blob_versioned_hashes == payload_versioned_hashes + type3_versioned_hashes
# 5. 运行 EL STF(现在使用 header.payload_blob_count 检查正确的 blob_gas_used 和 blob 限制)
return execute_payload(payload)
def blob_to_versioned_hash(blob: Blob) -> VersionedHash:
commitment = blob_to_kzg_commitment(blob)
return kzg_commitment_to_versioned_hash(commitment)
此变体将 blob_to_kzg_commitment 中的 MSM 替换为多项式打开证明,这在 zkEVM 电路内部验证成本更低。负载、承诺和 KZG 证明是 zkEVM 电路的私有输入,而相应的版本哈希(和负载头部)是公共输入。
fn new_payload(
payload: ExecutionPayload,
expected_blob_versioned_hashes: List[VersionedHash], # 公共输入
# BiB 新增:负载 blob 的前缀元数据
payload_kzg_commitments: List[KZGCommitment], # 私有输入
payload_kzg_proofs: List[KZGProof], # 私有输入
...
) -> PayloadStatus:
# 0. 从头部声明的负载 blob 计数
n = payload.payload_blob_count
assert n <= MAX_PAYLOAD_BLOBS_PER_BLOCK
assert len(payload_kzg_commitments) == n
assert len(payload_kzg_proofs) == n
# 1. 从执行负载数据构建负载 blob
payload_data = get_execution_payload_data(payload)
payload_blobs = execution_payload_data_to_blobs(payload_data)
assert len(payload_blobs) == n
# 2. 检查承诺是否对应预期的版本哈希前缀
payload_versioned_hashes = [\
kzg_commitment_to_versioned_hash(c) for c in payload_kzg_commitments\
]
assert expected_blob_versioned_hashes[:n] == payload_versioned_hashes
# 3. 使用批量 KZG 证明验证来检查 blob-承诺一致性
assert verify_blob_kzg_proof_batch(
blobs=payload_blobs,
commitments=payload_kzg_commitments,
proofs=payload_kzg_proofs
)
# 4. 继续进行标准的 EL 负载验证/执行
return execute_payload(payload)
共识层不会为负载 blob 引入超出 4844/7594 的新的 blob 特定验证规则。
共识层依赖执行负载头部中的 payload_blob_count 来解释 blob 承诺的顺序,但对于可用性和网络方面,负载 blob 的处理方式与其他 blob 完全相同。
BiB 重用现有的 blob 网络机制。
我们提出以下需要考虑的事项:
payload_blob_count 个 blob 并使用解码算法重建执行负载。BiB 引入了协议强制要求的 blob 使用,而不是通过类型 3 交易由用户发起。因此,负载 blob 的费用计算在性质上不同于交易 blob 费用。
本 EIP 不强制要求负载 blob 像交易 blob 那样支付每 blob 费用。
相反,负载 blob 被视为构建者在构建区块时的开销,并由构建者内部消化。具体来说:
由于负载 blob 消耗 blob gas,它们直接影响 blob 拥塞和 blob 基本费用。
执行负载数据包括 transactions、withdrawals 和 requests。这提供了一个自包含的“捆绑包”:例如,索引器可以在不访问 CL 状态或重执行的情况下解码 blob。
为什么不是头部? 头部不能放入 blob,因为它包含 payload_blob_count,这取决于 blob 的数量;会导致循环依赖。
注意:Withdrawals 和 requests 在技术上是冗余的(withdrawals 从 CL 派生,requests 可以通过执行交易重新计算)。
k 个 blob本 EIP 规定区块构建者选择 payload_blob_count,但受 MAX_BLOBS_PER_BLOCK 和 MAX_PAYLOAD_BLOBS_PER_BLOCK 施加的限制。
另一种方法始终预留 k 个 blob,其中 k 对应最坏情况下的执行负载大小。虽然这提供了更好的可预测性,但在 blob 拥塞时会降低灵活性。
在 EL STF 中执行此操作需要使负载 blob 承诺或版本哈希在核心执行逻辑中可见,而不是在 Engine API 边界处处理。
可以对序列化的执行负载数据使用压缩。这(理论上)应该允许使用更少的负载 blob,具体取决于压缩比。权衡包括:
是否应该使用压缩算法以及使用哪一种需要更多研究,特别是需要研究:
目前我们建议不采用任何压缩算法。
执行负载数据的序列化使用 SSZ。也可以选择更适合零知识证明的序列化算法,但目前没有证据表明 SSZ 反序列化是瓶颈,并且使用 SSZ 不会引入额外的依赖。
至于 RLP,由于我们在 EL STF 之外序列化 ExecutionPayload,SSZ 是自然的选择。
鉴于负载 blob 优先于类型 3 交易,指定此值意味着构建者将无法仅用负载 blob 填满整个区块。
TODO:但是,让我们研究将其移除的可能性,因为还有其他变量/EIP 限制了区块大小,并且需要与费用计算协调一致。
这需要修改执行负载头部和 EL STF;因此需要一次分叉。未实现 BiB 的节点将无法在激活后验证区块。
待定
待定
与 blob 拥塞和拒绝服务的交互
负载 blob 消耗 blob gas,因此与交易 blob 受相同的拥塞控制机制和 blob 限制的约束。
作为副产品,这确保了恶意区块生产者不能在不对 blob gas 限制负责的情况下创建任意大的执行负载。但是,我们注意到,区块生产者可以通过创建大型负载来提高 blob 基本费用。MAX_PAYLOAD_BLOBS_PER_BLOCK 为此攻击在每个区块的效果设定了上限,但这种情况需要更多研究;协议强制要求的行为是否应该收费?
数据隐藏
攻击者不能在不隐藏 blob 数据的情况下隐藏执行负载数据,否则将违反现有的 DAS 保证,并导致信标共识层拒绝该区块。
版权及相关权利通过 CC0 放弃。
- 原文链接: hackmd.io/@kevaundray/Hk...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
作者暂未设置收款二维码