概述
由JS和纯Web Assembly实现的zkSNARK,内部实现了Groth16、Plonk和FFLONK(Beta版)三个算法
snarkjs继承了所有协议需要的组件,包括初始可信设置的NMPC执行过程(用以计算全局的$\tau$的幂)和计算指定电路的证明
snarkjs基于nodejs,计算采用circom编译的电路
snarkjs采用ES模式,可以直接部署到rollup或webpack等项目中
环境安装
命令在linux上执行报错的话,可能是需要sudo权限
由于circom是基于Rust开发的,因此主机需要有Rust环境,这里用的是1.63.0的版本
snarkjs需要node环境,版本需要大于v12,推荐版本为v16
首先安装circom
git clone https://github.com/iden3/circom.git
进入circom目录后执行下列命令
cargo build --release
编译完成后执行安装命令,等待安装完毕即可
cargo install --path circom
之后可以用下列命令查看circom版本
circom --help
然后是安装snarkjs
npm install -g snarkjs@latest
安装完成后可以使用snarkjs --help
查看帮助
构建电路与证明
准备工作 - 电路部分
首先准备好一个电路文件circuit.circom
,具体如下
pragma circom 2.1.4;
template Multiplier() {
signal input a;
signal input b;
signal output c;
c <== a*b;
}
component main = Multiplier();
将上述代码保存为.circom
文件,放在对应目录下
第一行表示指定编译该电路用到的circom版本为2.1.4
电路包含三个变量,其中a,b为输入,输出为c,该电路可以证明Prover知道两个数字a和b,其乘积为c
这里第5行的<==
符号有两个含义:将值与变量c
相关联,然后是施加约束a*b
之后是将该电路声明为Multiplier模板,并用main组件将该模板实例化
circom在编译电路时必须要有一个main组件
之后执行下列命令编译电路
circom circuit.circom --r1cs --wasm --sym
这里的三个参数可以生成三个文件
- r1cs:生成基于R1CS约束系统的电路,得到一个.r1cs的二进制文件
- wasm:生成一个circuit_js的目录(目录名取决于电路名),目录内包含一个wasm文件和其他用于生成witness的相关文件
- sym:生成一个.sym的符号文件,用于调试和打印约束系统
如果还加了--c
参数,则还会生成一个_cpp
目录,里面包含了相关的.cpp,.dat,MakeFile等等文件,这些文件将用于编译C代码以生成witness文件
此外可以采用-o
参数来指定生成的目录名(如果不加这个参数,则默认目录名为circname_js
,这里circname是你的.circom电路文件的名字
circom 2.0.8版本后还可以用
-l
参数来指定include命令需要查找的电路的目录
编译电路后,可以用snarkjs的相关命令来查看电路,先看一下snarkjs的帮助文件
r1cs info Print statistiscs of a circuit
Usage: snarkjs ri [circuit.r1cs]
r1cs print Print the constraints of a circuit
Usage: snarkjs rp [circuit.r1cs] [circuit.sym]
这里用ri参数可以查看电路信息(或者用r1cs info也可以)
snarkjs ri circuit.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
输出表示电路采用的曲线为bn-128,包含4个导线,其中2个私有输入,一个输出,输入输出之间包含一个约束关系
或者使用rp参数查看电路中的约束(或者用print r1cs也可以)
snarkjs rp circuit.r1cs
得到下列输出
[INFO] snarkJS: [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.a ]
* [ main.b ] - [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.c ] = 0
这里忽略掉前面的系数,得到的约束就是$a*b-c=0$
到此为止,电路就编译好了,接下来需要给电路特定的输入,也就是构建witness,这里就需要用到前面生成的wasm文件
snarkjs采用json文件的方式将witness输入电路,需要准备一个input.json
文件,如下
{"a": "3", "b": "11"}
注意这里的输入均为字符串,因为JS无法处理大于$2^{53}$的整数,所以转换为字符串形式处理
将这个json文件放到刚刚生成的circuit_js下面,此时目录里面应该有四个文件,分别是之前的wasm文件和三个json文件
然后在该目录下执行下列命令
node generate_witness.js circuit.wasm input.json witness.wtns
这样就得到了witness.wtns
的证据文件,之后将利用这个文件来生成对应的snark证明
如果说前面使用了--c
参数,则可以进入到对应的cpp目录中,同时将对应的input.json文件放到该目录下,在该cpp目录下执行make命令即可
make
这里为了编译这些cpp文件,还需要一些额外的依赖
nlohmann-json3-dev
,libgmp-dev
,nasm
make完成之后可以得到一个可执行文件circuit
,再利用input文件生成对应的witness即可
./circuit input.json witness.wtns
准备工作 - 可信设置部分
证明生成需要两个文件,一个是前面构造电路时生成的r1cs文件circuit.r1cs
,另一个是证据文件witness.wtns
接下来以Groth16为例来生成证明
Groth16需要为每个电路都执行一次可信设置,因此在证明生成之前还需要针对电路完成一些准备工作,Plonk和FFlonk无需为每个电路都执行可信设置,Plonk和FFlonk的生成过程可以看$[1]$
Groth16的可信设置由两部分组成
- $\tau$的幂次:这一部分与电路无关
- phase 2:这一部分取决于电路
首先来生成$\tau$的幂次,命令如下
snarkjs powersoftau new bn128 12 pot12_0000.ptau -v
利用new
命令创建$\tau$的幂次,这里有几个参数说明一下
第一个参数为指定需要的曲线,snarkjs支持bn128和bls12-381两种曲线
第二个参数为约束参数,表示可信设置所支持的最大约束的数量,这里的12表示可信设置最多支持$2^{12}=4096$个约束,这个参数的最大值为28,也即snarkjs可以生成具有至多$2^{28}\approx2.68*10^{8}$个约束的电路
生成之后会在目录下得到一个pot12_0000.ptau
文件
接下来需要用contribute
命令,使用新的贡献来创建一个ptau文件
snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="First contribution" -v
这里会提示输入一些随机文本来为贡献提供额外的熵源,这里随便输就行,比如输入snowolf
contribute命令会将截止至现在的所有ptau文件作为输入(截止至目前为止的ptau文件为上面新建的pot12_0000.ptau
),同时输出一个新的ptau文件pot12_0001.ptau
,该文件中包含新贡献者执行的计算
ptau文件中会记录迄今为止进行的所有挑战和响应过程
这个命令还有一个name参数,可以输入任何内容(可以理解为执行contribute命令的一些注释),在后续验证ptau文件时这些信息会作为辅助内容输出
如果不想在contribute命令执行过程中输入文本(懒狗行为),可以使用-e
参数来讲命令变为非交互式的,就可以不用输入相关信息了
比如在第一次contribute的基础上利用-e
参数再执行一次contribute
snarkjs powersoftau contribute pot12_0001.ptau pot12_0002.ptau --name="Second contribution" -v -e="some random text"
contribute的过程还可以使用第三方软件来更新,比如下面这样(这里用的是$[6]$这个库的bn-256曲线构建)
snarkjs powersoftau export challenge pot14_0002.ptau challenge_0003
snarkjs powersoftau challenge contribute bn128 challenge_0003 response_0003 -e="some random text"
snarkjs powersoftau import response pot14_0002.ptau response_0003 pot14_0003.ptau -n="Third contribution name"
更新完成之后,用verify
命令验证截止至目前为止的ptau(只执行了一次更新,所以验证0001这个ptau文件)
snarkjs powersoftau verify pot12_0001.ptau
这里只是生成$\tau$的幂,还没有执行phase 2的命令,因此此时运行后系统会输出如下提示
[WARN] snarkJS: this file does not contain phase2 precalculated values. Please run:
snarkjs "powersoftau preparephase2" to prepare this file to be used in the phase2 ceremony.
[INFO] snarkJS: Powers of Tau Ok!
不过验证通过了,就没什么问题
这里注意一点,每当新的zk-snark需要执行可信设置时,只需要对最新的ptau文件执行verify命令,即可验证截止到目前为止的整个挑战和相应链
接下来是利用beacon命令创建一个ptau文件,这个ptau文件会以随机beacon的形式对ptau文件完成贡献,可信设置的第一阶段需要对其应用一个随机beacon
这里的随机beacon是公共随机性,其在某一特定时间前是不可用的,beacon值由基于高熵值和部分公开数据计算的延迟Hash函数
引入beacon的命令如下
snarkjs powersoftau beacon pot12_0001.ptau pot12_beacon.ptau 1cbf6603d6ff9ba4e1d15d0fd83be3a80bca470b6a43a7f9055204e860298f99 10 -n="Final Beacon"
这里利用了以太坊第16668892个区块的Hash值作为beacon值,后面的参数10表示以该beacon值作为输入,计算$2^{10}$次Hash
接下来是可信设置的第二部分,也即phase 2
phase 2用到的命令为prepare phase2
,该命令会用到前面输出的pot12_beacon.ptau
文件,并基于该文件计算基于$\tau,\alpha*\tau,\beta*\tau$的拉氏插值多项式,命令如下
snarkjs powersoftau prepare phase2 pot12_beacon.ptau pot12_final.ptau -v
输入命令之后,电脑会算一阵子,计算的时间取决于曲线、第一阶段中允许的约束数量(约束数量越大,计算时间越长)
这里不想算的话也可以直接使用$[1]$中给出的计算好的文件,包含了参数为8至28的所有final ptau文件,参数越大则ptau文件也就越大(从github下载后记得盐酸一下文件的blake2b hash值)
这里插个题外话,$[1]$中的最后一个ptau文件,也就是约束数量为$2^{28}$的final ptau文件,看了一下reddit和相关的github仓库,应该是19年的时候由Semaphore团队发起的多方参与的可信设置,目的是以安全可靠的方式生成zk-SNARK电路参数
生成完最终ptau文件后,别忘了用verify
命令验证一下
snarkjs powersoftau verify pot12_final.ptau
# output:[INFO] snarkJS: Powers of Tau Ok!
这里验证通过的话会输出Powers of Tau Ok,且不再出现前面的“not contain phase2 precalculated values”的警告信息
密钥生成
准备完毕之后,接下来是根据电路构建证明与验证密钥
首先需要用电路和上面的最终ptau文件生成一个zkey文件,该文件是一个零知识密钥文件,包含phase 2的所有贡献,以及用于证明和验证的密钥,利用这个zkey文件可以验证其是否属于特定的电路
snarkjs groth16 setup circuit.r1cs pot12_final.ptau circuit_0000.zkey
这里得到的circuit_0000.zkey
文件不包含任何贡献,不能用于最终电路的证明,因此需要执行前面的贡献更新步骤
接下来使用zkey
命令来对zkey文件进行贡献更新(前面用的是powersoftau
命令,基本流程是一样的)
snarkjs zkey contribute circuit_0000.zkey circuit_0001.zkey --name="First Contributor Name" -v #这里依然输入snowolf
snarkjs zkey contribute circuit_0001.zkey circuit_0002.zkey --name="Second contribution Name" -v -e="Another random entropy"
snarkjs zkey verify circuit.r1cs pot12_final.ptau circuit_0002.zkey # 更新完贡献记得验证一下
然后引入随机beacon值并验证
snarkjs zkey beacon circuit_0002.zkey circuit_final.zkey 1cbf6603d6ff9ba4e1d15d0fd83be3a80bca470b6a43a7f9055204e860298f99 10 -n="Final Beacon phase2"
snarkjs zkey verify circuit.r1cs pot12_final.ptau circuit_final.zkey #同样引入beacon后验证一下
之后基于该zkey文件,导出一个验证密钥,导出的密钥为json格式
snarkjs zkey export verificationkey circuit_final.zkey verification_key.json
证明生成
接下来利用电路和证明密钥来构建证明
snarkjs groth16 prove circuit_final.zkey witness.wtns proof.json public.json
这里需要注意,证据文件wtns是在circuit_js目录下面的,要么指定目录,要么把这个文件移出来
命令执行完毕后,可以得到两个json文件,一个是public.json
,也就是statement
[
"33"
]
我们前面给的witness分别是3和11,因此$3*11=33$正确
另一个是proof.json
,里面包含用于验证的元素
{
"pi_a": [
"8557899143982317778391948775060044948105591633617269037571657495122124282015",
"16098128471466170270917602716480235184983668955524323295238061011818817672135",
"1"
],
"pi_b": [
[
"6535470862257485234354806354033717855925707379802519644584327276468658273795",
"10502392128402042953625780127583458028533994641873753875165197959240896828858"
],
[
"2815733098382138808690818631672766444566431265627197275002608659963168374664",
"15021904494559773375029367557133607563192854842674738692583613287403685681164"
],
[
"1",
"0"
]
],
"pi_c": [
"15249144878438398758879654714528615775709062271951270398259545621398126856108",
"14001704222289679824993211163914298126082430747112685812890522008606900310782",
"1"
],
"protocol": "groth16",
"curve": "bn128"
}
上面三个就是Groth16中三个用于配对的元素
这里官方给了一个一步到位的方式,可以在一条命令里面同时构建witness文件和证明,如下
# Groth16
snarkjs groth16 fullprove input.json circuit.wasm circuit_final.zkey proof.json public.json
# Plonk
snarkjs plonk fullprove witness.json circuit.wasm circuit_final.zkey proof.json public.json
# FFlonk
snarkjs fflonk fullprove witness.json circuit.wasm circuit_final.zkey proof.json public.json
验证
利用上述两个json文件可以验证证明
snarkjs groth16 verify verification_key.json public.json proof.json
如果输入下列信息说明验证通过了
[INFO] snarkJS: OK!
我们可以改一下两个json文件,比如把public.json里面的33改成32,再执行一次验证命令,此时会输出验证失败
[ERROR] snarkJS: Invalid proof
或者改一下proof.json里面的一个群元素,也会验证失败
模拟执行
最后是将验证密钥导出为Solidity智能合约,就可以将这个合约发布到链上了
snarkjs zkey export solidityverifier circuit_final.zkey verifier.sol
snarkjs给了一个模拟验证执行的命令,利用上面的两个json文件可以模拟Verifier在remix中的验证过程
snarkjs zkey export soliditycalldata public.json proof.json
输出是三个群元素和公共输入,均编码为HEX形式
["0x12eb99696b105b00cc9632f23a0d78a87a4942bb3604bf5e6a51a0ff99f1189f", "0x239737c368c06c9a11d59a18c8b0bc258fad6776ac60a8e3515c3dd7f81253c7"],
[
["0x173824aecbc6aaadb1fdfe0e2fed1efee60629e0c31c2edaa356355442fd55ba", "0x0e72f207330151fc524a73e5ca18d746699801cfad9780548250591bb7587e03"],
["0x213618bd766d0a288966effff30d95a85c275f8bc2ce2147a59f845c6124180c", "0x0639a60145aa6de656ce115d39c0ee85306527e04ba72c2da110beb3cf4f3f88"]
],
["0x21b6b5cbc21e2dcda54684d9deb639dba86aadc7f9e5c125058afe3f2e2007ac", "0x1ef4af0a36d7b94b734bb176ef2f12475153885726b4bd3e7fefae34122552fe"],
["0x0000000000000000000000000000000000000000000000000000000000000021"]
这里Verifier会调用verifyProof函数来对上述这堆东西进行验证,这里可以把上面的那个智能合约导入到remix来模拟一下,这里给一个网址:http://remix.ethereum.org/
进去之后,先把刚刚导出的sol文件上传
然后点击左边第三个,编译我们刚刚上传的智能合约,等待编译完成
之后点左侧第四个部署并运行智能合约,这里选择Verifier的合约
然后下面有一个已部署的智能合约,把上面得到的那一串数字粘贴到verifyProof里面,点一下call来调用智能合约
右侧主界面可以看到正在调用,如果验证通过的话decode output这里会输出true
如果把输入改成32,验证就失败了
好了,基本上就是这么回事了,$[1]$中还给出了一个部署至浏览器的方法,这里不再介绍
其他
除了自己写电路意外,$[2]$这个仓库里面有一些已经写好的常用的电路,有需要的话可以直接拿过来用
或者懒得敲命令的话,可以直接到$[7]$这个网站,直接写电路,然后点击右侧直接生成就可以了,这个网站还可以生成对应的html文件,用来模拟证明和验证的过程
References
$[1]$ iden3/snarkjs: zkSNARK implementation in JavaScript & WASM (github.com)
$[2]$ iden3/circomlib: Library of basic circuits for circom (github.com)
$[3]$ Installation - Circom 2 Documentation
$[4]$ circom与snarkjs经典教程:创建第一个零知识 snark 电路 | 登链社区 | 区块链技术社区 (learnblockchain.cn)
$[5]$ 构建你的第一个零知识 snark 电路(Circom2) | 登链社区 | 区块链技术社区 (learnblockchain.cn)
$[6]$ kobigurk/phase2-bn254 (github.com)
$[7]$ zkREPL | Online Development Environment for zkSNARKs (0xPARC)
$[8]$ circom试用 | Zhuang's Diary (willzhuang.github.io)
想请教下,是否可以在不通过 zkey 文件生成 proof.json 的情况下,直接生成 output(public.json),这样会更方便调试
没试过,可以写shell脚本尝试一下
求教,这里的ptau文件给他加贡献生成新的ptau文件是mpc生成crs的实现吗?如果是的话,那plonk的srs可更新在这个框架中的体现在哪儿诶?
phase 2
所以groth16的phase2是针对具体电路都需要的设置,plonk的phase2是用于可更新。同样的操作不同的协议作用是不一样的。这样理解是吧
写一下电路,执行一下命令,会清楚一些