EIP-4788: EVM 中的信标链区块根
在 EVM 中公开信标链根
Authors | Alex Stokes (@ralexstokes), Ansgar Dietrichs (@adietrichs), Danny Ryan (@djrtwo), Martin Holst Swende (@holiman), lightclient (@lightclient) |
---|---|
Created | 2022-02-10 |
Requires | EIP-1559 |
Table of Contents
摘要
在相应的执行负载头中提交每个信标链块的哈希树根。
将每个根存储在一个智能合约中。
动机
信标链块的根是密码学累加器,允许对任意共识状态进行证明。 在 EVM 内部公开这些根可以实现对共识层的最小信任访问。 此功能支持各种用例,这些用例改进了质押池、 重新质押结构、智能合约桥、MEV 缓解等方面的信任假设。
规范
常量 | 值 |
---|---|
FORK_TIMESTAMP |
1710338135 |
HISTORY_BUFFER_LENGTH |
8191 |
SYSTEM_ADDRESS |
0xfffffffffffffffffffffffffffffffffffffffe |
BEACON_ROOTS_ADDRESS |
0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02 |
背景
总体思路是,每个执行块都包含父信标块的根。即使在错过时隙的情况下,由于之前的块根没有改变, 我们只需要一个常量空间来表示每个执行块中的这个“预言机”。为了提高此预言机的可用性,一小部分块根历史记录 存储在合约中。
为了限制此构造消耗的存储量,使用了一个环形缓冲区,该缓冲区镜像了共识层上的块根累加器。
块结构和有效性
从执行时间戳 FORK_TIMESTAMP
开始,执行客户端必须使用一个额外的字段扩展header schema:parent_beacon_block_root
。
此根占用 32 个字节,并且对于给定的执行块,它恰好是父信标块的 哈希树根。
因此,header 的最终 RLP 编码为:
rlp([
parent_hash,
0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347, # ommers hash
coinbase,
state_root,
txs_root,
receipts_root,
logs_bloom,
0, # difficulty
number,
gas_limit,
gas_used,
timestamp,
extradata,
prev_randao,
0x0000000000000000, # nonce
base_fee_per_gas,
withdrawals_root,
blob_gas_used,
excess_blob_gas,
parent_beacon_block_root,
])
父信标块根的有效性由共识层保证,就像处理提款一样。
验证块时,执行客户端必须确保块header中的根值与共识客户端提供的值匹配。
对于没有现有父信标块根的创世块,32 个零字节用作根占位符。
信标根合约
信标根合约有两个操作:get
和 set
。输入本身不用于确定要执行的函数,为此使用了 caller
的结果。如果 caller
等于 SYSTEM_ADDRESS
,则要执行的操作是 set
。否则,为 get
。
get
- 调用者以大端格式提供的查询
timestamp
编码为 32 字节。 - 如果输入不是正好 32 字节,则合约必须恢复。
- 如果输入等于 0,则合约必须恢复。
- 给定
timestamp
,合约通过计算模数timestamp % HISTORY_BUFFER_LENGTH
来计算存储timestamp
的存储索引,并读取该值。 - 如果
timestamp
不匹配,则合约必须恢复。 - 最后,与
timestamp
关联的信标根将返回给用户。它存储在timestamp % HISTORY_BUFFER_LENGTH + HISTORY_BUFFER_LENGTH
中。
set
- 调用者将父信标块根作为calldata提供给合约。
- 将
header.timestamp % HISTORY_BUFFER_LENGTH
处的存储值设置为header.timestamp
- 将
header.timestamp % HISTORY_BUFFER_LENGTH + HISTORY_BUFFER_LENGTH
处的存储值设置为calldata[0:32]
伪代码
if evm.caller == SYSTEM_ADDRESS:
set()
else:
get()
def get():
if len(evm.calldata) != 32:
evm.revert()
if to_uint256_be(evm.calldata) == 0:
evm.revert()
timestamp_idx = to_uint256_be(evm.calldata) % HISTORY_BUFFER_LENGTH
timestamp = storage.get(timestamp_idx)
if timestamp != evm.calldata:
evm.revert()
root_idx = timestamp_idx + HISTORY_BUFFER_LENGTH
root = storage.get(root_idx)
evm.return(root)
def set():
timestamp_idx = to_uint256_be(evm.timestamp) % HISTORY_BUFFER_LENGTH
root_idx = timestamp_idx + HISTORY_BUFFER_LENGTH
storage.set(timestamp_idx, evm.timestamp)
storage.set(root_idx, evm.calldata)
字节码
下面分享了确切的合约字节码。
caller
push20 0xfffffffffffffffffffffffffffffffffffffffe
eq
push1 0x4d
jumpi
push1 0x20
calldatasize
eq
push1 0x24
jumpi
push0
push0
revert
jumpdest
push0
calldataload
dup1
iszero
push1 0x49
jumpi
push3 0x001fff
dup2
mod
swap1
dup2
sload
eq
push1 0x3c
jumpi
push0
push0
revert
jumpdest
push3 0x001fff
add
sload
push0
mstore
push1 0x20
push0
return
jumpdest
push0
push0
revert
jumpdest
push3 0x001fff
timestamp
mod
timestamp
dup2
sstore
push0
calldataload
swap1
push3 0x001fff
add
sstore
stop
部署
信标根合约的部署方式与任何其他智能合约一样。通过从所需的部署反向交易来生成一个特殊的合成地址:
{
"type": "0x0",
"nonce": "0x0",
"to": null,
"gas": "0x3d090",
"gasPrice": "0xe8d4a51000",
"maxPriorityFeePerGas": null,
"maxFeePerGas": null,
"value": "0x0",
"input": "0x60618060095f395ff33373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500",
"v": "0x1b",
"r": "0x539",
"s": "0x1b9b6eb1f0",
"hash": "0xdf52c2d3bbe38820fff7b5eaab3db1b91f8e1412b56497d88388fb5d4ea1fde0"
}
请注意,交易中的输入有一个简单的构造函数,它将所需的运行时代码作为前缀。
交易的发送者可以计算为 0x0B799C86a49DEeb90402691F1041aa3AF2d3C875
。从帐户部署的第一个合约的地址为 rlp([sender, 0])
,等于 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02
。这就是 BEACON_ROOTS_ADDRESS
的确定方式。尽管这种合约创建方式没有像 create2 那样与任何特定的 initcode 相关联,但合成地址在密码学上绑定到交易的输入数据(例如,initcode)。
块处理
在处理任何 block.timestamp >= FORK_TIMESTAMP
的执行块开始时(即,在处理任何交易之前),以 SYSTEM_ADDRESS
身份调用 BEACON_ROOTS_ADDRESS
,并使用 32 字节的 header.parent_beacon_block_root
输入,gas 限制为 30_000_000
,以及 0
值。这将触发信标根合约的 set()
例程。这是一个系统操作,因此:
- 调用必须执行完成
- 该调用不计入块的 gas 限制
- 该调用不遵循 EIP-1559 燃烧语义 - 不应作为调用的一部分转移任何值
- 如果
BEACON_ROOTS_ADDRESS
处不存在代码,则调用必须静默失败
客户端可以选择省略显式的 EVM 调用并直接设置存储值。注意:虽然这对于以太坊主网来说是有效的优化,但在非主网情况下,如果使用了不同的合约,则可能会出现问题。
如果此 EIP 在创世块中处于活动状态,则创世header的 parent_beacon_block_root
必须为 0x0
且不得发生系统交易。
理由
为什么不重新利用 BLOCKHASH
?
可以重新利用 BLOCKHASH
操作码来提供信标根,而不是某些执行块哈希。
为了最大限度地减少代码更改,避免对智能合约进行重大更改,并简化到主网的部署,此 EIP 建议保留 BLOCKHASH
不动,并添加具有所需语义的新功能。
信标块根而不是状态根
块根优先于状态根,因此每个新的执行块都有一定量的工作要做。否则,跳过的时隙将需要每个新负载的线性工作量。虽然跳过的时隙在主网上非常罕见,但最好不要在已经不利的条件下增加额外的负载。
使用块根而不是状态根确实意味着证明将需要一些额外的节点,但这种成本可以忽略不计(并且可以在所有消费者之间分摊,例如,使用为每个时隙缓存证明的单例状态根合约)。
为什么是两个环形缓冲区?
第一个环形缓冲区仅跟踪 HISTORY_BUFFER_LENGTH
值的根,因此对于所有可能的timestamp值,将消耗恒定数量的存储。
但是,这种设计会使合约受到攻击,即如果跳过的时隙具有与环形缓冲区长度相同的模值,则会返回旧的根值,
而不是最新的根值。
为了消除此攻击,同时保留固定的内存占用,此 EIP 会跟踪环缓冲区中每个索引的数据对 (parent_beacon_block_root, timestamp)
,并验证 timestamp 是否与最初用于写入根数据时的 timestamp 匹配。鉴于存储槽的固定大小(仅 32 字节),存储一对值的要求需要两个环形缓冲区,而不仅仅是一个。
环形缓冲区的大小
环形缓冲区数据结构的大小可以容纳来自共识层的 8191 个根。使用素数作为环形缓冲区大小可确保在整个环形缓冲区饱和之前不会覆盖任何值,此后,每个值将每次迭代更新一次。这也意味着即使时隙时间发生变化,我们最多仍将使用 8191 个存储时隙。
鉴于当前的主网值,8191 个根大约提供了一天的覆盖范围。这使用户有足够的时间来创建一个针对特定根进行验证的事务,并将该事务包含在链上。
向后兼容性
没有问题。
测试用例
不适用
参考实现
不适用
安全考虑
不适用
版权
通过 CC0 放弃版权和相关权利。
Citation
Please cite this document as:
Alex Stokes (@ralexstokes), Ansgar Dietrichs (@adietrichs), Danny Ryan (@djrtwo), Martin Holst Swende (@holiman), lightclient (@lightclient), "EIP-4788: EVM 中的信标链区块根," Ethereum Improvement Proposals, no. 4788, February 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4788.