本文介绍了一个名为 libssz 的全新 Rust 库,旨在为以太坊共识层和执行层提供快速且支持 no_std 环境的 SSZ(Simple Serialize)序列化和 Merkle 化功能。它通过优化编码、解码及 Merkle 化过程,显著提升了性能,并解决了现有库在 no_std 兼容性方面的不足,支持 EIP-8025 等新的以太坊提案。
SSZ (Simple Serialize) 是以太坊共识层的序列化和 Merkleization 格式。共识客户端以 SSZ 格式编码信标状态和区块,并使用 SSZ 哈希树根对数据计算 Merkle 证明。直到最近,SSZ 只是共识层关注的问题,因为执行客户端不需要它。
目前存在的一些 Rust SSZ 库有:
Lighthouse (ethereum_ssz) |
ssz_rs | |
|---|---|---|
| 性能 | 快速,经过生产环境实战检验 | 在复合类型上慢 100-300 倍 |
no_std |
否。依赖 std,通过 ethereum_hashing 引入 ring (C 库) |
是。no_std + alloc |
| 有界类型 | typenum |
typenum |
Lighthouse 是 Rust SSZ 的参考实现,也是大多数项目使用的。ssz_rs 支持 no_std,但在复合类型上慢 100-300 倍,这意味着你无法在生产环境中使用它。直到最近,没有人同时需要快速且支持 no_std 的 SSZ,所以没有人构建它。
两项提案将 SSZ 引入执行层。
一项将二进制 SSZ 添加到 Engine API 的提案 用原始 SSZ 字节而非 JSON 通过 HTTP 传输。CL 已经有来自信标区块的 SSZ 格式的执行Payload;二进制传输使其能够将原始字节无需转换地转发到 EL。随着 Blob 数量的增长,JSON 成为瓶颈(十六进制编码使每个 128 KB 的 Blob 大小加倍),切换到 SSZ 允许客户端扩展 MAX_BLOB_COMMITMENTS_PER_BLOCK。
EIP-8025 (可选执行证明) 扩展了 Engine API,增加了用于生成、提交和验证 zkVM 执行证明的端点,允许验证者通过加密证明支持执行有效性进行证明。这些证明的公共输入是 tree_hash_root(NewPayloadRequest),一个 SSZ Merkle 根。执行客户端需要对 ExecutionPayload、NewPayloadRequest 及其子容器等结构化共识类型计算哈希树根。
RISC-V 目标标准 要求符合标准的 zkVM 在没有标准库的情况下编译 guest 代码。目前许多 zkVM 通过分叉编译器来解决这个问题,但 ZKsync 的 Airbender 强制执行 no_std。随着团队采纳该标准,未来会有更多项目效仿。
如果你正在构建二进制 SSZ 传输,你需要一个快速的 SSZ 库。如果你正在 zkVM 内部证明 EIP-8025 执行,你需要一个既快速又支持 no_std 的库。
jsign 在为 ere-guests 实现 EIP-8025 guest 程序时,曾尝试使 Lighthouse 的 SSZ 在 no_std 中工作 ( ere-guests#8):
我深入研究了一下,进展很大但至少需要这些补丁:
ethereum_ssz = { git = "https://github.com/han0110/ethereum_ssz", branch = "feature/no-std" } ethereum_ssz_derive = { git = "https://github.com/han0110/ethereum_ssz", branch = "feature/no-std" } ethereum_serde_utils = { git = "https://github.com/han0110/ethereum_serde_utils", branch = "feature/no-std" } tree_hash = { git = "https://github.com/jsign/tree_hash", rev = "2263d50" } tree_hash_derive = { git = "https://github.com/jsign/tree_hash", rev = "2263d50" } ssz_types = { git = "https://github.com/jsign/ssz_types", rev = "679b7ba" }
六个分叉的 crate 涉及三位维护者,但他仍然没有完成。在 PR 中,他实现了 EIP-8025 guest 程序设计,jsign 写道:
我花了一段时间修补 crate,但我已经需要修补 6 个 crate,这让我感到非常不舒服。
Lighthouse 团队从未为 no_std 构建他们的 SSZ 生态系统。ethereum_hashing 在没有功能门控的情况下引入 ring (一个 C 库),ssz_types 依赖于带有 std 功能的 typenum,而且依赖图足够深,你需要协调多个仓库的补丁才能使其完全 no_std 兼容。
SSZ 编码有两条路径:固定大小类型(整数、字节数组、只有固定字段的结构体)和可变大小类型(列表、至少包含一个可变字段的容器)。固定路径是直接内存复制 (memcpy)。可变路径需要支付偏移量簿记和中间缓冲区的开销。
Lighthouse 为可变数据分配一个单独的缓冲区,然后将其复制到最终输出中。每个可变字段都会经过两次写入。对于一个包含 21 个字段的 BeaconState,这会累积起来。
我们编写了一个 ContainerEncoder,它直接将可变数据写入输出缓冲区。固定字段被原地修补到一个预分配的区域。每个字段一次写入,零中间分配。对于全固定容器(BeaconBlockHeader、Fork、Checkpoint),derive 宏完全跳过编码器,生成直接的字段逐个追加。无堆内存,无偏移量跟踪。
另一个瓶颈是逐元素迭代。Lighthouse 通过对每个元素调用 ssz_append 来编码一个 Vec<u64>。在小端平台,Vec<u64> 在内存中的布局已符合 SSZ 预期。我们对整个切片使用单次 memcpy。[u8; N] 数组和解码也是如此。
Merkleization 也有类似的问题:Lighthouse 对零哈希表使用 lazy_static(首次访问时计算 64 个 SHA-256 哈希)。我们通过 build.rs 在构建时计算它们。无运行时初始化,无同步。
这些决定塑造了 crate 结构:
libssz-derive ···→ libssz-merkle ──→ libssz ←── libssz-types
(过程宏) (Merkleization) (核心) (有界集合)
libssz: SszEncode / SszDecode trait,ContainerEncoder / ContainerDecoder,原始实现libssz-types: 使用 const 泛型而非 typenum 的有界集合(SszVector、SszList、SszBitvector、SszBitlist)libssz-merkle: HashTreeRoot,merkleize,构建时零哈希libssz-derive: #[derive(SszEncode, SszDecode, HashTreeRoot)] 带有全固定检测该结构源于这些决定:Merkleization 是独立的,因为你可能不需要它(例如,仅用于传输的用例)。derive 宏在编译时检测所有固定容器,并生成不同的代码路径。有界类型使用 const 泛型(SszVector<T, 1024>)而非 typenum(Vector<T, U1024>),因为 SSZ 规范的边界始终是字面常量。
一切都为 no_std + alloc 构建。CI 验证每次提交在 thumbv7m-none-eabi 和 riscv64imac-unknown-none-elf 上的编译。
我们根据官方的 以太坊共识规范测试向量 (v1.6.1) 验证 libssz:涵盖所有 9 个分叉 (Phase0, Altair, Bellatrix, Capella, Deneb, Electra, Fulu, Gloas, EIP-7805) 的 62,489 个测试用例。
这包括:
BeaconState、BeaconBlock、Attestation 等)在主网参数下每个测试用例都验证解码、重新编码的往返正确性以及哈希树根的正确性。
我们对 Lighthouse 和 ssz_rs 运行 19 个差分模糊测试目标,涵盖往返测试、对抗性偏移、深度嵌套和宽联合体。这些测试在 CI 中每晚运行。
我们对照 Lighthouse (ethereum_ssz + tree_hash) 和 ssz_rs v0.9 进行了基准测试,使用 --release 和 thin LTO 编译,在 AMD Ryzen 9 9950X3D (x86_64) 上运行。
| 类型 | libssz | Lighthouse | ssz_rs | 对比 Lighthouse | 对比 ssz_rs |
|---|---|---|---|---|---|
BeaconBlockHeader |
10.1 ns | 84.2 ns | 1.71 µs | 8.4x | 170x |
Vec<u64> (100K) |
9.20 µs | 35.8 µs | 2.36 ms | 3.9x | 257x |
BeaconState (100K val) |
450 µs | 773 µs | 201 ms | 1.7x | 446x |
BeaconState (1M val) |
10.1 ms | 20.3 ms | 1.58 s | 2.0x | 156x |
| 类型 | libssz | Lighthouse | ssz_rs | 对比 Lighthouse | 对比 ssz_rs |
|---|---|---|---|---|---|
BeaconBlockHeader |
8.96 ns | 7.08 ns | 196 ns | 0.8x | 22x |
Vec<u64> (100K) |
9.24 µs | 59.7 µs | 31.8 µs | 6.5x | 3.4x |
BeaconState (100K val) |
313 µs | 908 µs | 20.5 ms | 2.9x | 66x |
BeaconState (1M val) |
9.27 ms | 13.9 ms | 172 ms | 1.5x | 19x |
在所有测试的验证者数量下,libssz 在 BeaconState 编码和解码方面均快于 Lighthouse。在原始类型上,libssz 在 u64 编码上达到 3.2 倍,在 [u8; 32] 编码上达到 3.2 倍。
包括 ARM (Apple M3 Max) 基准测试和哈希树根比较的完整结果请参见 README。所有基准测试均可通过 cargo bench --bench differential 重现。
ethlambda 是 LambdaClass 用 Rust 编写的一个极简主义的 Lean 共识客户端。PR #242 从 ethereum_ssz 迁移到 libssz:跨 34 个文件增加了 550 行/删除了 460 行。API 接口与 Lighthouse 的 SSZ 足够接近,团队可以无缝替换。
ethrex 正在 PR #6361 中实现 EIP-8025。该团队使用 libssz 处理 SSZ 容器(ExecutionPayload、NewPayloadRequest、NewPayloadRequestHeader)并计算作为执行证明公共输入的 hash_tree_root。
该实现支持多种 zkVM 后端:SP1、RISC Zero、OpenVM 和 ZisK。guest 程序将 (NewPayloadRequest [SSZ], ExecutionWitness [rkyv]) 作为输入,输出 33 字节(一个 32 字节的 SSZ 根加上一个 1 字节的有效性布尔值)。libssz 在 no_std 中工作,因此 ethrex 不需要维护一堆分叉的依赖。
ere-guests 在 PR #26 中从 ethereum_ssz 切换到 libssz。此次迁移使 stateless-validator-common 完全支持 no_std,从而解除了 Airbender 作为证明后端的阻碍。用于 CI 集成测试的 15 个主网区块仍然符合其固定预期。jsign 测量到 ziskemu 内部的 hash_tree_root 部分有 2 倍的加速,这转换为整体区块证明时间 1.05 倍的加速。
libssz v0.1.0 已发布在 crates.io 上。
cargo add libssz libssz-derive libssz-merkle libssz-types
对于 no_std 目标(zkVM、WASM、嵌入式):
cargo add libssz --no-default-features --features alloc
cargo add libssz-types --no-default-features --features alloc
cargo add libssz-merkle --no-default-features --features alloc
cargo add libssz-derive
完整文档、架构指南和示例:github.com/lambdaclass/libssz。采用双重 Apache-2.0 / MIT 许可证。
- 原文链接: blog.lambdaclass.com/lib...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!