yoozzeek/zk-scribble

GitHub: yoozzeek/zk-scribble

面向 Hekate ZK 程序的 trace 变异模糊测试工具,通过篡改有效 trace 来检测 AIR 约束是否存在完备性漏洞。

Stars: 0 | Forks: 0

# zk-scribble [![Crates.io](https://img.shields.io/crates/v/zk-scribble.svg)](https://crates.io/crates/zk-scribble) [![Docs.rs](https://docs.rs/zk-scribble/badge.svg)](https://docs.rs/zk-scribble) [![CI](https://static.pigsec.cn/wp-content/uploads/repos/cas/59/594c1f6147a5fb4ddd8b4a371662464e1ce9734e5068c158e96a3e4ba477273e.svg)](https://github.com/yoozzeek/zk-scribble/actions/workflows/ci.yml) [![License: Apache 2.0](https://img.shields.io/badge/License-Apache2-yellow.svg)](./LICENSE) [Hekate](https://github.com/yoozzeek/zk-scribble) ZK 程序与 chiplet 的 trace 变异 fuzzer。 它会篡改你有效的 trace,运行 preflight 检查, 如果篡改未被检测到则会触发 panic。如果 scribble 触发了 panic,说明你的约束存在漏洞。 ## 它的功能 - 翻转比特位、交换行、切换 selector、注入越界值 - 使用 preflight 进行验证(逐行约束求值 + bus multiset 检查) - 集成 Proptest,将失败案例收缩为逃逸你 AIR 的最小变异 - 在 debug 模式下数秒内运行完毕,而非数分钟 ## 它不能做什么 - **不生成证明。** Scribble 永远不会调用 prover 或 verifier。它检查的是具体 trace 上的约束,而 不是通过 ZK pipeline 进行。这就是它速度快的原因。 - **不进行协议级测试。** Transcript 绑定、Fiat-Shamir、求值参数、Brakedown,这些都需要真正的 prover/verifier e2e 测试。 - **不提供完备性保证。** 通过 scribble 检查意味着每一个随机变异都被捕获了。这并不能证明你的 约束是完备的,只能说明你写下的约束已经被正确接入了。 ## 两个层级 **第 1 层:随机覆盖。** 使用浅层变异(BitFlip、FlipSelector、SwapRows、 OutOfBounds)执行 `assert_all_caught`。用于发现无约束列、缺失的布尔值、selector 隔离漏洞。Proptest 会将任何逃逸的变异 收缩为最小的复现案例。在 chiplet 开发期间运行。 **第 2 层:确定性模拟。** 使用手动构造的结构化或复合变异 (SwapColumns、CopyColumns、Compound)执行 `check_single_mutation`。用 15 行的调用 取代每个 e2e 利用测试中 80 行的样板代码。相同的攻击逻辑,速度快 100 倍(preflight 对比 prove+verify)。作为回归测试套件运行。 ## 快速开始 ``` use zk_scribble::{ScribbleConfig, assert_all_caught}; #[test] fn my_chiplet_survives_chaos() { let (air, instance, witness) = setup_my_chiplet(); assert_all_caught(&air, &instance, &witness, ScribbleConfig::default()); } ``` ## 针对特定 chiplet ``` use zk_scribble::{ScribbleConfig, Target, assert_all_caught}; #[test] fn mlkem_ntt_chiplet_survives_chaos() { let (program, instance, witness) = setup_mlkem_fixture(); let config = ScribbleConfig::default() .target(Target::Chiplet(1)) .cases(512); assert_all_caught(&program, &instance, &witness, config); } ``` ## 限制变异类型 ``` use zk_scribble::{MutationKind, ScribbleConfig, assert_all_caught}; #[test] fn ram_selector_fuzzing() { let (air, instance, witness) = setup_ram_fixture(); let config = ScribbleConfig::default() .mutations([MutationKind::FlipSelector, MutationKind::SwapRows]) .cases(1024); assert_all_caught(&air, &instance, &witness, config); } ``` ## Dispatch swap(第 2 层) 交换两行之间的部分列。Selector 和 RAM 列保持不变, 用于模拟在保持 dispatch 结构的同时重新排列数据的攻击者。 ``` use zk_scribble::{Mutation, Target, check_single_mutation}; #[test] fn ntt_dispatch_swap_caught() { let (air, instance, witness) = setup_ntt_fixture(); let ntt_data_cols = vec![ NTT_A, NTT_B, NTT_A_OUT, NTT_B_OUT, NTT_LAYER, NTT_BFLY, NTT_INSTANCE, ]; let mutation = Mutation::SwapColumns { target: Target::Chiplet(0), cols: ntt_data_cols, row_a: 5, row_b: 12, }; let result = check_single_mutation(&air, &instance, &witness, &mutation); assert!(result.is_ok(), "dispatch swap must be caught by RAM binding"); } ``` ## 协调的跨 trace 攻击 `Compound` 会原子性地应用多个变异。同步修改 chiplet 和 main trace 以绕过单表检查,此时跨表 bus 是唯一的防线。 ``` use zk_scribble::{Mutation, Target, check_single_mutation}; #[test] fn out_of_range_cross_trace_caught() { let (program, instance, witness) = setup_ntt_with_cpu_fixture(); let mutation = Mutation::Compound(vec![ Mutation::OutOfBounds { target: Target::Chiplet(0), col: BUS_B_OUT_PHY, row: 0, value: Q as u128, }, Mutation::OutOfBounds { target: Target::Main, col: CPU_B_OUT, row: 0, value: Q as u128, }, ]); let result = check_single_mutation(&program, &instance, &witness, &mutation); assert!(result.is_ok(), "out-of-range b_out must be caught"); } ``` ## 阅读 preflight 报告 当 preflight 捕获到变异时,报告会指出失败的不变量、表和行: ``` PREFLIGHT: 1 constraint violations, 0 boundary violations, 0 bus issues [Chiplet 0] Constraint 32 "bits_01_equal" failed at row 0 ``` 边界违规会显示具体的偏差: ``` PREFLIGHT: 0 constraint violations, 2 boundary violations, 0 bus issues Boundary #0: col=0 row=0 actual=Flat(Block128(0)) expected=Flat(Block128(22067681354706156661646625971774519825)) Boundary #1: col=1 row=0 actual=Flat(Block128(1)) expected=Flat(Block128(265498766201044366875656389800751278795)) ``` Bus 诊断会列出每一个 endpoint 及其行数、活跃行, 以及 multiset 乘积,这样你就可以看出 bus 的哪一边出现了分歧: ``` PREFLIGHT: 0 constraint violations, 0 boundary violations, 1 bus issues Bus "test_bus" (2 endpoints): Main: 8 rows, 4 active, product=Flat(Block128(16200159481073039905153729824056248498)) Chiplet 0: 8 rows, 4 active, product=Flat(Block128(179105452667142969769264969604701840821)) ``` 如果某个变异逃逸了(报告是干净的),scribble 就会带着经 proptest 收缩的 `Mutation` 触发 panic。该篡改 就是完备性漏洞,请添加一个能捕获它的约束。 ## 环境要求 依赖于 `hekate-math`、`hekate-sdk`、`hekate-core`、`hekate-program`。 ## 许可证 MIT
标签:Rust, SOC Prime, ZK, 代码质量审计, 可视化界面, 开发工具, 网络流量审计, 通知系统, 零知识证明