EIP-7709 深度解析:从 EIP-2935 系统合约存储读取 BLOCKHASH 并重构其 Gas 计费
2026/9/16 7:11:15 网站建设 项目流程

EIP-7709 深度解析:从 EIP-2935 系统合约存储读取 BLOCKHASH 并重构其 Gas 计费

【免费下载链接】EIPsThe Ethereum Improvement Proposal repository项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs

导读

EIP-7709(Read BLOCKHASH from Storage and Update Cost)是面向以太坊执行层(Core)的协议提案,其核心目标是将BLOCKHASH (0x40)操作码从"依赖客户端历史区块数据"的特殊案例,重构为"基于 EIP-2935 系统合约存储"的状态支撑访问,并同步引入与SLOAD对齐的冷/热存储 Gas 计费。阅读本文后,你将掌握 EIP-7709 的完整规格(参数、窗口计算、槽位映射)、resolve_blockhash伪代码的逐行语义、三种客户端解析路径、激活前提,以及它与 EIP-2929 冷热访问计费、EIP-2930 访问列表之间的联动关系。


一、背景与动机:BLOCKHASH 的"协议特殊案例"困境

在现行 EVM 中,BLOCKHASH (0x40)操作码负责返回最近 256 个祖先区块中某一区块号的哈希。它存在一个结构性问题:它假定客户端本地拥有最近的区块历史。这使得它成为协议中的一个特殊案例——它依赖历史链数据,却没有像其他读取那样被建模为"状态支撑的读取"(state-backed read)。

这种特殊性带来的代价是多方面的:

  • 对于无状态客户端(stateless client),历史区块哈希无法打包进 witness,无法通过状态证明来验证;
  • 对于witness 构建,客户端不得不在 witness 构造逻辑中为BLOCKHASH单独开一条"通过父区块头链证明"的旁路,而不是复用现成的状态访问机制;
  • 计价上,BLOCKHASH使用固定的极低费用(现行 EVM 中为固定 20 gas 的基础成本),与它实际触发的资源消耗(状态存储访问)完全不匹配。

EIP-2935(已 Final)解决了第一个问题:它在每个区块处理开始时,将最近HISTORY_SERVE_WINDOW(8191)个区块哈希写入一个系统合约的环形缓冲区(ring buffer)存储。EIP-7709 则在此基础上更进一步:让窗口内的BLOCKHASH查询直接建模为对系统合约存储的访问,同时保持BLOCKHASH返回值的既有语义不变

二、核心规格:从存储解析 BLOCKHASH

2.1 参数表

EIP-7709 定义了以下协议参数:

参数
FORK_TIMESTAMPTBD(待定)
HISTORY_STORAGE_ADDRESS0x0000F90827F1C53a10cb7A02335B175320002935
BLOCKHASH_SERVE_WINDOW256
HISTORY_SERVE_WINDOW8191

其中HISTORY_STORAGE_ADDRESSHISTORY_SERVE_WINDOW均继承自 EIP-2935,BLOCKHASH_SERVE_WINDOW保持既有BLOCKHASH服务窗口不变(256 个祖先区块)。注意这两个窗口大小不同且作用不同BLOCKHASH_SERVE_WINDOW决定BLOCKHASH操作码的返回值窗口,HISTORY_SERVE_WINDOW决定环形缓冲区的槽位模数。

2.2 触发边界:fork_block

EIP-7709 从fork_block开始生效,其定义是:

fork_block.timestamp >= FORK_TIMESTAMP 且 fork_block.parent.timestamp < FORK_TIMESTAMP

即时间戳跨过FORK_TIMESTAMP的第一个区块。从该区块起,BLOCKHASH指令按新的存储解析逻辑执行。

2.3 resolve_blockhash 伪代码逐行解析

EIP-7709 用如下 Python 伪代码定义新的解析逻辑:

def resolve_blockhash(block: Block, state: State, arg: uint64): # note that outside the BLOCKHASH_SERVE_WINDOW we continue to return 0 # despite the 2935 history contract being able to serve more hashes if arg >= block.number or (arg + BLOCKHASH_SERVE_WINDOW) < block.number: return 0 # performs an sload on arg % HISTORY_SERVE_WINDOW including gas charges, # warming effects as well as state-access recording # # note that the `BLOCKHASH_SERVE_WINDOW` and the 2935 ring buffer window # `HISTORY_SERVE_WINDOW` for slot calculation are different return state.load_slot(HISTORY_STORAGE_ADDRESS, arg % HISTORY_SERVE_WINDOW)

逐行解读:

  1. 窗口外返回 0:若arg >= block.number(查询当前或未来区块)或arg + 256 < block.number(查询超出最近 256 个祖先),直接返回0,与现行语义一致。值得强调的是,即便 EIP-2935 合约存储了多达 8191 个哈希,BLOCKHASH操作码依然只在 256 窗口内提供服务——这是为了保持向后兼容,不扩展上述豁免范围。
  2. 槽位映射:窗口内的查询执行一次state.load_slot(HISTORY_STORAGE_ADDRESS, arg % HISTORY_SERVE_WINDOW),即对系统合约地址上arg % 8191号槽位做一次存储读取。这里的关键设计是:返回值窗口(256)与环形缓冲区模数(8191)不同——由于 EIP-2935 在set操作中写入block.number - 1 % HISTORY_SERVE_WINDOW槽位,查询区块号arg的哈希恰好落在arg % 8191槽位。
  3. 完整副作用load_slot的注释明确要求包含 Gas 计费、槽位预热(warming)以及状态访问记录(state-access recording)三类副作用,详见下一节。

2.4 三种客户端解析路径

对于窗口内的arg,客户端(执行客户端)可以任选以下方式之一解析:

  • 直接SLOAD:直接从状态中读取HISTORY_STORAGE_ADDRESS的对应槽位;
  • 系统调用 EIP-2935 合约的get机制:以非SYSTEM_ADDRESS调用者身份调用历史合约(因为get仅在调用者不是SYSTEM_ADDRESS时触发);
  • 内存/既有设计:如全节点客户端按现行设计维护所需历史时,直接从内存服务。

无论选择哪种解析方式,只要arg处于正确的BLOCKHASH窗口内,客户端必须应用当前激活分叉所定义的SLOAD操作的全部语义与副作用。

三、强制语义:完整落地 SLOAD 的三大副作用

EIP-7709 明确列出了窗口内查询必须应用的SLOAD副作用:

  1. SLOAD的 Gas 成本(冷或热):针对槽位arg % HISTORY_SERVE_WINDOW。按 EIP-2929(已 Final)的定价:首次访问(冷)为COLD_SLOAD_COST = 2100gas,交易内已访问过(热)为WARM_STORAGE_READ_COST = 100gas。
  2. SLOAD的预热后效:读取后该槽位被加入交易级accessed_storage_keys集合,后续同交易内访问按热价计费。
  3. 状态访问记录:按当前激活分叉要求,为对应的SLOAD记录状态访问(这是 witness 构建与无状态执行的基础)。

底层机制来自 EIP-2929(已 Final):它维护交易级集合accessed_addressesaccessed_storage_keysSLOAD (0x54)首次访问冷槽位收 2100 gas,已访问热槽位收 100 gas。EIP-7709 正是把这一整套机制"复用"到BLOCKHASH上,使BLOCKHASH的计价与其实际访问的资源(系统合约存储)严格对齐。

3.1 系统合约执行的 Gas 不适用

一个容易被忽略的细节:即便客户端选择通过系统调用来解析BLOCKHASH,系统代码执行的 Gas 也不会被收取——只应用上述SLOAD的效果。这一点在 EIP-2935 的系统中同样成立:区块处理开始时的系统更新调用(process_block_hash_historySYSTEM_ADDRESS调用)不会预热历史合约账户及其存储槽位,第一次调用需按 EIP-2929 规则支付预热费用。EIP-7709 延续了这一思路,保持 Gas 低廉且对选择其他解析方式的客户端实现简单。

3.2 预热的现实意义:访问列表(EIP-2930)

由于冷SLOAD成本高达 2100 gas,窗口内BLOCKHASH的成本将由"固定 20 gas"跃升为"基础成本 + 2100(冷)/ 100(热)"。这与 EIP-2930(已 Final)的访问列表机制天然互补:交易可在accessList中预先声明HISTORY_STORAGE_ADDRESS及其目标槽位(每个存储键预付费ACCESS_LIST_STORAGE_KEY_COST = 1900gas),将其提前载入accessed_addresses/accessed_storage_keys集合,从而使实际执行中的SLOAD仅按 100 gas 热价计费——这是对依赖固定 Gas 成本合约的迁移缓冲手段。

四、激活机制与前提条件

EIP-7709 的激活以 EIP-2935 已激活为前提,并规定了两种可接受的时序:

  • 提前量足够:EIP-7709 的激活须在 EIP-2935 激活之后至少BLOCKHASH_SERVE_WINDOW(256 个区块)以上;或
  • 创世激活:在测试网/开发网中,EIP-7709 可与 EIP-2935 一同在创世(genesis)激活。

这一前提的根源在于 EIP-2935 的环形缓冲区需要时间"灌满":它需要HISTORY_SERVE_WINDOW(8191)个区块才能完全填满缓冲区,且合约只包含分叉区块的父哈希、不含更早历史。若 EIP-7709 在 EIP-2935 激活后不足 256 个区块即生效,系统合约尚未保存所需的完整窗口历史,将导致破坏性变更(详见第六节)。

五、Rationale 解读:为什么这样设计

EIP-7709 的 Rationale 明确阐述了四个设计取舍:

  1. 计价与资源匹配:更新后的 Gas 成本与所访问的资源等价——即读取一次存储。将BLOCKHASH纳入既有的"冷/热状态访问定价"体系,而不是为近期区块哈希新增一条独立的 Gas 规则。
  2. 替代方案对比:客户端虽然也可以"通过一串父区块头链证明近期区块哈希",但那会让BLOCKHASH继续作为 witness 构建中的特殊案例,无法复用 EIP-2935 提供的状态访问机制。
  3. 不引入新特殊规则:无论"始终按热SLOAD收费"还是"自定义BLOCKHASH价格"都能降低兼容性风险,但都会引入一条新的特殊案例 Gas 规则,故被否决。
  4. 系统合约执行费用不适用:EIP-2935 系统合约执行的收费与访问均不被应用,以保持 Gas 低廉,并让选择直接 SLOAD 或内存解析的客户端实现保持简单。

此外需要明确边界:BLOCKHASH操作码只服务有限的BLOCKHASH_SERVE_WINDOW(256)窗口以保持向后兼容、不扩展上述豁免。更深层的历史访问需直接调用 EIP-2935 系统合约(其get操作支持[block.number - 8191, block.number - 1]范围),此时将按正常合约执行收费并记录访问——这正是 EIP-2935 为 Rollup 等场景提供的长历史窗口查询能力。

六、向后兼容性与破坏性影响

EIP-7709 明确了两点兼容性结论:

  1. 返回值语义不变BLOCKHASH在窗口内的返回值、窗口外返回0的语义均与现行行为一致,不改变任何应用读取到的哈希值。
  2. Gas 成本显著上升:窗口内查询从固定低费用变为"基础成本 + 冷/热 SLOAD 成本",可能破坏依赖旧 Gas 成本的用例(例如对子调用设置固定 Gas 上限的合约)。这与 EIP-2929 涨价时的合约破坏风险同类,开发者可通过 EIP-2930 访问列表预先预热存储槽位来缓解。
  3. 分叉间距不足的破坏:若 EIP-2935 与 EIP-7709 分叉之间不足BLOCKHASH_SERVE_WINDOW(256)个区块(且 EIP-2935 未在创世激活,如测试网/开发网场景),则 EIP-2935 系统合约尚未保存所需历史,将引入破坏性变更。

七、测试用例清单

EIP-7709 给出了可执行的行为验证标准:

  • BLOCKHASH的参数超出最近BLOCKHASH_SERVE_WINDOW个祖先,或大于等于当前区块号,返回0应用额外的存储访问副作用;
  • 若查询窗口内祖先且对应存储槽位为,操作码收取"基础BLOCKHASH成本 + 冷SLOAD成本(2100 gas)";
  • 若同一交易内多次BLOCKHASH查询映射到同一存储槽位,后续查询按SLOAD成本(100 gas)计费;
  • 每次BLOCKHASH操作的基础成本仍应被收取,叠加每次查询的SLOAD成本;
  • 若直接调用 EIP-2935 合约(而非经由BLOCKHASH),则按当前分叉的正常合约执行规则收取 Gas、记录状态访问并应用其他效果;
  • 若 EIP-7709 在 EIP-2935 之后正确激活(间隔>= BLOCKHASH_SERVE_WINDOW),BLOCKHASH解析结果应保持一致。

八、安全考量

EIP-7709 明确表示:除 EIP-2935 中已包含的安全考量外,暂无新增安全考量。需要留意的是 EIP-2935 提到的"分支投毒(branch poisoning)"风险——系统合约热更新路径可能被攻击者以微量 ETH 投毒以拖慢状态根更新,但该 EIP 已评估攻击成本会显著上升,难以造成有意义的减速。

九、生态位置与相关 EIP 脉络

EIP-7709 处于以太坊"将历史区块哈希纳入状态"的演进主线上,其技术栈关联如下:

  • EIP-2935(Final,必选前置):在区块处理开始时以SYSTEM_ADDRESS调用历史合约的set()(输入为block.parent.hash,Gas 上限30_000_000、价值0),将block.number - 1 % 8191槽位写入父哈希;该 EIP 明确"对BLOCKHASH解析机制无影响"——EIP-7709 正是补上这一环。
  • EIP-2929(Final):提供COLD_SLOAD_COST = 2100WARM_STORAGE_READ_COST = 100及交易级访问集合,是 EIP-7709 计费语义的底层来源。
  • EIP-2930(Final):访问列表机制为合约迁移提供预热缓冲。
  • EIP-4788:定义了系统调用(system call)的约定(执行至完成、不计入区块 Gas 上限、不遵循 EIP-1559 燃烧语义),EIP-2935 的合约调用遵循同一约定;EIP-161 则保证历史合约账户免于空账户清理。

在生态层面,EIP-8081(元 EIP,追踪分叉范围)已将 EIP-7709 列为 "Proposed for Inclusion"(提议纳入),而 EIP-7709 自身的状态仍为DraftFORK_TIMESTAMP为 TBD,属于推进中的协议变更提案。读者若需验证实现细节,可对照 EIP-2935 中给出的历史合约 EVM 汇编(get/set双路径、0x1fff即 8191 模数)及其合成部署交易参数,理解HISTORY_STORAGE_ADDRESS的由来——该地址由部署交易发送者0x3462413Af4609098e1E27A490f554f260213D685部署的首个合约地址rlp([sender, 0])计算得出,密码学绑定于部署交易的 initcode。

结语

EIP-7709 的提案思路可以概括为一句:"让BLOCKHASH不再是特例,而是一次普通的存储读取。"通过复用 EIP-2935 的系统合约存储与 EIP-2929 的冷热访问定价,它把历史区块哈希访问纳入统一的状态访问计价与 witness 记录框架,为无状态执行铺路,同时保持返回值语义完全向后兼容。对于合约开发者,理解其"基础成本 + 冷/热 SLOAD"的新计价模型,以及访问列表预热的缓解手段,是分叉升级前做好 Gas 预算评估的关键。

本文基于仓库 EIPS/eip-7709.md 编写,版权声明遵循 CC0(Copyright and related rights waived via CC0)。

【免费下载链接】EIPsThe Ethereum Improvement Proposal repository项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询