过去的一年出现了很多零知识证明应用,在这个教程中,我们将首先学习零知识证明的基本概念、使用circom搭建算术电路、使用snarkjs实现零知识证明的全过程,并利用这些知识实现二层扩容方案zk rollup。
区块链开发教程链接: 以太坊 | 比特币 | EOS | Tendermint | Hyperledger Fabric | Omni/USDT | Ripple
零知识程序和其他程序的实现不太一样。首先,你要解决的问题需要先转化成多项式,再进一步转化成电路。例如,多项式x³ + x +5 可以表示成如下的电路:
sym_1 = x * x // sym_1 = x²
sym_2 = sym_1 * x // sym_2 = x³
y = sym_2 + x // y = x³ + x
~out = y + 5
Circom编译器将逻辑转换为电路。通常我们不需要自己设计基础电路。如果你需要一个哈希函数或签名函数,可以在circomlib 找到。
在运行零知识证明程序之前,我们需要创建一个可信的设置,这需要 一个电路以及一些随机数。一旦设置完成就会生成一个证明密钥和一个验证密钥,分别用于生成证据和执行验证。
一旦创建了证明/验证密钥对,就可以生成证据了。
有两种类型的输入:公开输入和私有输入。例如,A向B转账但是不希望公开账户余额,那么A的账户余额就是私有输入,也被称为见证(Witness)。公开输入可以是A和B的地址或者转账金额,这完全取决于你的具体设计。
接下来证明人就可以利用证明密钥、公开输入和见证来生成证据:
最后一步是验证。验证方使用公开输入、证据和验证密钥来验证证据。
公开输入、见证(私有输入)、证明密钥、验证密钥、电路、证据这些基本概念以及相互之间的关系,就是我们继续下面的教程之前需要理解的零知识证明的基本概念。
首先我们先了解下Circom的语法。Circom的语法类似javascript和C,提供一些基本的数据类型和操作,例如for、while、>>、array等。
让我们看一个具体的实例。
假设x、y是保密的(即witness),我们不想暴露x和y的具体值,但是希望证明 (x * y) + z == out,其中z,out是公开输入。我们假设out = 30, z = 10, 那么显然 (x*y) = 20,但是这不会暴露x和y的具体值。
circom提供了如下这些关键字用于描述算术电路:
Circom也提供了一些操作符用于操作信号变量:
好了,这些就是我们继续零知识证明实践需要了解的circom关键字。
STEP 1:编译电路文件,生成circuit.json:
circom sample1.circom
STEP 2:创建可信设置,使用groth协议生成proving_key.json和verification_key.json
snarkjs setup — protocol groth
STEP 3:生成见证(私有输入)。这一步需要输入,因此应当将你的输入存入input.json,就像下面这样:
// input.json
{“x”:3, “y”:5, “z”: 100}
使用下面的命令生成见证文件witness.json:
snarkjs calculatewitness
STEP 4:使用如下的snarkjs命令生成证据:
snarkjs proof
结果是得到proof.json、public.json。在public.json中包含了公开输入,例如:
// public.json
{
“115”, // → out
“100” // → z:100
}
STEP 5:使用如下snarkjs命令进行验证:
snarkjs verify
zk rollup是一个二层解决方案,不过它和其他的二层方案不同。zk roolup将所有数据放在链上,使用zk-snark进行验证。因此,不需要复杂的挑战游戏。在zk rollup中,用户的地址记录在智能合约的merkle树上,使用3字节的索引来表征用户的地址(地址的原始大小是20字节),因此zk rollup可以通过减小数据大小来增加交易吞吐量。
为了便于理解,在下面的zk rollup实现中,我们有意忽略一些细节,原始的zk rollup教程可以参考 ZKRollup Tutorial。
首先,有一个记录账号的merkle树,账号记录的内容是(公钥,余额)。每个交易的内容是(发送方索引、接收方索引、金额)。流程如下:
1、检查发送方账号是否在merkle树上
2、验证发送方的签名
3、更新发送方的余额并验证中间merkle根
4、更新接收方的余额并更新merkle根
circom电路程序的变量定义如下:
// account tree
signal input account_root;
signal private input account_pubkey[2];
signal private input account_balance;
// new account root after sender's balance is updated
signal private input new_sender_account_root;
// tx
signal private input tx_sender_pubkey[2]
signal private input tx_sender_balance
signal private input tx_amount
signal private input tx_sender_sig_r[2]
signal private input tx_sender_sig_s
signal private input tx_sender_path_element[levels]
signal private input tx_sender_path_idx[levels]
signal private input tx_receiver_pubkey[2]
signal private input tx_receiver_balance
signal private input tx_receiver_path_element[levels]
signal private input tx_receiver_path_idx[levels]
// output new merkle root
signal output new_root;
在这个案例中几乎所有的变量都是私有的,不管是公钥、账户余额还是签名等等,只有merkle根和更新后的merkle根是公开的。path_element是构建merkle根的中间值,path_idx是一个索引数组,用于保存merkle树每一层的索引 —— 这时一个二叉树,因此只有左右两个分支,0表示左,1表示右。最终的路径像一个二进制字符串:001011。
下面的circom代码检查发送方是否存在:
//__1. verify sender account existence
component senderLeaf = HashedLeaf();
senderLeaf.pubkey[0] <== tx_sender_pubkey[0];
senderLeaf.pubkey[1] <== tx_sender_pubkey[1];
senderLeaf.balance <== account_balance;
component senderExistence = GetMerkleRoot(levels);
senderExistence.leaf <== senderLeaf.out;
for (var i=0; i
上面的代码也比较简单,哈希发送方的公钥和账户余额,用merkle树的中间值计算,然后得到merkle根(senderExistence.out)。检查计算得到的merkle根和输入是否一致(account_root)。
出于简化考虑,我们省略了merkle树和哈希函数的实现,你可以查看HashedLeaf和GetMerkleRoot。
下面的circom代码检查发送方的签名:
//__2. verify signature
component msgHasher = MessageHash(5);
msgHasher.ins[0] <== tx_sender_pubkey[0];
msgHasher.ins[1] <== tx_sender_pubkey[1];
msgHasher.ins[2] <== tx_receiver_pubkey[0];
msgHasher.ins[3] <== tx_receiver_pubkey[1];
msgHasher.ins[4] <== tx_amount
component sigVerifier = EdDSAMiMCSpongeVerifier();
sigVerifier.enabled <== 1;
sigVerifier.Ax <== tx_sender_pubkey[0];
sigVerifier.Ay <== tx_sender_pubkey[1];
sigVerifier.R8x <== tx_sender_sig_r[0];
sigVerifier.R8y <== tx_sender_sig_r[1];
sigVerifier.S <== tx_sender_sig_s;
sigVerifier.M <== msgHasher.out;
就像区块链交易需要验证发送方的签名一样,在上面的代码中,我们首先哈希消息然后进行签名,然后调用不同的封装函数。
更新发送方余额并检查新的merkle根。
//__3. Check the root of new tree is equivalent
component newAccLeaf = HashedLeaf();
newAccLeaf.pubkey[0] <== tx_sender_pubkey[0];
newAccLeaf.pubkey[1] <== tx_sender_pubkey[1];
newAccLeaf.balance <== account_balance - tx_amount;
component newTreeExistence = GetMerkleRoot(levels);
newTreeExistence.leaf <== newAccLeaf.out;
for (var i=0; i
前面的两个步骤从发送方的角度检查信息,然后更新发送方的余额并计算新的merkle根。最下面一行:newTreeExistence.out === new_sender_account_root;
作用是检查计算得到的merkle根和输入(new_sender_account_root)是否一致。通过这个检查,可以避免伪造或不正确的输入。
下面的代码更新接收方余额以及merkle树:
//__5. update the root of account tree
component newReceiverLeaf = HashedLeaf();
newReceiverLeaf.pubkey[0] <== tx_receiver_pubkey[0];
newReceiverLeaf.pubkey[1] <== tx_receiver_pubkey[1];
newReceiverLeaf.balance <== tx_receiver_balance + tx_amount;
component newReceiverTreeExistence = GetMerkleRoot(levels);
newReceiverTreeExistence.leaf <== newReceiverLeaf.out;
for (var i=0; i
最后一步更新接收方余额,计算并输出新的merkle根。一旦电路构建好,就像一个黑盒子。如果你输入正确的值,那么输出一定是正确的,因此用户容易检查以避免恶意中间人。这就是为什么我们需要在电路最后输出一些东西的原因 —— 在这个案例里我们输出的是merkle根。
zk rollup聚合了很多上述交易并生成单一证据来所见数据大小。在这个教程中为了便于理解,我们仅处理单一交易,点击[这里]https://github.com/KimiWu123/Samples/blob/master/circom/rollupSample/rollup.circom)
查看完整代码。
原文链接:零知识证明应用开发入门— 汇智网