Proof of SQL Hello World 实战:用 Space and Time 仓库跑通 SQL 零知识证明全流程
【免费下载链接】sxt-proof-of-sqlSpace and Time | Proof of SQL项目地址: https://gitcode.com/GitHub_Trending/sx/sxt-proof-of-sql
导读
本文以 hello_world 示例 为入口,带你从零跑通 Space and Time 的 Proof of SQL 完整流程:构造一张含数据的表、解析一条SELECT b FROM table WHERE a = 2查询、为查询结果生成零知识证明,再通过验证器确认证明有效并还原可读结果。读完本文,你将掌握仓库中示例程序的运行方式、CPU 与 GPU 两种运行模式的区别,以及从 SQL 解析、查询规划、证明生成到验证的完整调用链。
示例要解决什么问题
Proof of SQL 的核心场景是:证明者(Prover)掌握数据库,验证者(Verifier)只持有数据的承诺(Commitment),证明者需要向验证者证明某条 SQL 查询在真实数据上执行后得到的查询结果确实是正确的,整个过程不泄露底层数据。
hello_world 是这一流程的最小化落地。它演示的查询是:
SELECT b FROM table WHERE a = 2针对的数据表为:
| a | b |
|---|---|
| 1 | hi |
| 2 | hello |
| 3 | there |
| 2 | world |
从表中可以看到,a = 2命中两行,因此查询应返回b列的两个值:hello和world。这正是示例最终输出中验证成功的查询结果。该示例同时验证了过滤条件(WHERE a = 2)和投影(SELECT b)这两类最基本的 SQL 操作在 Proof of SQL 中的正确性。
运行示例
标准运行(默认启用 blitzar / GPU 加速)
在仓库根目录执行:
cargo run --example hello_world默认 feature 配置为arrow与perf(见 crates/proof-of-sql/Cargo.toml),其中perf = ["blitzar", "cpu-perf"],会同时启用 blitzar(GPU 承诺引擎)与 CPU 并行优化。运行时若检测到可用 GPU,会先执行一次预热。
纯 CPU 运行(禁用 blitzar)
如果不希望依赖 GPU 环境,可以关闭默认 feature 并显式启用test与cpu-perf:
cargo run --example hello_world --no-default-features --features="test cpu-perf"这条命令是 README 中特别强调的注意项:--no-default-features会关闭arrow与perf,而cpu-perf(对应 crates/proof-of-sql/Cargo.toml 中的cpu-perf = ["rayon", "ark-ec/parallel", "ark-poly/parallel", "ark-ff/asm", "halo2curves/asm"])会开启 Rayon 并行计算与 arkworks 的汇编级加速,使纯 CPU 也能获得较好的性能。
需要留意的是,hello_world 示例本身声明了required-features = ["proof-of-sql/test"](见 crates/proof-of-sql-planner/Cargo.toml),因此上述两种运行方式都必须保证testfeature 被激活。
示例输出解读
以仓库文档给出的输出为例:
Warming up GPU... 520.959485ms Loading data... 3.229767ms Parsing Query... 1.870256ms Generating Proof... 467.45371ms Verifying Proof... 7.106864ms Valid proof! Query result: OwnedTable { table: {Ident { value: "b", quote_style: None }: VarChar(["hello", "world"])} }各阶段含义如下:
| 阶段 | 输出示例 | 含义 |
|---|---|---|
| 预热 | Warming up GPU... 520.959485ms | 初始化并预热 GPU 后端,仅在启用 blitzar 时出现 |
| 加载数据 | Loading data... 3.229767ms | 构建公共参数、Prover/Verifier 设置并注册表数据 |
| 解析查询 | Parsing Query... 1.870256ms | 将 SQL 解析并转换为 Proof of SQL 查询计划 |
| 生成证明 | Generating Proof... 467.45371ms | 对查询求值并生成零知识证明(耗时最长) |
| 验证证明 | Verifying Proof... 7.106864ms | 验证者用承诺验证证明并还原最终结果 |
| 结果 | Valid proof!+Query result: ... | 证明有效,输出b列命中值["hello", "world"] |
注意,上述耗时数据来自示例 README 的一次实际运行记录,在不同硬件与 feature 组合下会显著不同,尤其是不带 GPU 时Generating Proof的耗时通常更长,不要将其视为性能基准。
输出中的Query result以内部OwnedTable形式打印(VarChar(["hello", "world"])即b列命中两行),而示例代码在验证通过后还会把它转换为 ArrowRecordBatch并调用pretty_format_batches打印成表格形式(见下文代码分析)。
从源码看完整调用链
hello_world 的核心逻辑全部位于 crates/proof-of-sql-planner/examples/hello_world/main.rs,其主流程可拆解为以下五步,与上文输出的阶段一一对应。
第 1 步:生成公共参数与设置(Loading data)
let mut rng = StdRng::from_seed([0u8; 32]); let public_parameters = PublicParameters::rand(5, &mut rng); let prover_setup = ProverSetup::from(&public_parameters); let verifier_setup = VerifierSetup::from(&public_parameters); let mut accessor = OwnedTableTestAccessor::<DynamicDoryEvaluationProof>::new_empty_with_setup(&prover_setup); accessor.add_table( TableRef::from_names(None, "tab"), owned_table([ bigint("a", [1, 2, 3, 2]), varchar("b", ["hi", "hello", "there", "world"]), ]), 0, );几个关键点:
- 固定随机种子:
StdRng::from_seed([0u8; 32])保证运行结果可复现; PublicParameters::rand(5, &mut rng):5是 Dory 协议的max_nu(最大对数规模),公共参数会生成2^max_nu个 G1/G2 群元素(见 crates/proof-of-sql/src/proof_primitive/dory/public_parameters.rs 中rand_impl的iter::repeat_with(...).take(1 << max_nu))。示例中表只有 4 行,规模很小,max_nu = 5已足够;若表规模超过设置上限,Dory 会抛出SmallSetup错误(见 dynamic_dory_commitment_evaluation_proof.rs 中的DoryError::SmallSetup);OwnedTableTestAccessor:这是仓库为测试与示例提供的统一访问器,同时实现了元数据、Schema、承诺与数据访问接口(见 crates/proof-of-sql/src/base/database/test_accessor.rs 中TestAccessortrait 的约束);add_table的第三个参数0:是表的偏移量(table offset),Dory 承诺依赖生成元偏移,示例固定为 0(当前实现也仅支持偏移 0);owned_table构造器:bigint(...)与varchar(...)来自 crates/proof-of-sql/src/base/database/owned_table_utility.rs,用于便捷构造OwnedTable;除这两种类型外,该工具模块还支持uint8、tinyint、smallint、int、int128、boolean、scalar、varbinary、decimal75、timestamptz等列类型,方便你扩展自己的实验表。
第 2 步:SQL 解析与查询规划(Parsing Query)
let sql = "SELECT b FROM tab WHERE a = 2"; let config = ConfigOptions::default(); let statements = Parser::parse_sql(&GenericDialect {}, sql).unwrap(); let query_plan = &sql_to_proof_plans(&statements, &accessor, &config).unwrap()[0];这里调用了proof_of_sql_plannercrate 的sql_to_proof_plans入口。其内部处理管线在 crates/proof-of-sql-planner/src/conversion.rs 的sql_to_posql_plans中有清晰注释,共五步:
- 用
sqlparser将 SQL 解析为 AST; - 用 DataFusion 的
SqlToRel将 AST 转换为LogicalPlan(解析选项来自ConfigOptions,如parse_float_as_decimal、enable_ident_normalization); - 用
Analyzer对逻辑计划做分析; - 用
Optimizer优化(仓库对 DataFusion 38 做了定制:临时移除了common_sub_expression_eliminate规则,详见同文件optimizer()函数); - 将优化后的
LogicalPlan通过logical_plan_to_proof_plan转换为 Proof of SQL 的DynProofPlan。
也就是说,一条 SQL 被翻译成了可证明的查询计划,这也是 "Proof of SQL" 中 "planner" 一词的来源。sql_to_proof_plans返回的是计划列表,示例取第 0 个元素。
第 3 步:证明生成(Generating Proof)
let verifiable_result = VerifiableQueryResult::<DynamicDoryEvaluationProof>::new( query_plan, &accessor, &&prover_setup, &[], ) .unwrap();VerifiableQueryResult::new同时完成两件事:计算查询结果并生成该结果有效的证明(见 crates/proof-of-sql/src/sql/proof/verifiable_query_result.rs 的文档注释:结果以中间形式保存,以处理溢出等边界情况)。它的类型参数DynamicDoryEvaluationProof是 Dory 承诺方案对应的证明类型(见 crates/proof-of-sql/src/proof_primitive/dory/dynamic_dory_commitment_evaluation_proof.rs),&[]是占位符参数(Placeholder 字面量列表,本查询未使用)。
第 4 步:证明验证与结果还原(Verifying Proof)
let result: RecordBatch = verifiable_result .verify(query_plan, &accessor, &&verifier_setup, &[]) .unwrap() .table .try_into() .unwrap();verify是VerifiableQueryResult的验证接口,验证者使用列承诺(这里示例简化:直接复用同一 accessor 模拟承诺访问器)检查证明;验证通过后返回QueryData,其中table被强制转换为查询目标列类型,再通过try_into()转成 ArrowRecordBatch。随后示例打印:
println!("Valid proof!"); println!("{}", pretty_format_batches(&[result]).unwrap());在真实场景中,证明者与验证者是分离的:验证者只有数据库列的承诺,将查询发给不可信的证明者,收到VerifiableQueryResult后用自己持有的承诺验证(这一交互模型在 verifiable_query_result.rs 顶部的伪代码中有完整描述)。如果证明者篡改结果或数据库版本不一致,验证会失败并返回DoryError::VerificationError之类的错误。
第 5 步:阶段计时
示例通过start_timer/end_timer两个辅助函数打印各阶段耗时(即 README 输出中... xxx.xxxms的来源),细节:
fn start_timer(message: &str) -> Instant { print!("{message}..."); stdout().flush().unwrap(); Instant::now() } fn end_timer(instant: Instant) { println!(" {:?}", instant.elapsed()); }更深一步:这是如何做到的
整个 hello_world 背后是 Proof of SQL 的两大支柱:
- Dory 承诺方案(PCS):
PublicParameters、ProverSetup、VerifierSetup、DynamicDoryEvaluationProof均来自proof_primitive::dory模块。Dory 是一种基于配对群的承诺方案,其公开参数由 G1/G2 群元素构成(见 public_parameters.rs)。仓库同时实现了 CPU 版与 GPU 版(dory_commitment_helper_cpu.rs/dory_commitment_helper_gpu.rs),GPU 路径即 blitzar 的职责; - Sumcheck 协议与查询证明:SQL 的每个算子(过滤、投影、聚合、连接等)在 crates/proof-of-sql/src/sql/proof_plans 中都有对应的证明执行器(如
filter_exec.rs、projection_exec.rs),它们把查询的逐行求值编码为多项式,通过 sumcheck 协议证明多项式恒等,从而证明查询结果与底层数据一致。
示例中的WHERE a = 2会被编译为 FilterExec,SELECT b编译为 ProjectionExec,二者共同组成一个可证明的查询计划,最终验证b = hello与b = world确实是数据表中a = 2两行的取值,而证明者无需向验证者泄露整张表。
下一步探索
跑通 hello_world 后,可以从以下路径继续深入:
- 更多数据示例:仓库在 crates/proof-of-sql-planner/examples 下提供了二十多个基于真实 CSV 数据的示例(如
books、census、stocks、dinosaurs、avocado-prices等),每个示例都用 CSV 加载数据并生成证明,其中posql_db示例演示了使用commit_accessor/csv_accessor/record_batch_accessor三种方式接入数据; - 读取数据表的方式:hello_world 直接以内存构造
OwnedTable,而生产场景更常见的是通过OwnedTableTestAccessor或commit_accessor将数据先提交为承诺,再执行证明与验证流程(见 crates/proof-of-sql-planner/examples/posql_db); - 端到端测试:仓库的 crates/proof-of-sql-planner/tests/e2e_tests.rs 对多种 SQL 特性(聚合、连接、分组等)做了端到端证明验证,可以作为理解 Proof of SQL 能力边界的参考。
小结
hello_world 是理解 Space and Time Proof of SQL 的最佳起点:它用最小代码量串起了「构造数据 → 解析 SQL → 生成证明 → 验证证明 → 得到结果」的完整链路,并通过逐阶段计时让开发者直观看到每个环节的成本分布。掌握本文内容后,你可以基于 main.rs 自由修改表结构与 SQL,实测 Proof of SQL 对各种查询类型的支持情况。
【免费下载链接】sxt-proof-of-sqlSpace and Time | Proof of SQL项目地址: https://gitcode.com/GitHub_Trending/sx/sxt-proof-of-sql
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考