在上一篇文章中,本人讲解了如何使用circom来编写算术电路的代码,如何将该代码转换成算术电路,并使用snarkjs来转换成zkSNARKs的witness和statement,然后,基于这个来proof和认证该证明,最后,使用snarkjs工具生成solidity代码,部署到Ethereum区块链上,来认证该proof。
1. 上篇文章总结
上篇文章中使用到的命令总结如下 (https://liangyihuai.blog.csdn.net/article/details/124043881?spm=1001.2014.3001.5502):
- 将我们的电路编译为 .json 文件 :
$ circom testcirc.circom -o testcirc.json
- 执行的电路运行设置:
$ snarkjs setup -c <circuit_name.json>
- 计算我们运行的见证
$snarkjs calculatewitness -c testcirc.json -i input.json
- 生成证明
$ snarkjs proof
- 验证证明
$ snarkjs verify
- 得到一个verifier.sol文件
$ snarkjs generateverifier
- 获得调用构造的捷径
$ snarkjs generatecall
2.区块链交易隐私保护:Tornado原理
根据上篇文章的知识,本文先讲解Tornado的基本原理,然后讲解如何使用circom和snarkjs实现它。
Tornado的目的是实现数字货币转账的隐私保护。主要的idea是防止一个用户,比如alice,旧地址和新的地址被关联起来。如上图,alice通过Tornado的智能合约(部署在Ethereum上)从旧地址中转账1000DAI到该智能合约中,然后,以零知识证明的方式,从该合约中取出1000DAI到新地址中,从而达到旧地址和新地址无法被关联的目的。
图片来源:Stanford University CS251
上面所述的智能合约维护了上图中间的结构/智能合约的状态,其中包含一个Merkle Tree的根节点,一个 n f i nf_i nfi 列表,其中的 i i i值和右边Merkle Tree的叶子节点的 C i C_i Ci对应。 C i C_i Ci表示用户存储在智能合约中的币, n f i nf_i nfi 表示用户从智能合约中取走的币。具体的,alice秘密生成两个随机数, r i , k i r_i, k_i ri,ki, 计算 C i = H ( k i , r i ) ; C_i = H(k_i, r_i); Ci=H(ki,ri); n f i = H ( k i ) nf_i = H(k_i) nfi=H(ki)。
- 当一个用户向智能合约中存入钱的时候,假设每次只能存入100DAI,她生成 C 4 C_4 C4,根据当前的Merkle Tree(有 C 1 , C 2 , C 3 C_1, C_2, C_3 C1,C2,C3三个叶子)插入 C 4 C_4 C4到(Sparse)Merkle tree中,并生成相应的证明来证明该 C 4 C_4 C4被正确插入。并转账100DAI。
- 当要取钱的时候(比如取 C 3 C_3 C3中的钱),如下图,该用户需要向智能合约证明她知道 C 3 C_3 C3所对应的 r i , k i r_i, k_i ri,ki和 n f 3 nf_3 nf3所对应的 k i k_i ki。接着,她向智能合约公开 k i k_i ki,但不公开 r i r_i ri。智能合约检测 n f 3 nf_3 nf3是否存在智能合约中,避免replay attack。
需要注意的是,第二步所构造的零知识证明中,Merkle tree的根和 k i k_i ki是公开的/非零知识的。由于除了该用户,其他人不知道 r i r_i ri,所以,其他人没法花费 C 3 C_3 C3所对应的钱。
一个问题是,上面的方案如何保护转账隐私呢?如果有很多人都使用该智能合约,转账进入该合约,然后又以零知识证明的方式无法被关联方式转账出来,那么,敌手很难将进入的和出来的转账关联起来。因此,使用的用户越多,该方案的隐私保护效果越好。一个极端情况是,只有一个用户使用该智能合约,那么敌手必定知道从该智能合约转账出来的用户必定是该给智能合约转账进入的用户。
3. 代码实现
如何实现呢?这里我们使用circom和snarkjs来实现一个简单版本的。
我们首先看最关键的代码部分:如何零知识证明一个用户知道 C i C_i Ci所对应的 k i , r i k_i,r_i ki,ri。使用的是sparse Merkle Tree,也即是,叶子节点的个数事先确定好了,如果该叶子节点没有元素,就是用一个特殊的符号代替。下面是circom代码,需要注意的点是变量digest和nullifier是public的,分别表示Merkle tree的根节点和 k i k_i ki,其它变量都是private的。最后一行约束了当前circuit的输入所对应的根节点和所给定的digest是相等的。
template Spend(depth) {
signal input digest;
signal input nullifier;
signal private input nonce;
signal private input sibling[depth];
signal private input direction[depth];
component hasherLeaf = Mimc2();
hasherLeaf.in0 <== nullifier;
hasherLeaf.in1 <== nonce;
component hashers[depth];
component selectors[depth];
for(var i = 0; i < depth; i++){
log(hasherLeaf.out);
selectors[i] = SelectiveSwitch();
selectors[i].in0 <== i == 0 ? hasherLeaf.out : hashers[i-1].out;
selectors[i].in1 <== sibling[i];
selectors[i].s <== direction[i];
hashers[i] = Mimc2(); //hash function
hashers[i].in0 <== selectors[i].out0;
hashers[i].in1 <== selectors[i].out1;
log(hashers[i].out);
}
digest === hashers[depth-1].out;
}
如果读者想执行下面给出的源码的话,可是使用test\circuits\spend10\spend10.circom
的电路,使用下面作为witness/input.json。具体请看test\circuits\spend10\
文件夹
{"digest": "8080087691978198117050369394765307980410920134807546634402909492739124119015","nullifier": "10137284576094","nonce": "45192935725965","sibling": ["171141047017399",
"1921104258255821504350400622926778547143386377757134762322772367658434578801",
"16973346385134691586810492335341496079657653339800419377504568909749787593353",
"6790254632552856235997996157028274815392252658513506763033770088770816748213",
"13263695242030544851800665282261527384879147831156083700346619397977212257688",
"1840967023991560259239032292502905978517614713257677771781870851685755225171",
"4027184480802781552027386339136795508535421388034532363464054617875501505940",
"11615143218180546404420789983788248771891070283082379174961217945780961556045",
"6910546519327067112999266121268451246286112395140456042979014731952547210842",
"6691476691364906793288780242577444371803878027060227645954155614755089787688"],"direction":["1",
"1",
"0",
"1",
"1",
"0",
"1",
"0",
"1",
"1"]}
下图是本人执行spend10的画面。
源码地址:https://download.csdn.net/download/liangyihuai/85114725
包含ppt和相关描述文件
以上