Alert Source Discuss
⚠️ Draft Standards Track: Core

EIP-7863: 区块级预热

在一个区块的持续时间内预热地址和存储键

Authors Toni Wahrstätter (@nerolation), Jochem Brouwer (@jochem-brouwer), Alex Stokes (@ralexstokes), Ansgar Dietrichs (@adietrichs), Yoav Weiss (@yoavw), Alex Forshtat (@forshtat)
Created 2025-01-15
Discussion Link https://ethereum-magicians.org/t/eip-7863-block-level-warming/22572

摘要

本提案引入了区块级地址和存储键的预热机制,允许被访问的地址和存储键在整个区块执行期间保持其“预热”状态。被访问的槽位可以在区块级别有效地缓存,从而实现此优化。

动机

目前,EVM 的存储槽位预热机制在交易级别运行,要求每个交易独立地“预热”槽位,即使在同一区块内访问相同的存储位置也是如此。这种设计没有利用现代节点实现可以在区块级别有效缓存存储访问模式这一事实。通过将槽位预热持续时间扩展到区块级别,我们可以:

  1. 减少频繁访问的槽位的冗余预热成本
  2. 更好地使 Gas 成本与实际计算开销保持一致
  3. 在不影响安全性的前提下,提高整体网络吞吐量。

截至 2025 年 1 月,经实验测量的效率提升约为 5%。

规范

机制

当一个存储槽位在一个区块内被访问时:

  1. 首次访问区块中的一个槽位会产生 EIP-2929 中规定的冷访问成本。
  2. 在同一区块内,所有后续对同一槽位的访问仅会产生 EIP-2929 中规定的热访问成本。
  3. 热/冷状态在区块边界重置

区块处理

  1. 在每个区块的开始:
    • 初始化两个空集合 block_level_accessed_addressesblock_level_accessed_storage_keys
  2. 对于区块中的每个交易:
    • 在处理存储访问之前:
      • 检查已访问的地址是否在 block_level_accessed_addresses
      • 如果是:收取 WARM_STORAGE_READ_COST
      • 如果否:
        • 收取 COLD_ACCOUNT_ACCESS_COST
        • 将地址添加到 block_level_accessed_addresses
      • 检查存储键是否在 block_level_accessed_storage_keys
      • 如果是:收取 WARM_STORAGE_READ_COST
      • 如果否:
        • 收取 COLD_SLOAD_COST
        • 将存储键添加到 block_level_accessed_storage_keys

实现细节

该实现修改了区块执行过程,以维护区块级别的已访问地址和存储槽位集合。

区块级存储管理

def apply_body(...):
    # 初始化区块级跟踪集合
    block_level_accessed_addresses = set()
    block_level_accessed_storage_keys = set()
    
    for i, tx in enumerate(map(decode_transaction, transactions)):
        # 使用区块级上下文创建环境
        env = vm.Environment(
            # ... 其他参数 ...
            block_level_accessed_addresses=block_level_accessed_addresses,
            block_level_accessed_storage_keys=block_level_accessed_storage_keys
        )
        
        # 处理交易并更新区块级集合
        gas_used, accessed_addresses, accessed_storage_keys, logs, error = process_transaction(env, tx)
        block_level_accessed_addresses += accessed_addresses
        block_level_accessed_storage_keys += accessed_storage_keys

此代码展示了如何在执行层实现区块级槽位预热。block_level_accessed_addressesblock_level_accessed_storage_keys 集合在整个区块的执行过程中保持,并传递给每个交易的环境。

交易处理

def process_transaction(env: vm.Environment, tx: Transaction) -> Tuple[Uint, Tuple[Log, ...], Optional[Exception]]:
    preaccessed_addresses = set()
    preaccessed_storage_keys = set()
    
    # 添加区块级预访问槽位
    preaccessed_addresses.add(env.block_level_accessed_addresses)
    preaccessed_storage_keys.add(env.block_level_accessed_storage_keys)
    
    # 处理来自交易的访问列表
    ...

这会将区块级已访问的地址和存储键添加到预访问的地址和存储键中。 因此,从交易的角度来看,区块级已访问的地址和存储键与预编译合约或 coinbase 地址的处理方式相同。

def process_message_call(message: Message, env: Environment) -> MessageCallOutput:
    return MessageCallOutput(
        # ... 其他字段 ...
        accessed_addresses=evm.accessed_addresses,
        accessed_storage_keys=evm.accessed_storage_keys
    )

消息调用处理跟踪执行期间访问的地址和存储键,然后将其传播回交易级别,最终传播到区块级别。

原理

该提案建立在几个关键观察之上:

  1. 缓存效率:现代以太坊客户端已经在区块级别实现了复杂的缓存机制。扩展地址和存储键的预热以匹配此缓存行为,可以更好地使 Gas 成本与实际计算成本保持一致。

  2. 向后兼容性:任何交易的最坏情况保持不变——对于首次访问一个槽位,它永远不会支付高于当前冷访问成本的费用。

  3. 处理回滚:在 EIP-2929 中,如果一个子调用回滚,所有访问的地址和槽位都将恢复到冷状态。在区块级预热下,一个失败的交易(即,一个回滚的交易)是否应该为同一区块内的后续交易“取消预热”先前预热的地址和槽位,仍然是一个悬而未决的问题。一些人建议完全删除这种回滚引起的“冷重置”,以保持与区块级预热的一致性。这一点值得进一步讨论,特别是关于即使在失败时也保持预热状态是否符合 Gas成本与实际客户端缓存行为更好匹配的更广泛目标。

  4. 首次访问为所有人预热:在提议的机制下,区块中第一个访问和预热多个地址或存储槽位的交易承担了全部预热成本,即使后续交易从那些已经预热的槽位中受益,而无需承担额外的成本。这种方法被认为是可接受的,因为区块内的早期执行——预热通常发生的地方——通常被专业构建者放置的旨在在区块顶部执行的交易占据。由于由此产生的成本差异相对较小,因此避免了更复杂的成本分摊模型,以保持简单性。 一种替代“一人预热,人人受益”机制的方法是在区块内的所有访问中分配预热成本。在这种方法中,每个交易必须至少能够支付冷访问成本。一旦区块中的所有交易都执行完毕,总的预热成本将被平均分配,并且交易发起者将获得相应的退款。

  5. 替代区块预热窗口:除了在区块级别应用预热之外,还可以考虑更高级的多区块预热方法。可能的选项包括:

    • 在一个 epoch 的持续时间内预热地址和存储键
    • 使用跨越 x 个区块的环形缓冲区

    由于执行层(EL)目前在运行时不考虑 epoch 边界,因此最好保持这种设计。因此,涉及大小为 x 的环形缓冲区的第二个选项可能更合适。对于环形缓冲区,一种方法可能是将相关的预热信息存储在这些 x 个区块的区块头中,但这会引入额外的开销并且会使无状态性变得复杂。如果采用多区块预热解决方案,则需要仔细设计来管理这些权衡(例如,标头大小、客户端复杂性)。

向后兼容性

此更改不向后兼容,需要硬分叉激活。但是,它不会对现有合约引入任何重大更改,因为:

  1. 所有现有交易将继续像以前一样工作
  2. Gas 成本只会降低,永远不会增加
  3. 依赖 Gas 成本的合约逻辑将保持有效,因为最坏的情况没有改变

安全考虑

  1. 内存使用:热槽位的集合仍然受区块 Gas 限制 / 每个槽位访问的最小 Gas 成本的限制,从而防止通过过度内存使用进行 DoS 攻击。

  2. MEV 考虑:区块生产者可以优化交易排序,以最大限度地为自己带来槽位预热的好处。第三方区块构建者可以反向运行预热特定地址或存储键的交易。

版权

版权和相关权利通过 CC0 放弃。

Citation

Please cite this document as:

Toni Wahrstätter (@nerolation), Jochem Brouwer (@jochem-brouwer), Alex Stokes (@ralexstokes), Ansgar Dietrichs (@adietrichs), Yoav Weiss (@yoavw), Alex Forshtat (@forshtat), "EIP-7863: 区块级预热 [DRAFT]," Ethereum Improvement Proposals, no. 7863, January 2025. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7863.