构建你的第一个零知识 snark 电路(Circom2)
0xE43a
June 20th, 2022

作者:Xiang

参考文档:

目前国内参考的经典教程是 circom 老版的(2020 年 4 月的文章),当时教程的一些包文件和指令格式,参数部分已弃用或者改变,circom 也升级到了 circom2,新的 circom2 编译器是通过 rust 生成的。

在本教程中,我们参考 iden3 官方最新教程文档,将指导你使用 circom2 和 snarkjs 库创建和执行你的第一个零知识证明。

零知识基础概念

什么是零知识证明?

在密码学中,零知识证明或零知识协议是一种方法,通过该方法,一方(证明者)可以向另一方(验证者)证明他们知道值x,而无需传达除了他知道值x这个事实之外任何信息。 解释来源于 Wiki

零知识证明使我们能够证明自己的某些特定特征,而无需透露任何额外的信息。

从哲学的角度来看,它们是一组新的加密工具的一部分,这些工具使得透明性不必与隐私性冲突。

什么是 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 的测试是阴性,而不用透露测试的确切日期或测试的医院:仅需要在官方认可的时间窗口内有效即可。

我们需要使用两个库:circomsnarkjs.

Circom 是一个可以轻松构建代数电路的库。

snarkjs 是 zk-snarks 协议的独立实现-完全用 JavaScript 编写。

这些库是设计好能协同工作的:在 circom 中构建的任何电路都可以在 snarkjs 中使用。

为什么我们需要电路?

zk-snarks 不能直接应用于任何计算问题。在使用之前,首先需要将问题转换为正确的形式。第一步就是将其转换为代数电路。

尽管这一步做起来并不总是很明显,但事实证明,我们关心的大多数计算问题都可以转化为代数电路。

关于零知识问题的转换,可参考前文:

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 输入信号,名为 ab ,还有一个输出信号 c

输入和输出使用<==运算符进行关联。 在 circom 中,<==运算符做两件事。 首先是连接信号。 第二个是施加约束。

在本例中,我们使用<==c连接到ab,同时将c约束为a * b的值,即电路做的事情是让强制信号 ca*b 的值。

注意:在每个template 中,我们首先声明它的信号,然后声明相关的约束。

3、编译电路

我们创建了叫Multiplier2template 电路。

但是,要实际创建电路,我们必须创建此模板的一个实例(使用名为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 电路的二进制格式的约束系统)
  • --wasm:生成 multiplier2_js 目录其中包含Wasm 代码(multiplier2.wasm) 和生成见证所需要的其他文件
  • --sym:生成 multiplier2.sym(以注释方式调试和打印约束系统所需的符号文件)

我们可以使用选项 -o 来指定创建这些文件的目录

查看电路有关的信息

要显示电路的信息,可以运行:

snarkjs info -r multiplier2.r1cs

可以看到如下输出:

[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

输出如下:

[INFO]  snarkJS: [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.a ] * [ main.b ] - [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.c ] = 0

忽略前缀,可以读为:

a*b-c=0

4、计算见证

什么是见证?

在创建证明之前,我们需要计算与电路的所有约束匹配电路的所有信号。为此,我们将使用circom 生成的Wasm 模块来协助完成这项工作。

使用生成的 Wasm二进制文件和三个 JavaScript 文件,我们只需提供一个包含输入的文件,模块将执行电路并计算所有中间信号和输出。输入、中间信号和输出的集合称为见证

在我们的例子中,我们想证明我们能够因式分解数字 33。因此,我们分配 a = 3b = 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教程

Groth16

我们将使用 Groth16 zk-SNARK 协议。要使用此协议,你需要生成可信设置(trusted setup)。 **Groth16 需要为每个电路生成可信设置。**更详细地说,可信设置由两部分组成:

  • tau 的权力,它独立于电路。
  • 阶段2,取决于电路。

接下来,我们为创建可信设置提供了一个非常基本的仪式,我们还提供了创建和验证 Groth16 证明的基本命令。查看相关的背景部分可以查看 snarkjs 教程以获取更多信息。

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 证明并输出两个文件:

  • 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、从智能合约上进行验证

👉 snarkjs 还可以生成一个 Solidity 验证器,允许在以太坊区块链上验证证明。

首先,我们需要使用以下命令生成 Solidity 代码:

snarkjs zkey export solidityverifier multiplier2_0001.zkey verifier.sol

此命令采用验证密钥 multiplier2_0001.zkey 并在名为 verifier.sol的文件中输出 Solidity 代码。你可以从此文件中获取代码并将其剪切并粘贴到 Remix 中运行。你将看到代码包含两个合约:PairingVerifier。你只需要部署Verifier 合约。

你可能使用 Rinkeby、Kovan 或 Ropsten 这样的测试网。你也可以使用 JavaScript VM,但在某些浏览器中,验证需要很长时间并且页面可能会冻结。

Verifier 有一个名为 verifyProofview 函数,当且仅当证明和输入有效时才返回 TRUE 。为了方便调用,可以使用 snarkJS 生成调用的参数,输入

snarkjs generatecall

将命令的输出的可以复制粘贴到 Remix 中 verifyProof 方法的参数字段中。如果一切正常,此方法应返回结果 TRUE。当你尝试只更改参数,就会看到结果是 FALSE

以下链接已通过验证,可在浏览器查看生成的verifier.sol 的源码:


声明:本文内容仅供参考、交流,不构成任何投资建议。若存在明显的理解或数据的错误,欢迎反馈。

本文内容系 W3.Hitchhiker 参考相关教程后所作内容,如需转载请标明出处。

商务合作:hello@w3hitchhiker.com

W3.Hitchhiker 官方推特:

Arweave TX
HLztLqjoG4xzSoFbD_2wlcx9Cr32GXpX0FRVuxsRSTg
Ethereum Address
0xE43a21Ee76b591fe6E479da8a8a388FCfea6F77F
Content Digest
BHJ9fqXMABXspaFxbaDbt9c-PvrQLdi77OjN6Au9YqU