今日实时汇率
1 美元(USD)=
7.3043 人民币(CNY)
反向汇率:1 CNY = 0.1369 USD 更新时间:2025-04-19 08:02:31
1、zk-SNARKs 和 libsnark 背景简介
零知识证明,可能是目前最具应用前景和想象力的密码学黑科技。而 zk-SNARKs 正是一类零知识证明方案的简称,全称为 Zero-Knowledge Succinct Non-interactive Arguments of Knowledge。这一名字几乎包含了其所有技术特征,即可以在不泄露任何其他信息的前提下证明一个命题的正确性,并且最终生成的证明具有简洁性(Succinct),也就是说最终生成的证明足够小,并且与计算量大小无关,是一个常数。用白话说就是,你理论上可以在不暴露任何隐私的情况下向其他所有人证明某件事,并且生成的证明体积很小,校验成本很低,与需要证明的内容计算量无关。听起来简直太美好了!
zk-SNARKs 能应用到很多场景,比如隐私保护、区块链扩容、可验证计算等。本文不介绍 zk-SNARKS 和零知识证明的理论细节,不熟悉或想深入了解的同学可阅读其他文章或论文。
如 Vitalik 写的关于 zk-SNARKs 著名的三篇博文。
扎实的理论基础和工程能力,让 libsnark 的作者们能够化繁为简,将形如下图的高深理论和复杂公式逐一实现,高度工程化地抽象出简洁的接口供广大开发者方便地调用。向这些将非凡的理论研究推广至更大规模应用的先锋们致敬。
下图是 libsnark 的模块总览图,摘自 libsnark 代码贡献量第一作者 Madars Virza 在 MIT 的博士论文(https://madars.org/phd-thesis/)。
libsnark 框架提供了多个通用证明系统的实现,其中使用较多的是 BCTV14a 和 Groth16。
查看libsnark/libsnark/zk_proof_systems路径,就能发现 libsnark 对各种证明系统的具体实现,并且均按不同类别进行了分类,还附上了实现依照的具体论文。
其中:
2、基本原理与步骤
利用 libsnark 库开发 zk-SNARKs 应用从原理上可简要概括为以下四个步骤:
有这样一个函数C(x, out),用于判断秘密x是否满足等式x^3 + x + 5 == out,若满足则返回true。
function C(x, out) {
return ( x^3 + x + 5 == out );
}
lambda <- random()
(pk, vk) = G(C, lambda)
proof = P(pk, out, x)
V(vk, out, proof) ?= true
3、搭建 zk-SNARKs 应用开发环境
下面进入动手环节,快速上手 libsnark,跑通例子。
先下载本文对应的 libsnark 最小可用例子代码库libsnark_abc。
git clone https://github.com/sec-bit/libsnark_abc.git
通过 git submodule 拉取 libsnark 代码。
cd libsnark_abc
git submodule update --init --recursive
sudo apt-get install build-essential cmake git libgmp3-dev libprocps4-dev python-markdown libboost-all-dev libssl-dev
mkdir build && cd build && cmake ..
mkdir build && cd build && CPPFLAGS=-I/usr/local/opt/openssl/include LDFLAGS=-L/usr/local/opt/openssl/lib PKG_CONFIG_PATH=/usr/local/opt/openssl/lib/pkgconfig cmake -DWITH_PROCPS=OFF -DWITH_SUPERCOP=OFF ..
make
main
range
test
./src/main 最终出现如下日志,则说明一切正常。你已顺利拥有了 zkSNARK 应用开发环境,并成功跑了第一个 zk-SNARKs 的 demo。
4、理解示例代码
下面我们一起来仔细瞅瞅代码。示例项目包含了 3 份代码(也可查看文末附录)。
r1cs_gg_ppzksnark_keypair
keypair = r1cs_gg_ppzksnark_generator
(example.constraint_system);
r1cs_gg_ppzksnark_processed_verification_key
pvk = r1cs_gg_ppzksnark_verifier_process_vk
(keypair.vk);
r1cs_gg_ppzksnark_proof
proof = r1cs_gg_ppzksnark_prover
(keypair.pk, example.primary_input, example.auxiliary_input);
const bool ans = r1cs_gg_ppzksnark_verifier_strong_IC
(keypair.vk, example.primary_input, proof);
const bool ans2 = r1cs_gg_ppzksnark_online_verifier_strong_IC
(pvk, example.primary_input, proof); 仅从“超长”的函数名就能看出来每步是在做什么,但是却看不到如何构造电路的细节。实际上这里仅仅是调用了自带的r1cs_example,隐去了实现细节。
既然如此,那让我们通过一个更直观的例子来学习电路细节。研究src/test.cpp,这个例子改编自 Christian Lundkvist 的libsnark-tutorial(https://github.com/christianlundkvist/libsnark-tutorial)。
代码开头仅引用了三个头文件,分别是:
#include #include #include 第一个头文件是为了引入default_r1cs_gg_ppzksnark_pp类型,第二个则为了引入证明相关的各个接口。pb_variable则是用来定义电路相关的变量。 下面需要进行一些初始化,定义使用的有限域,并初始化曲线参数。这是相当于每次的准备工作。 typedef libff::Fr default_r1cs_gg_ppzksnark_pp::init_public_params(); 接下来就需要明确「待证命题」是什么。这里不妨沿用之前的例子,证明秘密x满足等式x^3 + x + 5 == out。这实际也是 Vitalik 博文"Quadratic Arithmetic Programs: from Zero to Hero"(https://medium.com/@VitalikButerin/quadratic-arithmetic-programs-from-zero-to-hero-f6d558cea649)中用的例子。如果对下面的变化陌生,可尝试阅读该博文。 通过引入中间变量sym_1、y、sym_2将x^3 + x + 5 = out扁平化为若干个二次方程式,几个只涉及简单乘法或加法的式子,对应到算术电路中就是乘法门和加法门。你可以很容易地在纸上画出对应的电路。 x * x = sym_1 sym_1 * x = y y + x = sym_2 sym_2 + 5 = out // Create protoboard protoboard // Define variables pb_variable pb_variable pb_variable pb_variable pb_variable out.allocate(pb, "out"); x.allocate(pb, "x"); sym_1.allocate(pb, "sym_1"); y.allocate(pb, "y"); sym_2.allocate(pb, "sym_2"); pb.set_input_sizes(1); // x*x = sym_1 pb.add_r1cs_constraint(r1cs_constraint // sym_1 * x = y pb.add_r1cs_constraint(r1cs_constraint // y + x = sym_2 pb.add_r1cs_constraint(r1cs_constraint // sym_2 + 5 = ~out pb.add_r1cs_constraint(r1cs_constraint const r1cs_constraint_system const r1cs_gg_ppzksnark_keypair pb.val(out) = 35; pb.val(x) = 3; pb.val(sym_1) = 9; pb.val(y) = 27; pb.val(sym_2) = 30; const r1cs_gg_ppzksnark_proof bool verified = r1cs_gg_ppzksnark_verifier_strong_IC 经过上面的例子,我们已经了解了利用 libsnark 库开发 zk-SNARKs 电路的所有重要步骤。 现在不妨用新的例子来巩固一下:在不泄露秘密数字大小的前提下,证明数字小于60。 这个在常规程序里用一个运算符就能完成的事情,在 libsnark 下面应该如何表示呢? zk-SNARKs 电路开发的主要工作量和难点在于如何用代码“精确”地描述命题中的所有约束。一旦描述不“精确”,则要么是漏掉约束、要么是写错约束,最终电路想要证明的内容则会与原命题相差甚远。上一节的例子只涉及简单的乘法和加法,与r1cs_constraint最基本的形式一致,因此约束的表达相对容易。除此之外几乎所有的约束都不是很直观,作为初学者很难正确地描述约束细节。 幸好 libsnark 已经为我们实现了大量基础电路小组件。gadgetlib1和gadgetlib2下提供了许多可以直接使用的 gadget。其中gadgetlib1更常用一些,里面收集了包括sha256在内的 hash 计算、merkle tree、pairing 等电路实现。 DangDangDang,gadgetlib1/gadgets/basic_gadgets.hpp中的comparison_gadget正是我们所需。 comparison_gadget(protoboard const size_t n, const pb_linear_combination const pb_linear_combination const pb_variable const pb_variable const std::string &annotation_prefix="") protoboard pb_variable pb_variable x.allocate(pb, "x"); max.allocate(pb, "max"); pb.val(max)= 60; comparison_gadget cmp.generate_r1cs_constraints(); pb.add_r1cs_constraint(r1cs_constraint // Add witness values pb.val(x) = 18; // secret cmp.generate_r1cs_witness(); 读到这里,相信大家都对 libsnark 的使用方法和 zk-SNARKs 电路开发有了一个初步的了解。 你或许已经发现,libsnark 的使用方法较简单,而真正的重点在于 zk-SNARKs 电路开发。正如前面提过的,必须用代码“精确”描述待证命题中的所有约束,“漏掉”或“写错”约束都会让证明内容与原本意图大相径庭,从而导致证明无意义。 如何正确高效地把真实业务逻辑转化为 zk-SNARKs 电路代码,这正是我们开发者需要不断研究和练习的。 好在我们已经有了一个 libsnark 试验场,可以很方便地自由修改、添加代码来尝试。 不论多复杂的电路实现,都是通过一个个更简单地「电路组件」组合封装而形成。因此 libsnark 自带的基础库是一个非常重要的学习资料——既要学习它们的使用方法,又要研究其实现原理。 我们也能通过阅读其他项目的电路实现来了解如何将 ZKP 应用到实际业务中,如 HarryR 的ethsnarks-miximus(https://github.com/HarryR/ethsnarks-miximus)和 Loopring 的protocol3-circuits(https://github.com/Loopring/protocol3-circuits)。从这些项目中可以学习到如何工程化地开发更大规模的电路,以及与电路性能相关的各种设计优化细节,同时对电路约束规模会有更深刻的理解。 同时也欢迎大家继续关注安比实验室「零知识证明 Learn by Coding:libsnark 系列」后续文章,下次我们将尝试从 zk-SNARKs 与智能合约的结合、电路模块化开发、更复杂的 libsnark 实现案例、电路开发过程中容易踩的坑等角度来进一步讨论。 main.cpp #include #include #include #include using namespace libsnark; /** * The code below provides an example of all stages of running a R1CS GG-ppzkSNARK. * * Of course, in a real-life scenario, we would have three distinct entities, * mangled into one in the demonstration below. The three entities are as follows. * (1) The "generator", which runs the ppzkSNARK generator on input a given *constraint system CS to create a proving and a verification key for CS. * (2) The "prover", which runs the ppzkSNARK prover on input the proving key, *a primary input for CS, and an auxiliary input for CS. * (3) The "verifier", which runs the ppzkSNARK verifier on input the verification key, *a primary input for CS, and a proof. */ template bool run_r1cs_gg_ppzksnark(const r1cs_example { libff::print_header("R1CS GG-ppzkSNARK Generator"); r1cs_gg_ppzksnark_keypair keypair = r1cs_gg_ppzksnark_generator (example.constraint_system); printf("\n"); libff::print_indent(); libff::print_mem("after generator"); libff::print_header("Preprocess verification key"); r1cs_gg_ppzksnark_processed_verification_key pvk = r1cs_gg_ppzksnark_verifier_process_vk (keypair.vk); libff::print_header("R1CS GG-ppzkSNARK Prover"); r1cs_gg_ppzksnark_proof proof = r1cs_gg_ppzksnark_prover (keypair.pk, example.primary_input, example.auxiliary_input); printf("\n"); libff::print_indent(); libff::print_mem("after prover"); libff::print_header("R1CS GG-ppzkSNARK Verifier"); const bool ans = r1cs_gg_ppzksnark_verifier_strong_IC (keypair.vk, example.primary_input, proof); printf("\n"); libff::print_indent(); libff::print_mem("after verifier"); printf("* The verification result is: %s\n", (ans ? "PASS" : "FAIL")); libff::print_header("R1CS GG-ppzkSNARK Online Verifier"); const bool ans2 = r1cs_gg_ppzksnark_online_verifier_strong_IC (pvk, example.primary_input, proof); assert(ans == ans2); return ans; } template void test_r1cs_gg_ppzksnark(size_t num_constraints, size_t input_size) { r1cs_example const bool bit = run_r1cs_gg_ppzksnark (example); assert(bit); } int main () { default_r1cs_gg_ppzksnark_pp::init_public_params(); test_r1cs_gg_ppzksnark return 0; } #include #include #include using namespace libsnark; using namespace std; int main () { typedef libff::Fr // Initialize the curve parameters default_r1cs_gg_ppzksnark_pp::init_public_params(); // Create protoboard protoboard // Define variables pb_variable pb_variable pb_variable pb_variable pb_variable // Allocate variables to protoboard // The strings (like "x") are only for debugging purposes out.allocate(pb, "out"); x.allocate(pb, "x"); sym_1.allocate(pb, "sym_1"); y.allocate(pb, "y"); sym_2.allocate(pb, "sym_2"); // This sets up the protoboard variables // so that the first one (out) represents the public // input and the rest is private input pb.set_input_sizes(1); // Add R1CS constraints to protoboard // x*x = sym_1 pb.add_r1cs_constraint(r1cs_constraint // sym_1 * x = y pb.add_r1cs_constraint(r1cs_constraint // y + x = sym_2 pb.add_r1cs_constraint(r1cs_constraint // sym_2 + 5 = ~out pb.add_r1cs_constraint(r1cs_constraint const r1cs_constraint_system // generate keypair const r1cs_gg_ppzksnark_keypair // Add public input and witness values pb.val(out) = 35; pb.val(x) = 3; pb.val(sym_1) = 9; pb.val(y) = 27; pb.val(sym_2) = 30; // generate proof const r1cs_gg_ppzksnark_proof // verify bool verified = r1cs_gg_ppzksnark_verifier_strong_IC cout << "Number of R1CS constraints: " << constraint_system.num_constraints() << endl; cout << "Primary (public) input: " << pb.primary_input() << endl; cout << "Auxiliary (private) input: " << pb.auxiliary_input() << endl; cout << "Verification status: " << verified << endl; } #include #include #include #include using namespace libsnark; using namespace std; int main () { typedef libff::Fr // Initialize the curve parameters default_r1cs_gg_ppzksnark_pp::init_public_params(); // Create protoboard protoboard pb_variable pb_variable x.allocate(pb, "x"); max.allocate(pb, "max"); pb.val(max)= 60; comparison_gadget cmp.generate_r1cs_constraints(); pb.add_r1cs_constraint(r1cs_constraint const r1cs_constraint_system // generate keypair const r1cs_gg_ppzksnark_keypair // Add witness values pb.val(x) = 18; // secret cmp.generate_r1cs_witness(); // generate proof const r1cs_gg_ppzksnark_proof // verify bool verified = r1cs_gg_ppzksnark_verifier_strong_IC cout << "Number of R1CS constraints: " << constraint_system.num_constraints() << endl; cout << "Primary (public) input: " << pb.primary_input() << endl; cout << "Auxiliary (private) input: " << pb.auxiliary_input() << endl; cout << "Verification status: " << verified << endl; } 5、再次上手实践
6、What's NEXT?
7、附录