从状态中提供历史区块哈希

该EIP提出将过去8191个历史区块哈希存储在系统合约中,以环形缓冲区形式存在,旨在支持无状态客户端,并通过类似EIP-4788的系统调用机制在状态中提供这些哈希。这不改变BLOCKHASH操作码的语义,但为更长的历史查询提供了可能,并详细说明了其实现细节、EVM变更和部署方式。

摘要

在系统合约存储中,将最后 HISTORY_SERVE_WINDOW 个历史区块哈希作为区块处理逻辑的一部分进行存储。此外,本 EIP 对 BLOCKHASH 解析机制(以及因此其范围/成本等)没有影响。

动机

EVM 隐含地假设客户端拥有最近的区块(哈希)。考虑到无状态客户端的前景,这个假设并非面向未来的。将区块哈希包含在状态中将允许把这些哈希捆绑在提供给无状态客户端的见证中。这在 MPT 中已经可能,并且在 Verkle 之后会变得更高效。

扩展 BLOCKHASH 可以服务的区块范围(BLOCKHASH_SERVE_WINDOW)将是一个语义上的改变。通过这个合约存储来扩展它将允许一个软过渡。Rollup 可以通过直接查询此合约来受益于更长的历史窗口。

这种方法的一个附带好处可能是,它允许直接针对当前状态构建/验证与最后 HISTORY_SERVE_WINDOW 个祖先相关的证明。

规范

参数
BLOCKHASH_SERVE_WINDOW 256
HISTORY_SERVE_WINDOW 8191
SYSTEM_ADDRESS 0xfffffffffffffffffffffffffffffffffffffffe
HISTORY_STORAGE_ADDRESS 0x0000F90827F1C53a10cb7A02335B175320002935

本 EIP 规定将最后 HISTORY_SERVE_WINDOW 个区块哈希存储在长度为 HISTORY_SERVE_WINDOW 的环形缓冲区存储中。请注意,HISTORY_SERVE_WINDOW > BLOCKHASH_SERVE_WINDOW(后者保持不变)。

区块处理

在处理任何激活本 EIP 的区块的开始(即在处理任何交易之前),以 SYSTEM_ADDRESS 的身份调用 HISTORY_STORAGE_ADDRESS,输入为 block.parent.hash 的 32 字节数据,gas 限制为 30_000_000,值为 0。这将触发历史合约的 set() 例程。这是一个系统操作,遵循 EIP-4788 的相同约定,因此:

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

注意:客户端也可以选择直接写入合约存储,但 EVM 调用合约仍是首选。请参阅理由获取更多信息。

请注意,EIP 激活后需要 HISTORY_SERVE_WINDOW 个区块才能完全填充环形缓冲区。合约将只包含分叉区块的父哈希,不包含此之前的任何哈希。

EVM 更改

BLOCKHASH 操作码的语义保持不变。

区块哈希历史合约

历史合约有两个操作:getsetset 操作仅在 caller 等于 SYSTEM_ADDRESS 时调用,如 EIP-4788 所述。否则执行 get 操作。

get

它用于从 EVM 中查找区块哈希。

  • 调用者提供其查询的区块号,采用大端编码。
  • 如果 calldata 不是 32 字节,则回滚。
  • 对于超出 [block.number-HISTORY_SERVE_WINDOW, block.number-1] 范围的任何请求,则回滚。

set

  • 调用者提供 block.parent.hash 作为 calldata 给合约。
  • (block.number-1) % HISTORY_SERVE_WINDOW 处的存储值设置为 calldata[0:32]

字节码

可用于历史合约的精确 EVM 汇编:

// https://github.com/lightclient/sys-asm/blob/f1c13e285b6aeef2b19793995e00861bf0f32c9a/src/execution_hash/main.eas
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

部署

通过从期望的部署交易逆向推导,生成一个特殊的合成地址:

{
  "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 具有一个简单的构造函数前缀,后跟所需的运行时代码。

交易的发送者可以计算为 0x3462413Af4609098e1E27A490f554f260213D685。从该账户部署的第一个合约的地址是 rlp([sender, 0]),等于 0x0000F90827F1C53a10cb7A02335B175320002935。这就是 HISTORY_STORAGE_ADDRESS 的确定方式。尽管这种合约创建方式不像 create2 那样与任何特定的 initcode 绑定,但合成地址与交易的输入数据(例如 initcode)通过密码学方式绑定。

一些激活场景:

  • 如果分叉在创世区块激活,则创世状态不写入任何历史记录,并且在区块 1 开始时,创世哈希将作为正常操作写入槽 0
  • 如果分叉在区块 1 激活,则只有创世哈希会写入槽 0
  • 如果分叉在区块 32 激活,则区块 31 的哈希将写入槽 31。其他所有槽都将是 0

EIP-161 处理

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

Gas 成本

区块开始时的系统更新,即 process_block_hash_history(或通过 SYSTEM_ADDRESS 调用合约),将不会根据 EIP-2929 规则预热 HISTORY_STORAGE_ADDRESS 账户或其存储槽。因此,对合约的首次调用将支付预热账户及其访问的存储槽的费用。为了进一步澄清,对 HISTORY_STORAGE_ADDRESS 的任何合约调用都将遵循正常的 EVM 执行语义。

由于 BLOCKHASH 语义没有改变,本 EIP 对 BLOCKHASH 机制和成本没有影响。

理由

之前曾提出过非常相似的想法。本 EIP 是一个简化,移除了两个不必要的复杂性来源:

  1. 拥有树状结构(多层)而不是单一列表
  2. 用 EVM 代码编写 EIP
  3. 对历史的深度访问进行序列无边界的哈希存储

然而,在权衡利弊之后,我们决定只使用有限的环形缓冲区来服务必需的 HISTORY_SERVE_WINDOW,因为 EIP-4788 和信标状态累加器允许(尽管稍微复杂一些)针对合并后的任何祖先进行证明。

第二个担忧是如何在分叉后最好地转换 BLOCKHASH 解析逻辑:

  1. 要么等待 HISTORY_SERVE_WINDOW 个区块,以便所有相关的历史记录都持久化
  2. 在分叉区块上存储所有最后 HISTORY_SERVE_WINDOW 个区块哈希。

我们选择前者。它极大地简化了逻辑。合约的引导大约需要一天时间。鉴于这是一种访问历史的新方式,并且没有合约依赖于它,这被认为是一个有利的权衡。

插入父区块哈希

客户端通常有两种选择将父区块哈希插入状态:

  1. 执行对 HISTORY_STORAGE_ADDRESS 的系统调用,并让其处理状态存储。
  2. 避免 EVM 处理,直接写入状态 trie。

后者的选项如下:

def process_block_hash_history(block: Block, state: State):
    if block.timestamp >= FORK_TIMESTAMP: # FORK_TIMESTAMP 应该在 EIP 外部定义
        state.insert_slot(HISTORY_STORAGE_ADDRESS, (block.number-1) % HISTORY_SERVE_WINDOW , block.parent.hash)

在 Verkle 分叉之前,建议使用第一种选项,以与 EIP-4788 保持一致,并解决 EIP 激活但历史合约未部署的配置错误网络的问题。如果过滤系统合约代码块被认为过于复杂,可以在 Verkle 分叉时重新考虑该建议。

环形缓冲区的大小

环形缓冲区数据结构的尺寸设计为可容纳 8191 个哈希。在其他系统合约中,选择素数环形缓冲区大小是因为使用素数作为模数可以确保在整个环形缓冲区饱和之前不会覆盖任何值,并且此后,每个值在每次迭代中都会更新一次,无论是否存在一些缺失的槽或槽时间是否改变。然而,在本 EIP 中,区块号是模运算中的值,并且它每次迭代只增加 1。这意味着我们可以确信环形缓冲区将始终保持饱和。

为了与其他系统合约保持一致,我们决定保留 8191 的缓冲区大小。考虑到当前主网的值,8191 个根提供了大约一天的覆盖范围。这也为用户提供了充足的时间,使用针对特定哈希的验证进行交易,并将交易包含在链上。

向后兼容性

本 EIP 对区块验证规则集引入了不兼容的更改。但这些更改都不会破坏与当前用户活动和体验相关的任何内容。

测试用例

EIP-2935 执行规范测试

安全考虑

拥有具有热更新路径(分支)的合约(系统或其他)存在“分支”投毒攻击的风险,攻击者可以在这些热路径(分支)周围撒上少量 ETH。但已被认为攻击成本将显著升级,以致于难以对状态根更新造成任何有意义的减速。

  • 原文链接: github.com/nerolation/EI...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Nerolation
Nerolation
江湖只有他的大名,没有他的介绍。