EIP-7886: 延迟执行
将区块验证与执行分离
Authors | Francesco D'Amato (@fradamt), Toni Wahrstätter (@nerolation) |
---|---|
Created | 2025-02-18 |
Discussion Link | https://ethereum-magicians.org/t/eip-7886-delayed-execution/22890 |
Requires | EIP-1559, EIP-2930, EIP-4844, EIP-7623, EIP-7702 |
Table of Contents
摘要
本提案引入了一种机制,通过最小的检查使执行区块能够进行静态验证,这些检查仅需要先前的状态,而无需执行区块的交易。这使得验证者能够在不完成区块执行的情况下证明区块的有效性。
动机
该提案的主要优势是异步区块验证。在目前的以太坊协议中,区块必须在验证者对其进行证明之前完全执行。这个要求在共识过程中形成了一个瓶颈,因为证明者必须等待执行结果才能提交他们的投票,限制了网络的吞吐量潜力。
通过引入一种机制,执行负载可以被恢复,而不是使整个区块无效,执行不再是验证的直接要求。相反,区块的有效性可以基于其结构正确性和发送者预先支付的交易费用来确定。这允许证明在插槽中更早地发生,独立于执行,有可能实现更高的区块 gas 限制和整个网络吞吐量的显著提升。
规范
Header 变更
区块头结构被扩展以支持延迟执行:
@dataclass
class Header:
# 现有字段
parent_hash: Hash32
ommers_hash: Hash32
coinbase: Address
# 执行前状态根 - 这是执行交易之前的状态根
pre_state_root: Root
# 来自父区块的延迟执行输出
parent_transactions_root: Root # 来自父区块的交易根
parent_receipt_root: Root # 来自父区块的收据根
parent_bloom: Bloom # 来自父区块的日志布隆过滤器
parent_requests_hash: Hash32 # 来自父区块的请求哈希
parent_execution_reverted: bool # 指示父区块的执行是否被恢复
# 其他现有字段
difficulty: Uint
number: Uint
gas_limit: Uint
gas_used: Uint # 声明的交易使用的 gas,执行后验证
timestamp: U256
extra_data: Bytes
prev_randao: Bytes32
nonce: Bytes8
base_fee_per_gas: Uint
withdrawals_root: Root
blob_gas_used: U64 # 交易使用的总 blob gas
excess_blob_gas: U64
parent_beacon_block_root: Root
关键的变更包括:
pre_state_root
:表示执行之前的状态根(对照父区块的执行后状态进行检查)parent_receipt_root
:来自父区块的收据根(延迟执行输出)parent_bloom
:来自父区块的日志布隆过滤器(延迟执行输出)parent_requests_hash
:来自父区块的请求哈希(延迟执行输出)parent_execution_reverted
:指示父区块的执行是否被恢复
区块头必须包含所有这些字段,才能被认为在本 EIP 下有效。pre_state_root
必须与应用父区块执行后的状态根相匹配。父执行输出必须准确地反映前一个区块的执行结果,以保持链的完整性。
链状态跟踪
扩展区块链对象以跟踪执行输出,以便在后续区块中进行验证:
@dataclass
class BlockChain:
blocks: List[Block]
state: State
chain_id: U64
last_transactions_root: Root # 上一个执行区块的交易根
last_receipt_root: Root # 上一个执行区块的收据根
last_block_logs_bloom: Bloom # 上一个执行区块的日志布隆过滤器
last_requests_hash: Bytes # 上一个执行区块的请求哈希
last_execution_reverted: bool # 指示上一个区块的执行是否被恢复
这些附加字段用于验证后续区块中声明的延迟执行输出。last_transactions_root
、last_receipt_root
、last_block_logs_bloom
、last_requests_hash
和 last_execution_reverted
作为关键的链状态引用,必须在每个区块执行后更新,以确保正确的状态进展。当区块的执行由于 gas 不匹配而回滚时,last_execution_reverted
字段设置为 True
,这将影响后续区块的基础费用计算。
区块验证
静态验证与执行分开进行。在此阶段,将执行所有无需执行交易即可完成的检查:
def validate_block(chain: BlockChain, block: Block) -> None:
# 根据父区块验证区块头
validate_header(chain, block.header)
# 验证来自父区块的延迟执行输出
if block.header.parent_transactions_root != chain.last_transactions_root:
raise InvalidBlock
if block.header.parent_receipt_root != chain.last_receipt_root:
raise InvalidBlock
if block.header.parent_bloom != chain.last_block_logs_bloom:
raise InvalidBlock
if block.header.parent_requests_hash != chain.last_requests_hash:
raise InvalidBlock
if block.header.pre_state_root != state_root(chain.state):
raise InvalidBlock
if block.header.parent_execution_reverted != chain.last_execution_reverted:
raise InvalidBlock
...
# 处理所有交易的 trie 并验证交易
total_inclusion_gas = Uint(0)
total_blob_gas_used = Uint(0)
withdrawals_trie = Trie(secured=False, default=None)
# 按地址跟踪发送者的余额和 nonce
sender_balances = {}
sender_nonces = {}
# 根据 excess blob gas 计算 blob gas 价格
blob_gas_price = calculate_blob_gas_price(block.header.excess_blob_gas)
# 验证每笔交易
for i, tx in enumerate(map(decode_transaction, block.transactions)):
# 验证交易参数(签名、费用等)
validate_transaction(tx, block.header.base_fee_per_gas, block.header.excess_blob_gas)
# 恢复发送者
sender_address = recover_sender(chain.chain_id, tx)
# 计算 gas 成本(包括 EIP-7623 固有成本和 floor 成本)
intrinsic_gas, calldata_floor_gas_cost = calculate_intrinsic_cost(tx)
blob_gas_used = calculate_total_blob_gas(tx)
# 跟踪总 gas 使用量(使用固有 gas 和 floor 成本的最大值)
total_inclusion_gas += max(intrinsic_gas, calldata_floor_gas_cost)
total_blob_gas_used += blob_gas_used
# 计算最大 gas 费用(包括 blob 费用)
effective_gas_price = calculate_effective_gas_price(tx, block.header.base_fee_per_gas)
max_gas_fee = tx.gas * effective_gas_price + blob_gas_used * blob_gas_price
# 验证发送者是 EOA 还是具有委托支持
if sender_address not in sender_balances:
account = get_account(chain.state, sender_address)
is_sender_eoa = (
account.code == bytearray()
or is_valid_delegation(account.code)
)
if not is_sender_eoa:
raise InvalidBlock
sender_balances[sender_address] = account.balance
sender_nonces[sender_address] = account.nonce
# 验证发送者有足够的余额
if sender_balances[sender_address] < max_gas_fee + Uint(tx.value):
raise InvalidBlock
# 验证正确的 nonce
if sender_nonces[sender_address] != tx.nonce:
raise InvalidBlock
# 跟踪余额和 nonce 变化
sender_balances[sender_address] -= max_gas_fee + Uint(tx.value)
sender_nonces[sender_address] += 1
# 验证 gas 约束
if total_inclusion_gas > block.header.gas_used:
raise InvalidBlock
if total_blob_gas_used != block.header.blob_gas_used:
raise InvalidBlock
# 验证 withdrawals trie
for i, wd in enumerate(block.withdrawals):
trie_set(withdrawals_trie, rlp.encode(Uint(i)), rlp.encode(wd))
if block.header.withdrawals_root != root(withdrawals_trie):
raise InvalidBlock
此验证函数强制执行以下几个要求:
- 客户端必须验证区块的父执行输出是否与链跟踪的最后一个执行输出匹配。
pre_state_root
必须与当前状态根匹配,以确保正确的状态转换。- 所有交易必须经过静态验证,以确保签名正确性和交易类型特定的要求(EIP-1559、EIP-4844 等)。
- 发送者账户必须是外部拥有的账户 (EOA) 或具有有效的委托支持 (EIP-7702)。
- 发送者必须有足够的余额来支付最大可能的 gas 费用加上交易价值。
- 交易 nonce 必须正确并且每个发送者都是连续的。
- 总的包含 gas(使用固有 gas 和 EIP-7623 floor 成本的最大值)不能超过区块声明的 gas_used。
- 总的 blob gas 必须与声明的 blob_gas_used 相匹配。
- Withdrawal trie 根必须与其对应的头部字段匹配。
在计算包含 gas 时,实现使用常规固有 gas 成本和 EIP-7623 calldata floor 成本中的最大值,这确保了对 calldata gas 的正确计算,而无论执行结果如何。
区块执行与状态快照
在区块通过静态验证后,执行将继续进行,并预先向交易发送者收费:
def apply_body(
block_env: vm.BlockEnvironment,
transactions: Tuple[Union[LegacyTransaction, Bytes], ...],
withdrawals: Tuple[Withdrawal, ...],
) -> vm.BlockOutput:
block_output = vm.BlockOutput()
# 首先处理系统交易(信标根、历史存储)
process_system_transaction(
block_env=block_env,
target_address=BEACON_ROOTS_ADDRESS,
data=block_env.parent_beacon_block_root,
)
process_system_transaction(
block_env=block_env,
target_address=HISTORY_STORAGE_ADDRESS,
data=block_env.block_hashes[-1], # 父哈希
)
# 处理用户交易
process_transactions(block_env, block_output, transactions)
# 处理提款
process_withdrawals(block_env, block_output, withdrawals)
# 处理请求(存款、提款、合并)
# 如果头部中的 execution_reverted 为 True,则不生成
process_general_purpose_requests(
block_env=block_env,
block_output=block_output,
)
return block_output
def process_transactions(
block_env: vm.BlockEnvironment,
block_output: vm.BlockOutput,
transactions: Tuple[Union[LegacyTransaction, Bytes], ...],
) -> None:
# 在交易执行之前获取状态的区块级快照
begin_transaction(block_env.state)
# 解码交易
decoded_transactions = [decode_transaction(tx) for tx in transactions]
# 提前向发送者预先收取最高可能的 gas 费用
for tx in decoded_transactions:
deduct_max_tx_fee_from_sender_balance(block_env, tx)
# 执行每笔交易
for i, tx in enumerate(decoded_transactions):
process_transaction(block_env, block_output, tx, Uint(i))
# 如果执行已经恢复,则停止处理
if block_output.execution_reverted:
break
# 验证声明的 gas 用量与实际 gas 用量是否一致
block_output.execution_reverted = (
block_output.execution_reverted
or block_output.block_gas_used != block_env.block_gas_used
)
# 如果执行被恢复,则重置所有输出并回滚状态
if block_output.execution_reverted:
rollback_transaction(block_env.state)
block_output.block_gas_used = Uint(0)
block_output.transactions_trie = Trie(secured=False, default=None)
block_output.receipts_trie = Trie(secured=False, default=None)
block_output.receipt_keys = ()
block_output.block_logs = ()
block_output.requests = []
block_output.execution_reverted = True
else:
# 如果执行有效,则提交状态
commit_transaction(block_env.state)
在区块执行期间:
def deduct_max_tx_fee_from_sender_balance(block_env, tx):
effective_gas_price = calculate_effective_gas_price(tx, block_env.base_fee_per_gas)
blob_gas_price = calculate_blob_gas_price(block_env.excess_blob_gas)
blob_gas_used = calculate_total_blob_gas(tx)
max_gas_fee = tx.gas * effective_gas_price + blob_gas_used * blob_gas_price
sender = recover_sender(block_env.chain_id, tx)
sender_account = get_account(block_env.state, sender)
set_account_balance(block_env.state, sender, sender_account.balance - U256(max_gas_fee))
-
顺序执行交易,直到执行交易将导致区块超过 gas 限制。
-
执行后,客户端必须验证总 gas 用量是否与区块头中声明的
gas_used
匹配。 - 如果实际使用的 gas 与声明的值不匹配,客户端必须:
- 将
execution_reverted
设置为True
- 回滚到执行前拍摄的快照
- 重置所有执行输出(收据、日志等)
- 返回零 gas 用量
- 将
-
区块本身保持有效,但执行输出不应用于状态。
-
当执行被恢复时,将跳过 EIP-7685 中定义的一般目的请求。
- 执行输出(
last_transactions_root
、last_receipt_root
、last_block_logs_bloom
、last_requests_hash
、last_execution_reverted
)根据执行结果在链状态中更新,并将在后续区块中进行验证。
超过区块 gas 限制后,执行应停止并且负载应恢复:
def process_transaction(...)
if block_output.block_gas_used + tx.gas > block_env.block_gas_limit:
block_output.execution_reverted = True
return
...
理由
延迟执行输出
将执行输出延迟到下一个区块的核心创新使得无需立即执行即可进行静态和有状态的验证。pre_state_root
为验证提供了一个密码学上可验证的起点,而父执行输出创建了一个延迟执行结果链,从而维护了区块链状态的完整性。
这种方法允许验证者根据区块的结构和预先收取的交易费用来证明区块的有效性,而无需等待执行结果,从而消除了验证管道中的执行瓶颈。
预收费机制
在执行之前向发送者预先收取最高可能的费用,可以保证交易有足够的余额被包含在区块中。该机制与现有的费用模型兼容,包括 EIP-1559 动态费用交易和 EIP-4844 blob 交易。
通过在验证期间跟踪发送者的余额和 nonce,该协议可以强制执行交易有效性而无需执行,从而实现更早的区块证明。
状态快照架构
块级快照机制提供了一种在必要时恢复执行的方法。如果实际使用的 gas 与头部中声明的 gas 不匹配,这种方法允许客户端回滚整个区块的执行,而不会使区块结构本身无效。
这提供了两个关键优势:
- 它允许验证者在执行完成之前证明区块
- 它确保最终正确执行,并对不正确的 gas 声明进行经济处罚
执行恢复处理
当由于 gas 不匹配而恢复区块的执行时:
- 区块本身仍然有效并且是规范链的一部分
last_execution_reverted
标志在链状态中设置为True
- 下一个区块必须包含此标志作为
parent_execution_reverted
- 当父区块执行被恢复时,后续区块的基础费用计算使用 0 作为父 gas 用量
向后兼容性
此 EIP 需要进行硬分叉,因为它会改变区块验证和执行过程。
安全考虑
执行正确性保证
该协议通过以下主要机制确保执行的正确性:
- 延迟执行输出必须在后续区块中匹配,从而创建一个可验证执行结果链。
- 如果实际使用的 gas 与声明的值不匹配,则必须进行状态回滚,从而为正确的 gas 声明提供经济激励。
parent_execution_reverted
标志确保区块在父执行已恢复时确认,从而维护链的完整性。- 静态和有状态的验证保证所有交易都经过正确授权,并且发送者有足够的资金来支付最高费用。
恢复执行后的基础费用动态
当区块的执行被恢复时,下一个区块的基础费用计算会将父区块的 gas 用量视为零,而不管头部中声明了什么。这确保了基础费用调整仍然能够响应实际的链使用情况,并防止通过不正确的 gas 声明来操纵费用市场。
数据可用性和经济激励
区块提议者必须声明正确的 gas 用量,否则在执行恢复时将失去交易费用。这使正确的 gas 声明的激励措施保持一致,并确保执行的完整性。
即使由于不正确的 gas 声明而导致区块的执行被恢复,所有节点仍必须存储交易数据(calldata、EIP-2930 访问列表和 blob 数据),以用于同步和区块验证。此要求创建了一个潜在的攻击向量,其中恶意行为者可以通过故意通过不正确的 gas 声明使区块无效来尝试以降低的成本将大量数据放置在链上。
但是,出于以下几个原因,这种攻击在经济上是不可持续的:
- 通过不正确的 gas 声明使区块无效的区块提议者将失去与该区块关联的所有执行层奖励。
- 攻击需要控制区块的生产,这在共识层中是一种稀缺资源。
放弃区块奖励的经济成本大大超过任何潜在的好处,这使得这种攻击在正常的网络条件下在财务上不切实际。
版权
在 CC0 下放弃版权和相关权利。
Citation
Please cite this document as:
Francesco D'Amato (@fradamt), Toni Wahrstätter (@nerolation), "EIP-7886: 延迟执行 [DRAFT]," Ethereum Improvement Proposals, no. 7886, February 2025. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7886.