EIP-2935: 从状态提供历史区块哈希
将最近的 8191 个区块哈希作为系统合约的存储槽存储和提供,以允许无状态执行
Authors | Vitalik Buterin (@vbuterin), Tomasz Stanczak (@tkstanczak), Guillaume Ballet (@gballet), Gajinder Singh (@g11tech), Tanishq Jasoria (@tanishqjasoria), Ignacio Hagopian (@jsign), Jochem Brouwer (@jochem-brouwer), Sina Mahmoodi (@s1na) |
---|---|
Created | 2020-09-03 |
Table of Contents
摘要
将最近的 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
,输入为 32 字节的 block.parent.hash
,gas 限制为 30_000_000
,值为 0
。这将触发历史合约的 set()
例程。这是一个系统操作,遵循与 EIP-4788 相同的约定,因此:
- 调用必须执行完成
- 调用不计入区块的 gas 限制
- 调用不遵循 EIP-1559 的燃烧语义 - 不应作为调用的一部分转移任何价值
- 如果
HISTORY_STORAGE_ADDRESS
处不存在代码,则调用必须静默失败
注意:或者,客户端可以选择直接写入合约的存储,但 EVM 调用合约仍然是首选。有关更多信息,请参阅理由。
请注意,在 EIP 激活后的 HISTORY_SERVE_WINDOW
个区块后,才会完全填满环形缓冲区。合约将仅包含分叉区块的父哈希,而不包含分叉之前的哈希。
EVM 变更
BLOCKHASH
操作码语义与之前相同。
区块哈希历史合约
历史合约有两个操作:get
和 set
。仅当 caller
等于 EIP-4788 中的 SYSTEM_ADDRESS
时才调用 set
操作。否则,将执行 get
操作。
get
它从 EVM 用于查找区块哈希。
- 调用者以大端编码提供他们正在查询的区块号。
- 如果 calldata 不是 32 字节,则 revert。
- 对于 [block.number-
HISTORY_SERVE_WINDOW
, block.number-1] 范围外的任何请求,revert。
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"
}
请注意,交易中的输入具有一个简单的构造函数,该构造函数位于所需的运行时代码之前。
交易的发送者可以计算为 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 是一种简化,消除了两个不必要的复杂性来源:
- 具有多层树状结构而不是单个列表
- 以 EVM 代码编写 EIP
- 对历史记录进行深度访问的哈希的串行无界存储
但是,在权衡利弊之后,我们决定仅使用有限的环形缓冲区来服务所需的 HISTORY_SERVE_WINDOW
,因为 EIP-4788 和信标状态累加器允许(尽管有点复杂)针对合并后的任何祖先进行证明。
第二个关注点是如何最好地通过以下方式过渡分叉后的 BLOCKHASH 解析逻辑:
- 要么等待
HISTORY_SERVE_WINDOW
个区块以使整个相关历史记录保持不变 - 要么在分叉区块上存储所有最后的
HISTORY_SERVE_WINDOW
个区块哈希。
我们选择前者。它极大地简化了逻辑。引导合约大约需要一天的时间。鉴于这是一种访问历史记录的新方式,并且没有合约依赖于它,因此被认为是良好的权衡。
插入父区块哈希
客户端通常有两种选择将父区块哈希插入状态:
- 对
HISTORY_STORAGE_ADDRESS
执行系统调用,并让其处理状态中的存储。 - 避免 EVM 处理并直接写入状态树。
后一种选择如下:
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 的缓冲区大小。考虑到当前的 mainnet 值,8191 个根大约提供一天的覆盖范围。这也使用户有足够的时间来进行针对特定哈希的验证的交易,并将该交易包含在链上。
向后兼容性
此 EIP 引入了对区块验证规则集的向后不兼容的更改。但是,这些更改都不会破坏与当前用户活动和体验相关的任何内容。
测试用例
安全注意事项
拥有具有热更新路径(分支)的合约(系统或其他)会带来“分支”中毒攻击的风险,攻击者可以在这些热路径(分支)周围散布少量的 eth。但是,已经认为攻击的成本将大大增加,从而导致状态根更新的任何有意义的减慢。
版权
版权及相关权利通过 CC0 放弃。
Citation
Please cite this document as:
Vitalik Buterin (@vbuterin), Tomasz Stanczak (@tkstanczak), Guillaume Ballet (@gballet), Gajinder Singh (@g11tech), Tanishq Jasoria (@tanishqjasoria), Ignacio Hagopian (@jsign), Jochem Brouwer (@jochem-brouwer), Sina Mahmoodi (@s1na), "EIP-2935: 从状态提供历史区块哈希," Ethereum Improvement Proposals, no. 2935, September 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-2935.