本文作者:Xiang | W3.Hitchhiker[1]
参考文档:
https://blog.iden3.io/first-zk-proof.html
https://docs.circom.io/getting-started/installation
https://learnblockchain.cn/article/1078
目前国内参考的经典教程是 circom 老版的(2020 年 4 月的文章),当时教程的一些包文件和指令格式,参数部分已弃用或者改变,circom 也升级到了 circom2,新的 circom2 编译器是通过 rust 生成的。
在本教程中,我们参考 iden3 官方最新教程文档,将指导你使用 circom2 和 snarkjs 库创建和执行你的第一个零知识证明。
零知识基础概念
什么是零知识证明?
在密码学中,零知识证明或零知识协议是一种方法,通过该方法,一方(证明者)可以向另一方(验证者)证明他们知道值 x,而无需传达除了他知道值 x 这个事实之外任何信息。解释来源于 Wiki[2]
零知识证明使我们能够证明自己的某些特定特征,而无需透露任何额外的信息。
从哲学的角度来看,它们是一组新的加密工具的一部分,这些工具使得透明性不必与隐私性冲突。
什么是 zk-snark?
术语“ zk-snarks”代表zero-knowledge succinct non-interactive arguments of knowledge:
zero-knowledge :零知识
Succinctness:简洁(证明信息较短,方便验证)
Non-interactivity :无需交互
arguments of knowledge :知识论据
暂时无需了解这些概念意味着什么。可以简单地将 zk-snarks 视为产生零知识证明的有效(或简洁)方法:可以使证明信息足够短到可以发布到区块链,并且可以被任何有权验证它们的人( 我们称为验证者)以后都能读取。
一些例子
众筹
如果众筹仅针对 KYC 或授权用户,使用 zk-snarks,你可以证明自己是被授权可参加众筹的人,而无需透露自己是谁或花费了多少。
匿名投票
与上述类似,您可以在不透露性别,年龄甚至姓名的情况下证明自己有资格投票。
例如,可以在全国大选中投票,而仅表明您是该国的公民,并且年满 18 岁。
Covid-19 新冠病毒测试
您可以使用 zk-snarks 来证明您最近对 Covid-19 的测试是阴性,而不用透露测试的确切日期或测试的医院:仅需要在官方认可的时间窗口内有效即可。
库
我们需要使用两个库:circom[3] 和 snarkjs[4].
Circom 是一个可以轻松构建代数电路的库。
snarkjs 是 zk-snarks 协议的独立实现-完全用 JavaScript 编写。
这些库是设计好能协同工作的:在 circom 中构建的任何电路都可以在 snarkjs 中使用。
为什么我们需要电路?
zk-snarks 不能直接应用于任何计算问题。在使用之前,首先需要将问题转换为正确的形式。第一步就是将其转换为代数电路。
尽管这一步做起来并不总是很明显,但事实证明,我们关心的大多数计算问题都可以转化为代数电路。
关于零知识问题的转换,可参考前文:
零知识 QAP 问题的转化原文:w3hitchhiker.mirror.xyz[5]
1、安装
安装依赖
你需要系统中的多个依赖项来运行 circom
及其相关工具。
- 核心工具是用 Rust 编写的
circom
编译器。为使用 Rust , 你可以安装rustup
。如果你使用 Linux 或者 macOS,请打开终端输入以下指令。
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
安装 circom
从我们的源代码安装,请克隆 circom
仓库:
git clone https://github.com/iden3/circom.git
进入 circom 目录,使用 cargo build 编译
cargo build --release
你可以按如下指令安装此二进制可执行文件:
cargo install --path circom
前面指令生成 circom
二进制文件将存在目录 $HOME/.cargo/bin
。
现在,你应该能够使用help
查看可执行文件的所有选项:
circom --help
Circom Compiler 2.0.0
IDEN3
Compiler for the Circom programming language
USAGE:
circom [FLAGS] [OPTIONS] [input]
FLAGS:
-h, --help Prints help information
--inspect Does an additional check over the constraints produced
--O0 No simplification is applied
-c, --c Compiles the circuit to c
--json outputs the constraints in json format
--r1cs outputs the constraints in r1cs format
--sym outputs witness in sym format
--wasm Compiles the circuit to wasm
--wat Compiles the circuit to wat
--O1 Only applies var to var and var to constant simplification
-V, --version Prints version information
OPTIONS:
--O2 <full_simplification> Full constraint simplification [default: full]
-o, --output <output> Path to the directory where the output will be written [default: .]
ARGS:
<input> Path to a circuit with a main component [default: ./circuit.circom]
安装 snarkjs
需要在电脑中先安装Node.js
snarkjs
是一个 npm 包,其中包含从 circom
生成的工件生成和验证 ZK 证明的代码。
你可以用以下命令安装snarkjs
:npm install -g snarkjs
2、设计电路
circom
允许程序员定义算术电路的约束。所有约束必须采用 A*B C = 0 的形式,其中 A、B 和 C 是信号的线性组合。
使用circom
构建的算术电路对信号进行操作。让我们定义我们的第一个电路,它简单地将两个输入信号相乘并产生一个输出信号。
pragma circom 2.0.0;
/*This circuit template checks that c is the multiplication of a and b.*/
template Multiplier2 () {
// Declaration of signals.
signal input a;
signal input b;
signal output c;
// Constraints.
c <== a * b;
}
首先, pragma
指令用于指定编译器版本(类似于 solidity)。这是为了确保电路与 pragma
指令后的编译器版本兼容。否则,编译器会抛出警告。
然后,我们使用关键字template
来定义新电路的形状,为 Multiplier2
。现在,我们必须定义它的信号。信号可以用标识符命名,例如 a, b, c
。
这个电路有 2 个 private 输入信号,名为 a
和 b
,还有一个输出信号 c
。
输入和输出使用<==
运算符进行关联。在 circom 中,<==
运算符做两件事。首先是连接信号。第二个是施加约束。
在本例中,我们使用<==
将c
连接到a
和b
,同时将c
约束为a * b
的值,即电路做的事情是让强制信号 c
为 a*b
的值。
注意:在每个template
中,我们首先声明它的信号,然后声明相关的约束。
3、编译电路
我们创建了叫Multiplier2
的template
电路。
但是,要实际创建电路,我们必须创建此模板的一个实例(使用名为main
的组件实例化它)。为此,请创建一个包含以下内容的文件:
pragma circom 2.0.0;
template Multiplier2() {
signal input a;
signal input b;
signal output c;
c <== a*b;
}
component main = Multiplier2();
使用 circom
编写算术电路后,我们应该将其保存在扩展名为 .circom
的文件。你可以创建自己的电路或使用我们电路库 circomlib
中的模板。
在我们的案例中,我们创建了multiplier2.circom文件。现在是编译电路以获得表示它的算术方程组的时候了。作为编译的结果,我们还将获得计算见证的程序。我们可以使用以下命令编译电路:
circom multiplier2.circom --r1cs --wasm --sym
使用这些选项,我们生成三种类型的文件:
--r1cs
:生成multiplier2.r1cs
( R1CS[6] 电路的二进制格式的约束系统)--wasm
:生成multiplier2_js
目录其中包含Wasm
代码(multiplier2.wasm) 和生成见证[7]所需要的其他文件--sym
:生成multiplier2.sym
(以注释方式调试和打印约束系统所需的符号文件)
我们可以使用选项 -o 来指定创建这些文件的目录
查看电路有关的信息
要显示电路的信息,可以运行:
snarkjs info -r multiplier2.r1cs
可以看到如下输出:
代码语言:javascript复制[INFO] snarkJS: Curve: bn-128
[INFO] snarkJS: # of Wires: 4
[INFO] snarkJS: # of Constraints: 1
[INFO] snarkJS: # of Private Inputs: 2
[INFO] snarkJS: # of Public Inputs: 0
[INFO] snarkJS: # of Labels: 4
[INFO] snarkJS: # of Outputs: 1
此信息与我们设计的电路相吻合。记住,我们有两个私有输入 a 和 b,以及一个输出 c。我们指定的一个约束是a * b = c
。
可以再检查一遍,通过运行以下命令来打印电路的约束:snarkjs r1cs print multiplier2.r1cs multiplier2.sym
输出如下:
代码语言:javascript复制[INFO] snarkJS: [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.a ] * [ main.b ] - [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.c ] = 0
忽略前缀,可以读为:
a*b-c=0
4、计算见证
什么是见证?
在创建证明之前,我们需要计算与电路的所有约束匹配电路的所有信号。为此,我们将使用circom
生成的Wasm
模块来协助完成这项工作。
使用生成的 Wasm
二进制文件和三个 JavaScript 文件,我们只需提供一个包含输入的文件,模块将执行电路并计算所有中间信号和输出。输入、中间信号和输出的集合称为见证[8]。
在我们的例子中,我们想证明我们能够因式分解数字 33。因此,我们分配 a = 3
和 b = 11
。
请注意,我们也可以将数字 1 分配给一个输入,将数字 33 分配给另一个。所以,我们的证明并没有真正表明我们能够分解数字 33。
我们需要创建一个名为 input.json
的文件,其中包含以标准 json 格式编写的输入:
{"a": 3, "b": 11}
现在,我们计算见证并生成二进制文件 witness.wtns
,其中包含 snarkjs
接受的格式。
在使用标志 --wasm
和电路 multiplier2.circom
调用 circom
编译器后,我们可以找到 multiplier2_js
文件夹,其中包含 multiplier2.wasm 中的 Wasm
代码和所有需要的 JavaScript
文件。
使用 WebAssembly 计算见证
进入 multiplier2_js
目录,添加 input.json
文件并执行:
node generate_witness.js multiplier2.wasm input.json witness.wtns
见证文件
将生成 ẁitness.wtns
文件, 该文件以与 snarkjs
兼容的二进制格式编码,这是我们用来创建实际证明的工具。
注意. circom
也支持使用 C 行进计算见证,我们的例子是采用的小型电路,对于大型电路,C 见证计算明显快于 WASM 计算器,使用 C 的方法可以参考官方文档。
5、验证电路
在编译电路并使用适当的输入运行见证计算器后,我们将拥有一个扩展名为 .wtns 的文件,其中包含所有计算的信号,以及一个扩展名为 .r1cs 的文件,其中包含描述电路的约束。这两个文件都将用于创建我们的证明。
现在,我们将使用 snarkjs 工具为我们的输入,生成证明和验证证明。特别是,使用 multiplier2 时,意味着我们可以证明我们能够提供数字 33 的两个因数。也就是说,我们将证明我们知道两个整数 a 和 b,因此当我们将它们相乘时,它会得到数字 33。
可信设置
目前,snarkjs 支持 2 个证明系统:Groth16 和 PLONK。
我们样例中采用的方案是 Groth16,使用 PLONK 可以参考 snarkjs 教程[9]。
Groth16
我们将使用 Groth16[10] zk-SNARK 协议。要使用此协议,你需要生成可信设置(trusted setup[11])。Groth16 需要为每个电路生成可信设置。更详细地说,可信设置由两部分组成:
- tau 的权力,它独立于电路。
- 阶段 2,取决于电路。
接下来,我们为创建可信设置提供了一个非常基本的仪式,我们还提供了创建和验证 Groth16[12] 证明的基本命令。查看相关的背景部分可以查看 snarkjs 教程[13]以获取更多信息。
Tau 的权力
首先,我们开始新的“tau 的权力”仪式:
snarkjs powersoftau new bn128 12 pot12_0000.ptau -v
然后,我们为仪式做出贡献:
snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="First contribution" -v
现在,我们在文件 pot12_0001.ptau 中有对 tau 权力的贡献,下面,我们就可以继续进行阶段 2。
阶段 2
阶段 2 是特定电路的。执行以下命令开始该阶段的生成:
snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -v
接下来,我们生成一个 .zkey
文件,其中包含证明和验证密钥以及所有 阶段 2 的贡献。执行以下命令启动一个新的 zkey:
snarkjs groth16 setup multiplier2.r1cs pot12_final.ptau multiplier2_0000.zkey
为仪式的 阶段 2 做出贡献:
snarkjs zkey contribute multiplier2_0000.zkey multiplier2_0001.zkey --name="1st Contributor Name" -v
导出验证密钥:
snarkjs zkey export verificationkey multiplier2_0001.zkey verification_key.json
生成证明
一旦计算出见证并且已经执行了可信设置,我们就可以 生成与电路和见证人相关联的 zk-proof :
snarkjs groth16 prove multiplier2_0001.zkey witness.wtns proof.json public.json
此命令生成 Groth16[14] 证明并输出两个文件:
proof.json
: 它包含了证明public.json
: 它包含公共输入和输出的值。
Verifying a Proof 验证证明
要验证证明,请执行以下指令:
snarkjs groth16 verify verification_key.json public.json proof.json
该命令使用我们之前导出的文件 verify_key.json、proof.json 和 public.json 来检查证明是否有效。如果证明有效,则命令输出 OK。
一个有效的证明不仅证明我们知道一组满足电路的信号,而且证明我们使用的公共输入和输出与 public.json
文件中描述的相匹配。
6、从智能合约上进行验证