kona-preimage 源码解读:Optimism Fault Proof 预镜像预言机的高层 Rust API 设计
【免费下载链接】optimismOptimism is Ethereum, scaled.项目地址: https://gitcode.com/GitHub_Trending/op/optimism
本文以 OP Stack 官方 Rust 实现 Kona 仓库中的kona-preimagecrate(位于 rust/kona/crates/proof/preimage)为核心,系统讲解 Fault Proof 系统中Preimage Oracle(预镜像预言机)的高层接口设计:客户端程序如何通过 32 字节 Key 向主机请求数据、主机如何通过 Hint 通道预取数据并回传,以及这套协议在no_std与异步两种运行环境下的具体实现。读完本文,你将掌握PreimageKey的编码规范、OracleReader/OracleServer与HintWriter/HintReader的交互流程、Channel抽象与原生实现,以及VerifyingPreimageFetcher的防篡改校验机制,并能够基于这套 trait 体系自行接入新的数据源或移植到自定义证明客户端。
一、定位:为什么 Fault Proof 需要预镜像预言机
在 Optimism 的 Fault Proof(故障证明)架构中,链上运行的证明程序必须能够读取 L1 区块头、L2 输出根、Rollup 配置等大量链外数据来完成状态派生与执行验证。但链上存储空间与成本极其有限,无法直接容纳这些大体积数据。解决思路是:把数据交给链外的主机(host)进程持有,客户端(client)证明程序只提交一个 32 字节的 Key,由主机通过一个"预言机"通道把对应的数据喂回给客户端。这套机制在 OP Stack 规范中被称为 Preimage Oracle(预镜像预言机)。
kona-preimagecrate 正是这套机制在 Rust 侧的高层封装。它的设计要点在 README 中概括为两点:
no_std兼容:客户端程序运行在 FPVM(Fault Proof Virtual Machine)等受限环境中,因此 crate 默认不依赖标准库,lib.rs中通过#![cfg_attr(not(feature = "std"), no_std)]声明;- 主机侧异步化:主机程序需要通过网络 RPC、KV 存储等外部数据源取数,因此主机侧的句柄全部是
async的,允许在等待外部数据时并发处理其他请求。
lib.rs中#![doc = include_str!("../README.md")]将 README 直接嵌入 crate 文档,说明这份 README 同时也是该 crate 的 API 文档入口。
二、核心抽象:Channel、trait 体系与角色划分
2.1 通信底座:Channeltrait
客户端与主机之间的一切交互都建立在通道(channel)之上。traits.rs定义了Channeltrait,包含三个异步方法:
async fn read(&self, buf: &mut [u8]) -> ChannelResult<usize>; async fn read_exact(&self, buf: &mut [u8]) -> ChannelResult<usize>; async fn write(&self, buf: &[u8]) -> ChannelResult<usize>;read_exact保证读满buf.len()字节,这是所有定长协议帧(如 32 字节 Key、4 字节长度前缀、8 字节长度前缀)读取的基础。Channel的实现与运行环境解耦,no_std环境下由 FPVM 提供系统调用通道,std环境下则由 native_channel.rs 提供基于async_channel无界队列的内存实现NativeChannel与BidirectionalChannel。
BidirectionalChannel::new()通过两条无界异步队列构成双向通信:client 句柄的write连接 host 句柄的read,反之亦然。测试代码正是用它在内存中模拟完整的 client–host 往返。
2.2 六大 trait:客户端面与主机面
traits.rs将整条数据流拆解为职责清晰的 trait 体系:
| Trait | 所属侧 | 职责 |
|---|---|---|
PreimageOracleClient | 客户端 | 按PreimageKey获取预镜像,get返回新分配的Vec<u8>,get_exact写入调用方提供的缓冲区 |
HintWriterClient | 客户端 | 向主机写入一条 hint 字符串,阻塞至收到主机确认 |
CommsClient | 客户端 | PreimageOracleClient + Clone + HintWriterClient的组合 trait,一个类型即可同时承担两种客户端职责 |
PreimageOracleServer | 主机 | 循环读取下一条预镜像请求,取数后写回客户端管道 |
HintReaderServer | 主机 | 读取客户端发来的 hint,交给HintRouter路由处理并回执确认 |
PreimageFetcher | 主机 | 根据PreimageKey从外部数据源获取预镜像 |
HintRouter | 主机 | 将 hint 路由到对应的处理函数(如"拉取某个 L1 区块头") |
PreimageServerBackend | 主机 | PreimageFetcher + HintRouter的组合 trait |
值得注意的组合 trait 实现方式:CommsClient与PreimageServerBackend都是空 trait + 自动 blanket impl,即任何同时满足其子 trait 约束的类型都会自动获得该组合 trait,无需手工实现。
2.3 错误模型
errors.rs定义了贯穿全 crate 的PreimageOracleError与ChannelError。PreimageOracleError覆盖了:管道断裂(IOError)、非法 Key(InvalidPreimageKey)、Key 不存在(KeyNotFound)、返回数据与 Key 哈希不符(IncorrectData)、不支持的 Key 类型(UnsupportedKeyType)、等待超时(Timeout)、缓冲区长度不匹配(BufferLengthMismatch)、hint 解析失败(HintParseFailed)以及兜底的Other。这种错误分类使主机在处理每个请求时都能精确区分"管道问题"与"业务问题",从而决定是终止服务还是仅记录错误后继续。
三、PreimageKey:32 字节的身份编码
3.1 布局与类型字节
key.rs定义了预镜像的寻址方式。一个PreimageKey始终是 32 字节,布局为:
| 位段 | 含义 |
|---|---|
| 第 0 字节 | 类型字节(PreimageKeyType) |
| 第 1~31 字节 | 数据区(31 字节) |
PreimageKeyType是一个#[repr(u8)]枚举,取值与语义如下(与 OP 规范 Pre-image key types 对齐):
| 值 | 类型 | 语义 |
|---|---|---|
| 1 | Local | 本地 Key,与特定 Fault Proof 实例相关、依赖上下文,通常映射引导(bootstrap)数据 |
| 2 | Keccak256(默认) | 全局 Key,由预镜像的 keccak256 摘要低 31 字节映射 |
| 3 | GlobalGeneric | 保留,未来使用 |
| 4 | Sha256 | 全局 Key,由预镜像的 sha256 摘要低 31 字节映射 |
| 5 | Blob | 全局 Key,构造为keccak256(commitment ++ z)后把最高字节替换为类型字节 |
| 6 | Precompile | 全局 Key,构造为keccak256(precompile_addr ++ input)后把最高字节替换为类型字节 |
TryFrom<u8>将类型字节解析为枚举,超出 1~6 的范围返回InvalidPreimageKey;test_preimage_key_from_u8测试验证了0与7均报错。
3.2 构造器与编码转换
PreimageKey提供多种构造方式,覆盖典型使用场景:
new(key: [u8; 32], key_type):取 32 字节输入的低 31 字节作为数据区;new_local(local_ident: u64):把 64 位本地标识写入数据区低 8 字节(大端),用于引导数据寻址;new_keccak256(digest):keccak256 摘要的快捷构造;new_precompile(addr, input):内部计算keccak256(addr ++ input)并截断。
与线格式互转的路径为:PreimageKey -> [u8; 32](From实现,把类型字节放回首位)、[u8; 32] -> PreimageKey(TryFrom,解析首位类型字节)。Display实现输出完整的0x前缀 B256 字符串(见test_preimage_key_display,类型字节为01时输出0x01ffff...)。
3.3 本地 Key 常量:Fault Proof ABI 的一部分
local_keys.rs中导出了 8 个引导数据槽位的本地 Key 常量,源码注释明确指出这些值属于 Fault Proof ABI 的一部分,禁止变更:
| 常量 | 值 | 用途 |
|---|---|---|
L1_HEAD_KEY | 1 | L1 头哈希,含派生争议 L2 区块所需数据 |
L2_OUTPUT_ROOT_KEY | 2 | 双方认可的 L2 输出根(派生起点);Interop 下为约定的超级链前状态承诺 |
L2_CLAIM_KEY | 3 | 争议中的 L2 输出根声明;Interop 下为声明的超级链后状态承诺 |
L2_CLAIM_BLOCK_NUMBER_KEY | 4 | 争议 L2 区块号;Interop 下为声明的 L2 时间戳 |
L2_CHAIN_ID_KEY | 5 | 选择网络特定配置的链 ID |
L2_ROLLUP_CONFIG_KEY | 6 | Rollup 配置的 oracle 回退 |
L1_CONFIG_KEY | 7 | L1 链配置的 oracle 回退 |
DEPENDENCY_SET_KEY | 8 | Interop 依赖集的 oracle 回退 |
local_key_values_match_fault_proof_abi测试断言这些常量与 Fault Proof ABI 完全一致,防止无意改动。
四、客户端侧:OracleReader 与 HintWriter 的取数与提示协议
4.1OracleReader.get:一次完整的取数往返
客户端通过OracleReader(oracle.rs)发起请求,流程分三步:
- 写 Key:
write_key将PreimageKey序列化为 32 字节写入通道,主机据此准备数据; - 读长度:读取 8 字节大端
u64长度前缀,作为后续读取的数据量(也为get_exact的缓冲区校验提供依据); - 读数据:按长度读取完整数据;长度为 0 时直接返回空结果。
get返回新分配的Vec<u8>,适合数据大小未知的场景;get_exact则要求调用方预先提供精确大小的缓冲区,若buf.len() != length返回BufferLengthMismatch,避免不必要的数据拷贝。test_oracle_client_and_host与test_oracle_reader_get_exact两个测试在内存通道上完整验证了这两条路径,主机侧循环处理请求直至收到IOError(通道关闭)退出。
4.2HintWriter.write:hint 线协议与阻塞确认
hint 是客户端提前告知主机"接下来可能需要哪些数据"的轻量通知,使主机可以并行预取,从而降低取数延迟。hint.rs中的HintWriter实现了HintWriterClient:
- 将 hint 字符串按4 字节大端长度前缀 + 原始字节写入通道;
- 阻塞等待主机返回 1 字节确认(
read_exact读入hint_ack)后才返回。
这个"阻塞至确认"的语义在 trait 文档中有明确说明:写入会覆盖管道中任何旧 hint,且必须等到主机处理完成,客户端才能继续。
五、主机侧:OracleServer 与 HintReader 的服务循环
5.1OracleServer.next_preimage_request:请求—取数—回写
主机侧对应物是OracleServer,其next_preimage_request<F: PreimageFetcher>是一个单次服务循环:
- 读取 32 字节并解析为
PreimageKey; - 调用
fetcher.get_preimage(key)从外部数据源取数(异步); - 依次写入 8 字节大端长度与数据本体。
该方法是逐请求调用的,主机通常在一个循环中不断调用它(测试代码即如此),直到通道关闭返回IOError退出。PreimageFetcher是注入点:主机可自由实现为 RPC 客户端、KV 读取器或内存缓存,与OracleServer完全解耦。
5.2HintReader.next_hint:读取、路由、回执
HintReader实现HintReaderServer,流程为:
- 读取 4 字节长度前缀,再读取对应字节数的 hint 负载;
- 将字节解码为 UTF-8 字符串,失败时仍回写 1 字节失败信号(
0x00),防止客户端永久阻塞,并返回HintParseFailed; - 将 hint 交给
HintRouter.route_hint处理(如触发 L1 数据预取); - 路由失败同样回写
0x00解除客户端阻塞; - 成功后回写
0x00作为确认。
hint.rs的测试(test_unblock_on_bad_utf8、test_unblock_on_fetch_failure、test_hint_client_and_host)专门验证了"失败也必须解除客户端阻塞"这一关键健壮性设计:无论 UTF-8 解码失败还是路由失败,客户端都不会卡死。
六、防篡改校验:VerifyingPreimageFetcher
6.1 为什么要二次校验
预镜像数据可能经过不可信的 RPC 响应、hint 处理 bug 或被篡改的 KV 存储。若错误数据进入证明程序,可能导致错误的证明结果。verifyfeature 下的 verifier.rs 提供一个装饰器VerifyingPreimageFetcher<F>:包装内部PreimageFetcher,在取数时就重新哈希校验,把坏数据挡在传播之前。
6.2verify_preimage的分类型策略
verify_preimage(key, data)按 Key 类型区分处理:
- Keccak256:对返回数据计算
keccak256,比较摘要低 31 字节与 Key 数据区是否一致; - Sha256:同理计算
sha256并比较; - Local / Blob / Precompile:直接放行——Local 是本地不透明标识、Blob 的单个字段元素需要 KZG 证明才能验证、Precompile 结果需要输入才能验证,仅凭
(key, data)无法自证; - GlobalGeneric:作为保留且当前无任何主机路径产生它的类型,直接返回
UnsupportedKeyType,让未来可能的误用"响亮地失败"而非静默通过。
校验失败统一返回携带原 Key 的IncorrectData,方便定位是哪一次请求的数据被污染。VerifyingPreimageFetcher同时实现了HintRouter(透传给内部),因此可以直接作为PreimageServerBackend使用。
verify模块的 9 个测试覆盖了全部关键路径:合法 keccak/sha 数据通过、损坏数据与空数据被拒绝、Local/Blob/Precompile 放行、GlobalGeneric 拒绝、错误携带 Key、内部错误透传。
七、Feature 配置与在 Kona 中的实际使用
7.1 Cargo features
crates/proof/preimage/Cargo.toml 将依赖按 feature 严格隔离:
default = []:默认不启用任何 feature,保证no_std纯净;std:启用async-channel及alloy-primitives/std等,提供NativeChannel/BidirectionalChannel等原生内存通道;verify:启用sha2,提供VerifyingPreimageFetcher与verify_preimage;rkyv/serde:为PreimageKey与PreimageKeyType派生零拷贝序列化 / serde 序列化,便于在 SP1 等场景跨边界传递。
7.2 在仓库中的消费方
从各Cargo.toml的依赖声明可以看出该 crate 的覆盖范围:
- bin/client:
kona-preimage.workspace = true(默认无 std feature),用于 FPVM 内运行的no_std客户端程序; - bin/host:
features = ["std", "verify"],主机侧既需要异步原生通道,也需要防篡改校验; - crates/proof/proof 与 proof-interop、std-fpvm:在证明与 Interop 证明逻辑中复用其 trait 与类型;
- sp1/crates 等 SP1 相关 crate:分别按需启用
serde、rkyv、std,说明该 crate 已适配 zkVM 证明路径。
这种"客户端零依赖 + 主机全功能"的 feature 拆分,正是它能在 FPVM、SP1 与传统异步主机三种环境中无缝复用的关键。
八、小结:一套协议,两个角色,多种载体
kona-preimage用约十个文件、一组精心设计的 trait,完整实现了 Fault Proof 预镜像预言机的高层协议:
- 一条数据通道(
Channel):承载 32 字节 Key、8 字节长度、数据本体的定长帧交互,与运行环境解耦; - 两条交互线:取数线(
PreimageOracleClient/PreimageOracleServer/PreimageFetcher)与提示线(HintWriterClient/HintReaderServer/HintRouter),客户端提示预取、主机回执确认,二者并行不悖; - 一种寻址规范(
PreimageKey):类型字节 + 31 字节数据的 32 字节编码,覆盖 Local/Keccak256/Sha256/Blob/Precompile 五类语义; - 一层纵深防御(
VerifyingPreimageFetcher):在取数入口对自描述哈希类 Key 做重哈希校验。
无论你是要理解 OP Stack Fault Proof 的数据交互机制,还是计划在自定义证明客户端或主机中接入新的数据源,都可以从这套 trait 出发:客户端侧实现Channel+ 复用OracleReader/HintWriter,主机侧实现PreimageFetcher/HintRouter+ 复用OracleServer/HintReader,即可获得与 Kona 官方实现一致的协议行为。
继续深入可阅读的仓库源码:客户端取数实现 oracle.rs、hint 线协议 hint.rs、Key 编码 key.rs、防篡改校验 verifier.rs、原生内存通道 native_channel.rs,以及各 trait 的完整定义 traits.rs。
【免费下载链接】optimismOptimism is Ethereum, scaled.项目地址: https://gitcode.com/GitHub_Trending/op/optimism
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考