深入解析 EIP-2935:将历史区块哈希写入状态存储的系统合约方案(EIPs 仓库)
2026/9/15 17:54:17 网站建设 项目流程

深入解析 EIP-2935:将历史区块哈希写入状态存储的系统合约方案(EIPs 仓库)

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

本文基于 EIP-2935 官方规范 编写。EIP-2935(Serve historical block hashes from state)是进入 Prague/Electra(Pectra)网络升级的执行层核心提案之一,其核心思路是把最近8191个区块的哈希写入一个系统合约的环形缓冲区存储,让历史区块哈希成为"状态的一部分"。读完本文,你将掌握该提案的动机、环形缓冲区与系统调用设计、get/set操作语义、部署地址的推导过程、Gas 影响,以及它与 EIP-4788(信标根入 EVM)、EIP-7709(BLOCKHASH 从存储读取并调整成本)等后续提案之间的演进关系。

一、提案定位与核心动机

EIP-2935 的全称是Serve historical block hashes from state,状态为 Final,类型为 Standards Track / Core,创建于 2020-09-03,作者包括 Vitalik Buterin、Guillaume Ballet、Gajinder Singh(g11tech)等以太坊核心开发者。

其摘要给出了一句最精炼的定义:

在区块处理逻辑中,将最近HISTORY_SERVE_WINDOW个历史区块哈希存入一个系统合约的存储中,并且该 EIP不改变BLOCKHASH指令的解析机制(因此也不改变其查询范围与成本)。

1.1 为什么需要把区块哈希放进状态

传统上,EVM 的BLOCKHASH指令隐式假设执行客户端手头持有最近的区块(哈希)。这个假设在"无状态客户端(stateless client)"的前景下并不未来可期——无状态客户端本身不保存历史区块数据。将区块哈希纳入状态之后,这些哈希就可以被捆绑进提供给无状态客户端的witness(见证数据)中。这一点在 Merkle Patricia Trie(MPT)下已经可行,而在 Verkle 树时代将变得更加高效。

1.2 为什么不直接扩展 BLOCKHASH 的服务范围

直接扩展BLOCKHASH可以服务的区块范围(BLOCKHASH_SERVE_WINDOW)属于语义变更,会破坏大量现有合约的假设。而通过本提案的合约存储来"软过渡"地扩展历史访问窗口,Rollup 等二层方案可以直接查询该合约,从而获得更长的历史窗口。

此外,该方案还有一个附带好处:可以直接针对当前状态构建/验证与最近HISTORY_SERVE_WINDOW个祖先相关的证明

二、规范速览:四个关键参数

| 参数 | 值 | | - | - | |BLOCKHASH_SERVE_WINDOW|256| |HISTORY_SERVE_WINDOW|8191| |SYSTEM_ADDRESS|0xfffffffffffffffffffffffffffffffffffffffe| |HISTORY_STORAGE_ADDRESS|0x0000F90827F1C53a10cb7A02335B175320002935|

核心数据结构是一个长度为HISTORY_SERVE_WINDOW(8191)的环形缓冲区(ring buffer),用于存储最近 8191 个区块哈希。注意HISTORY_SERVE_WINDOW>BLOCKHASH_SERVE_WINDOW,而BLOCKHASH_SERVE_WINDOW(256)保持不变——这正是"不改 BLOCKHASH 语义、只新增更长的存储式历史访问通道"的设计意图。

三、区块处理:系统调用与写入流程

3.1 每区块的系统调用

处理任何区块的开始阶段(即处理任何交易之前),以SYSTEM_ADDRESS身份调用HISTORY_STORAGE_ADDRESS

  • calldatablock.parent.hash(32 字节)
  • gas limit30_000_000
  • value0

这会触发历史合约的set()例程。这是与 EIP-4788 相同的系统操作约定,因此必须满足以下条件:

  • 该调用必须执行到完成;
  • 该调用不计入区块的 gas 限制
  • 该调用不遵循 EIP-1559 的销毁语义——调用中不应转移任何 value;
  • 如果HISTORY_STORAGE_ADDRESS处没有代码,调用必须静默失败

规范同时给出了一个替代方案:客户端也可以选择直接写入合约的存储,但通过 EVM 调用该合约仍是首选方式(详见 Rationale 部分)。

3.2 环形缓冲区的填充期

需要注意,EIP 激活后需要经过HISTORY_SERVE_WINDOW(8191)个区块才能完全填满环形缓冲区。合约在激活之初只包含分叉区块的父哈希,不包含更早的任何哈希。

3.3 EVM 变更

BLOCKHASH操作码的语义与之前完全相同。本 EIP 对BLOCKHASH机制及其成本零影响。

四、历史区块哈希合约:get / set 双操作

历史合约只有两个操作:getset合约本身不通过 calldata 的函数选择器来区分操作,而是通过调用者身份判断:

  • caller等于SYSTEM_ADDRESS(EIP-4788 引入的约定)时,执行set
  • 否则执行get

4.1get:从 EVM 查询区块哈希

get用于 EVM 内的区块哈希查询:

  • 调用者以大端编码的区块号作为 calldata 传入;
  • 如果 calldata 不是 32 字节,revert
  • 对于任何超出[block.number - HISTORY_SERVE_WINDOW, block.number - 1]范围的请求,revert

4.2set:写入父哈希

  • 调用者将block.parent.hash作为 calldata 传入;
  • 将存储槽block.number - 1 % HISTORY_SERVE_WINDOW的值设置为calldata[0:32]

4.3 官方 EVM 汇编字节码

以下是规范给出的、可直接用于历史合约的精确 EVM 汇编(源自sys-asm项目):

caller push20 0xfffffffffffffffffffffffffffffffffffffffe eq push1 0x46 jumpi push1 0x20 calldatasize sub push1 0x42 jumpi push0 calldataload push1 0x01 number sub dup2 gt push1 0x42 jumpi push2 0x1fff dup2 number sub gt push1 0x42 jumpi push2 0x1fff swap1 mod sload push0 mstore push1 0x20 push0 return jumpdest push0 push0 revert jumpdest push0 calldataload push2 0x1fff push1 0x01 number sub mod sstore stop

可以对照阅读这段汇编与上面get/set的语义描述:push2 0x1fff(即 8191)出现在取模运算处,是环形缓冲区大小在字节码中的直接体现;get路径中的两次边界检查分别对应"请求不小于block.number - 1"与"请求不早于block.number - 8191"。

4.4 部署:从交易反推合成地址

合约通过一个特殊的合成地址(synthetic address)部署,该地址由期望的部署交易反向推导而来。规范给出的部署交易如下:

{ "type": "0x0", "nonce": "0x0", "to": null, "gas": "0x3d090", "gasPrice": "0xe8d4a51000", "maxPriorityFeePerGas": null, "maxFeePerGas": null, "value": "0x0", "input": "0x60538060095f395ff33373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500", "v": "0x1b", "r": "0x539", "s": "0xaa12693182426612186309f02cfe8a80a0000", "hash": "0x67139a552b0d3fffc30c0fa7d0c20d42144138c8fe07fc5691f09c1cce632e15" }

关键点在于:交易input中有一段简单的构造器(constructor)前缀,用于在部署时拼接出所需的运行时字节码。该交易的发送者可以计算为:

0x3462413Af4609098e1E27A490f554f260213D685

该账户部署的第一个合约地址为rlp([sender, 0]),计算结果正是:

0x0000F90827F1C53a10cb7A02335B175320002935

这就是HISTORY_STORAGE_ADDRESS的由来。需要强调:虽然这种合约创建方式不像create2那样绑定特定 initcode,但该合成地址在密码学上绑定于交易的 input 数据(即 initcode)

4.5 几种激活场景

规范给出了三种典型激活情形,帮助理解环形缓冲区的初始状态:

  • 在创世(genesis)激活:创世状态不写入任何历史;在区块1开始时,创世哈希作为一次正常操作写入槽0
  • 在区块1激活:只有创世哈希被写入槽0
  • 在区块32激活:区块31的哈希被写入槽31,其余所有槽为0

五、EIP-161 处理与 Gas 成本

5.1 EIP-161 豁免

上述字节码将按照 EIP-4788 的方式部署。因此HISTORY_STORAGE_ADDRESS处的账户会带有代码且 nonce 为 1,并豁免于 EIP-161 的空账户清理

5.2 Gas:不预热、按需支付

区块开始时的系统更新(即process_block_hash_history,或通过以SYSTEM_ADDRESS为调用者的系统调用)不会按照 EIP-2929 规则预热HISTORY_STORAGE_ADDRESS账户或其存储槽。因此:

  • 第一次调用该合约时,需要为预热账户及其访问的存储槽付费;
  • 任何对该合约的普通合约调用都遵循正常的 EVM 执行语义;
  • 由于BLOCKHASH语义不变,本 EIP 对BLOCKHASH机制及其成本没有影响

六、设计 Rationale:为什么这样做

6.1 简化:去除三处不必要的复杂性

此前有过非常相似的提案,本 EIP 是对它们的简化,砍掉了三个复杂性来源:

  1. 用多层树状结构而非单层列表;
  2. 用 EVM 代码书写 EIP;
  3. 为深度历史访问做无界串行存储哈希。

权衡利弊后,最终决定只使用有限的环形缓冲区来服务所需的HISTORY_SERVE_WINDOW——因为 EIP-4788 与信标状态累加器已经允许(虽然稍复杂)针对合并(merge)以来的任意祖先构建证明。

6.2 过渡策略:选择"等窗口填满"

分叉后BLOCKHASH解析逻辑的过渡有两种方案:

  1. 等待HISTORY_SERVE_WINDOW个区块,让整个相关历史写入完毕;
  2. 在分叉块一次性存储最近全部HISTORY_SERVE_WINDOW个区块哈希。

最终选择了前者,理由是其大幅简化逻辑:合约启动大约需要一天时间,考虑到这是一个全新的历史访问方式且没有既有合约依赖它,这一取舍被认为是有利的。

6.3 插入父哈希的两种客户端实现

客户端插入父区块哈希到状态一般有两种选择:

  1. 系统调用HISTORY_STORAGE_ADDRESS,让合约自行处理存储;
  2. 绕过 EVM 处理,直接写入状态 trie

直接写入的实现伪代码如下:

def process_block_hash_history(block: Block, state: State): if block.timestamp >= FORK_TIMESTAMP: // FORK_TIMESTAMP should be defined outside of the EIP state.insert_slot(HISTORY_STORAGE_ADDRESS, (block.number-1) % HISTORY_SERVE_WINDOW , block.parent.hash)

规范建议:在 Verkle 分叉之前采用方案 1,以与 EIP-4788 保持一致,并规避"该 EIP 已激活但历史合约尚未部署"的错误配置网络问题。如果 Verkle 分叉时过滤系统合约代码块被认为过于复杂,该建议可能会被重新评估。

6.4 为什么缓冲区大小是 8191

其他系统合约中,环形缓冲区大小选用素数,是为了确保在缓冲区被完全填满之前不会有值被覆盖,之后每个值每轮迭代更新一次,即使部分槽位缺失或出块时间变化也是如此。而本 EIP 中,参与取模运算是区块号,它每轮只递增 1,因此可以确信环形缓冲区始终饱和

为了与其他系统合约保持一致,仍然保留了 8191 的大小。以当前主网参数计算,8191 个根大约覆盖一天的区块历史,这给用户留出了充足的时间来发起一笔针对特定哈希的验证交易并让其上链。

七、向后兼容性与安全考量

7.1 向后兼容

本 EIP 对区块验证规则集引入了向后不兼容的变更,但这两处变更都不影响当前用户活动与体验——因为BLOCKHASH指令的对外语义原封不动。

7.2 安全考量:分支投毒攻击

拥有"热更新路径(分支)"的合约(系统合约或其他)存在分支投毒(branch poisoning)攻击风险:攻击者可能在热路径(分支)附近撒上微不足道的 ETH,试图拖慢状态根更新。但评估结论是:要让状态根更新出现有意义的减速,攻击成本会急剧上升,因此该风险被认为可接受。

7.3 测试用例

官方测试用例位于 execution-specs 仓库的tests/prague/eip2935_historical_block_hashes_from_state目录(对应 commit27174ca81b09dee4d41db23b40305cd1bb70f5bf),覆盖了从状态提供历史区块哈希的完整场景。

八、在 Pectra 升级中的落地与生态关联

8.1 随 Pectra 主网上线

EIP-7600(Hardfork Meta - Pectra) 将 EIP-2935 列为 Prague/Electra 升级的核心执行层 EIP 之一,并给出了各网络的激活时间:

网络激活 Epoch激活时间戳
Holešky1159681740434112
Sepolia2224641741159776
Hoodi20481742999832
Mainnet3640321746612311

8.2 与 EIP-4788 的姊妹关系

EIP-2935 在系统调用约定(SYSTEM_ADDRESS、30M gas、0 value、静默失败)、部署方式(合成地址 + 构造器前缀)、环形缓冲区设计(素数 8191)等方面都与 EIP-4788(Beacon block root in the EVM) 高度同构。区别在于:4788 以timestamp % HISTORY_BUFFER_LENGTH为索引、存储(timestamp, beacon_root)双缓冲以抵御跳槽攻击;而 2935 以(block.number - 1) % HISTORY_SERVE_WINDOW为索引、只需单缓冲,因为区块号单调递增、缓冲区必然饱和。

8.3 为 EIP-7709 铺路:BLOCKHASH 存储化

EIP-7709(Read BLOCKHASH from Storage and Update Cost) 直接建立在 EIP-2935 之上:它要求BLOCKHASH (0x40)操作码改为从该系统合约的存储中读取,并对窗口内的查询施加(冷/暖)SLOAD存储成本。其伪代码清晰展示了 2935 环形缓冲区的槽位计算被复用的方式:

def resolve_blockhash(block: Block, state: State, arg: uint64): 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 return state.load_slot(HISTORY_STORAGE_ADDRESS, arg % HISTORY_SERVE_WINDOW)

EIP-7709 明确指出:BLOCKHASH指令本身仍只服务有限的 256 窗口以保持向后兼容,更深层的历史访问需要直接调用 EIP-2935 系统合约(这会走一次正常的合约执行并产生相应收费与状态访问记录)。这也印证了 EIP-2935 的设计定位:它是一套"更长历史的存储式查询通道",而非对BLOCKHASH的替换。

8.4 在后续提案中的引用

  • EIP-3670(EOF 代码格式) 指出BLOCKHASH指令已被 EIP-2935 引入的系统合约很好地取代;
  • EIP-7886(区块级快照相关提案) 要求客户端在任何交易执行前创建区块级快照,且该步骤发生在处理 EIP-4788 与 EIP-2935 系统合约之后
  • EIP-7928(区块级访问列表相关提案) 的ITEM_COST设计特意为系统合约执行(如 EIP-2935、EIP-4788、EIP-7002、EIP-7251)与提款收款人(EIP-4895)产生的、不消耗区块 gas 的 BAL 条目预留缓冲,并将 EIP-2935 历史哈希合约地址0x0000F90827F1C53a10cb7A02335B175320002935明确列为"在区块哈希合约存储父哈希"的系统合约写入点。

九、总结

EIP-2935 用"状态内 8191 槽环形缓冲区 + 系统调用写入 + 合成地址部署"三件套,把历史区块哈希变成了可被 witness 打包、可被合约直接查询的状态数据。它不改动BLOCKHASH的任何语义,却为无状态客户端、Rollup 长历史访问和针对近期祖先的链上证明打开了大门,并作为 Pectra 核心执行层变更与 EIP-7709 的前置依赖,持续影响着以太坊状态访问模型与 Gas 定价的演进。对想要深入理解客户端实现细节的读者,建议对照阅读 EIP-2935 规范原文、EIP-4788 与 EIP-7709,三份文档共同构成了"系统合约式历史数据服务"的完整图景。

版权说明:本文内容基于 EIP-2935 规范 编写,规范原文以 CC0 协议放弃版权。

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

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

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

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

立即咨询