EIP-7797 深度解析:通过定制 SHA-256 为 hash_tree_root 带来双倍性能
【免费下载链接】EIPsThe Ethereum Improvement Proposal repository项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs
导读
EIP-7797(Double speed for hash_tree_root)是面向以太坊共识层(Consensus Layer)的 Core 类标准提案,其核心思路是:利用hash_tree_root中所有输入消息长度恒定为 512 位的特性,定义一个跳过消息预处理步骤的定制版 SHA-256——SHA-256-512,从而将底层哈希吞吐量提升一倍。本文将以 EIPS/eip-7797.md 为主体,结合本仓库中 SSZ Merkleization 的参考实现(如 assets/eip-4881/eip_4881.py)与相关联的 EIP-7495、EIP-7688、EIP-7916 等提案,完整解读其背景动机、算法规范、性能收益、兼容性影响与安全边界。读完本文,你将理解为什么 SHA-256 在hash_tree_root场景下"白做"了约一半工作、SHA-256-512 如何把这些工作省掉,以及该方案与 Progressive SSZ 类型改造之间的关系。
背景:为什么哈希是共识层的性能瓶颈
在以太坊共识层实现中,哈希(Hashing)是占主导地位的性能瓶颈。随着验证者(validator)数量的增长,状态根(state root)的计算、区块签名验证、证明校验等操作都在持续消耗 CPU。为了支撑大规模验证者数量,优化哈希性能至关重要(见 EIPS/eip-7797.md Motivation 一节)。
共识层的所有哈希都基于hash_tree_root——一种将数据切分成 chunk、再把相邻的两个 chunk 递归组合并哈希为单个父 chunk、直到只剩一个根 chunk 的 Merkle 化(merkleization)机制。其典型实现可以在本仓库 EIP-4881 的参考实现 中看到:MerkleTree.Node.get_root()直接对左右子树根做sha256(left + right)(assets/eip-4881/deposit_snapshot.py),zerohashes预计算也依赖sha256(h + h)逐层生成(assets/eip-4881/eip_4881.py)。这正是 EIP-7797 想优化的热路径:每次树节点合并都是一次对两个 32 字节子根(共 64 字节 = 512 位)的 SHA-256 计算。
EIP-7797 观察到一个关键事实:
- SHA-256 对可变长度输入消息,恰好产生 256 位(32 字节)输出;
- 而
hash_tree_root会把所有输入 chunk 都填充到恰好 256 位,因此hash_tree_root场景下 SHA-256 的实际输入消息长度永远是恰好 512 位。
既然输入长度恒定,就可以针对这一先验知识对 SHA-256 做定制化改造,在保留安全属性的前提下把性能翻倍。
SHA-256 的预处理:被"浪费"的那一半工作
标准的 SHA-256 在正式计算哈希之前,要先对输入消息做预处理(preprocessing):在消息末尾追加一个1位,随后补足若干0位,最后附上一个大端序的uint64表示输入消息的位长度。0位的数量被选择为使填充后消息总大小成为 512 位的最小倍数。
在hash_tree_root场景下,输入消息大小恰好为 512 位,预处理后得到的填充消息如下(来自 EIPS/eip-7797.md Specification):
0 1 2 3 4 5 6 7 8 9 A B C D E F +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 0x00 | | 0x10 | Input | 0x20 | message | 0x30 | | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 0x40 |80|00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| 0x50 |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| 0x60 |00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00| 0x70 |00 00 00 00 00 00 00 00|00 00 00 00 00 00 02 00| +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+解读这张图:
0x00~0x30:原始的 512 位(64 字节)输入消息;0x40首字节80:追加的1位(其后跟 7 个0位);0x40~0x6F:用于补齐的0位;0x70末尾 8 字节00 00 00 00 00 00 02 00:大端序uint64,值为0x200= 512,即输入消息的位长度。
注意:由于 512 位输入恰好是 SHA-256 块大小的整数倍,填充后消息变成了整整 1024 位(两个 512 位块)。
两个消息块:一块承载数据,一块完全是静态数据
SHA-256 按 512 位消息块(message block)为单位进行压缩运算。在hash_tree_root的 512 位输入场景下,预处理后形成两个消息块:
- 第一个块:包含完整的输入消息(真实数据);
- 第二个块:完全由预处理步骤产生的静态数据(
80+ 零填充 + 位长度字段)。
第二个 512 位块不提供任何熵(entropy),它唯一的用途是在通用 SHA-256 中区分"共享共同前缀、仅在尾部0位数量上不同"的输入消息。而hash_tree_root使用固定消息长度,根本不需要这种区分。
此外,密码学上 SHA-256 对"恰好能塞进单个消息块的填充后消息"同样被认为是安全的。因此,可以安全地跳过第二个静态块的压缩运算——这就是性能翻倍的来源:原本每 64 字节输入要做两轮 512 位块压缩,现在只需做一轮。
SHA-256-512:跳过预处理、限定 512 位输入的新算法
EIP-7797 据此定义一个新算法 SHA-256-512:
- 它是修改版的 SHA-256,跳过输入消息预处理;
- 它仅限于恰好 512 位的输入;
- 输入消息按原样作为单个 512 位 SHA-256 消息块处理。
在规范层面,本 EIP 遵循 RFC 2119 / RFC 8174 的 MUST / SHOULD / MAY 措辞约定(见 EIPS/eip-7797.md Specification 开头):
- 对于每一个正在使用的复合 SSZ 类型(composite SSZ type),实现方**必须(SHALL)**支持一个新的、功能完全相同的类型,区别仅在于使用 SHA-256-512 而非常规 SHA-256 进行哈希;
- 从引入本 EIP 的硬分叉开始,基于 SHA-256-512 的复合 SSZ 类型**应当(SHOULD)**优先于现有的 SHA-256 类型被使用。
值得注意的是,SHA-256-512只改动 SHA-256 的预处理环节,其核心的 512 位消息块压缩函数(compression function)保持不变。因此现有的 SHA-256 硬件加速方案依然完全可用——硬件加速通常只实现消息块函数,而这部分在本 EIP 中没有任何改变。
共识类型的历史数据处理
切换到新哈希后,存在两类需要特殊处理的历史数据场景(见 EIPS/eip-7797.md Consensus types 一节):
- 历史对象回算:某些覆盖历史对象的用例可能(MAY)需要转换回历史数据类型、并用原始的 SHA-256 类型重新哈希,才能恢复其历史根。典型例子包括
compute_signing_root对历史数据签名,以及BeaconState.latest_block_header这类可能引用先前分叉数据的字段——例如BeaconBlockHeader这类历史结构体,需要新的逻辑来正确选择哈希算法; - 保持原算法:某些对象(如
DepositData、VoluntaryExit)**可能(MAY)**继续依赖现有的 SHA-256 逻辑,不必迁移到 SHA-256-512。
这暗示迁移的落地形态不是"一刀切替换所有哈希",而是按类型区分新旧算法,确保历史根(historical root)的可恢复性。
收益量化:哈希吞吐翻倍意味着什么
关于性能收益,EIPS/eip-7797.md Rationale 给出了量化说明:
- 将底层哈希算法吞吐量翻倍,允许在相同硬件上支撑更多验证者,或者把省下的 CPU 时间用于其他任务;
- 即便在跨计算缓存了很少变化的中间哈希(如
BeaconState的validators列表),并且使用了针对树结构进一步优化的硬件加速 SHA-256 实现(例如prysmaticlabs/hashtree这类库),共识层状态转换函数中的状态根校验步骤仍可消耗约 25% 的 CPU 时间(Holesky 测试网,约 170 万验证者); - 其中大部分开销来自频繁变化的每个验证者结构体,例如
EpochParticipationFlags(epoch 参与标志)列表。
从源码结构可以推断,这正是BeaconState中validators、balances、previous_epoch_participation、current_epoch_participation等字段在 Merkle 树中层层上推时所产生的大量 SHA-256 调用——每层树节点合并都是一次 512 位输入的哈希。SHA-256-512 让每一次这样的合并只做一轮块压缩而不是两轮,直接把该热路径的哈希开销减半。
面向未来的哈希算法迁移:提前"排雷"
EIP-7797 的另一个动机是为未来更换更 ZK 友好(zero-knowledge friendly)的哈希算法铺路:
- 未来如果要把哈希算法换成更 ZK 友好的算法,需要识别"哪些位置的历史对象必须用历史哈希算法进行哈希",并引入新的复合 SSZ 类型;
- 从 SHA-256 切换到 SHA-256-512 相当于提前完成这项工作:未来任何哈希算法变更都只需在这些已知位置上扩展即可;
- 多次切换哈希算法的总工作量,与只切换一次相当。
也就是说,SHA-256-512 充当了一次"干跑"——把"历史对象用旧哈希、新对象用新哈希"的迁移框架和类型机制先建立起来,后续真正换哈希时可以直接复用。
向后兼容性
EIPS/eip-7797.md Backwards Compatibility 一节明确了影响范围:
- 验证共识层数据的智能合约与客户端应用需要更新,才能与使用 SHA-256-512 哈希的数据保持兼容;
- 对于
BeaconBlockHeader等历史结构体,可能需要新的逻辑来正确选择哈希算法(历史数据用 SHA-256,新数据用 SHA-256-512); - 不受影响的部分:SSZ 序列化、广义索引(generalized indices)以及各对象字段的语义均保持不变。
这一点与 EIP-7688(Forward compatible consensus data structures,将共识 SSZ 数据结构迁移到ProgressiveContainer)形成呼应:EIP-7688 只改变 Merkle 树形态(merkleization),不改变序列化;EIP-7797 则只改变哈希算法,同样不触碰序列化与字段语义。两者都是"改树不改序列化"类型的核心层变更,因此都存在"历史数据必须用旧逻辑校验"的兼容性要求。
安全考量:跨算法碰撞的理论边界
EIP-7797 在 Security Considerations 中主动披露了一个重要的理论碰撞场景:
某些 512 位数据的 SHA-256-512 哈希,可能与较短数据的常规 SHA-256 哈希发生碰撞。具体示例:
- 任意共同的 440 位前缀:
COMMON_PREFIX := 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233343536 SHA-256-512(COMMON_PREFIX ++ 0x80 ++ 0x00000000000001b8) == 463eb28e72f82e0a96c0a4cc53690c571281131f672aa229e0d45ae59b598b59SHA-256(COMMON_PREFIX) == 463eb28e72f82e0a96c0a4cc53690c571281131f672aa229e0d45ae59b598b59
为什么会出现这种现象?因为 SHA-256-512 跳过了预处理,而常规 SHA-256 对COMMON_PREFIX(440 位)的预处理恰好会追加0x80和位长度0x1b8(440),形成与前者相同的 512 位输入——于是两个算法的输出一致。
不过这一理论碰撞在 SSZ 语境下并不可行:
- SSZ 从未哈希过 512 位以外大小的数据,因此基于 SHA-256 的 SSZ 哈希与基于 SHA-256-512 的 SSZ 哈希不会发生碰撞;
- 规范同时明确:SHA-256-512 不应(SHOULD NOT)被用于 SSZ Merkleization 以外的任何目的——它不是一个通用哈希算法,而是一个为 SSZ 定制的专用原语。
仓库内证据:SHA-256 在 Merkle 化中的实际形态
本仓库虽然没有 EIP-7797 专用的参考实现目录,但 EIP-4881 的参考实现完整展示了"以 SHA-256 为原语的 Merkle 化"代码形态,可作为理解热路径的实证:
- 底层包装:
def sha256(x) -> Hash32: return SHA256(x).digest()(assets/eip-4881/eip_4881.py),所有 Merkle 树节点合并都经由它; - 节点合并:
Node.get_root()返回sha256(self.left.get_root() + self.right.get_root())(assets/eip-4881/deposit_snapshot.py)——左右子根各 32 字节,拼接后恰好 64 字节 = 512 位,正是 SHA-256-512 的目标输入长度; - 零哈希预计算:
zerohashes[i] = sha256(zerohashes[i-1] + zerohashes[i-1])(assets/eip-4881/eip_4881.py); - 根混合长度:
get_root()返回sha256(self.tree.get_root() + to_le_bytes(self.mix_in_length))(assets/eip-4881/deposit_snapshot.py)——同样是 512 位输入。
由此可见,共识层 Merkle 化中的每一次sha256(a + b)调用,输入都是两个 32 字节值拼接成的 512 位。把这些调用替换为 SHA-256-512(单块压缩),即可直接获得近一倍的吞吐提升,且无需改动 Merkle 树结构本身。
与 SSZ 类型演进提案的关系
EIP-7797 的"为每个复合 SSZ 类型引入并行的新类型"思路,与仓库内另外几份由同一作者群推进的 SSZ 类型提案形成清晰的路线图:
- EIP-7916(SSZ ProgressiveList):引入渐进式 Merkle 树形态,列表按需生长、消除容量上限与不必要的零填充哈希;
- EIP-7495(SSZ ProgressiveContainer):引入字段级稳定广义索引的前向兼容容器;
- EIP-7688(Forward compatible consensus data structures):把共识数据结构整体迁移到上述渐进式类型。
从源码结构可以推断,这些提案与 EIP-7797 共享同一个目标:在不改变 SSZ 序列化与字段语义的前提下,重塑共识层的哈希路径——前者优化"哈希多少次、树长什么样",后者优化"每一次哈希多快"。若未来 SHA-256-512 落地,共识层状态根校验(Holesky 约 170 万验证者下约占 25% CPU)有望显著下降,为更大规模验证者集或 ZK 化共识留出空间。
版权说明
EIP-7797 的版权及相关权利已通过 CC0 许可 放弃,其规范文本可自由引用与实现。
【免费下载链接】EIPsThe Ethereum Improvement Proposal repository项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考