今天就跟大家聊聊有关circom/snarkjs实战zk rollup的示例分析,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。
过去的一年出现了很多零知识证明应用,在这个教程中,我们将首先学习零知识证明的基本概念、使用circom搭建算术电路、使用snarkjs实现零知识证明的全过程,并利用这些知识实现二层扩容方案zk rollup。
零知识程序和其他程序的实现不太一样。首先,你要解决的问题需要先转化成多项式,再进一步转化成电路。例如,多项式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提供了如下这些关键字用于描述算术电路:
signal:信号变量,要转换为电路的变量,可以是private或public
template:模板,用于函数定义,就像Solidity中的function或golang中的func
component:组件变量,可以把组件变量想象成对象,而信号变量是对象的公共成员
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<levels; i++) { senderExistence.path_index[i] <== tx_sender_path_idx[i]; senderExistence.path_elements[i] <== tx_sender_path_element[i]; } senderExistence.out === account_root;
上面的代码也比较简单,哈希发送方的公钥和账户余额,用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<levels; i++) { newTreeExistence.path_index[i] <== tx_sender_path_idx[i]; newTreeExistence.path_elements[i] <== tx_sender_path_element[i]; } newTreeExistence.out === new_sender_account_root;
前面的两个步骤从发送方的角度检查信息,然后更新发送方的余额并计算新的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<levels; i++) { newReceiverTreeExistence.path_index[i]<==tx_receiver_path_idx[i]; newReceiverTreeExistence.path_elements[i] <==tx_receiver_path_element[i]; } new_root <== newReceiverTreeExistence.out;
最后一步更新接收方余额,计算并输出新的merkle根。一旦电路构建好,就像一个黑盒子。如果你输入正确的值,那么输出一定是正确的,因此用户容易检查以避免恶意中间人。这就是为什么我们需要在电路最后输出一些东西的原因 —— 在这个案例里我们输出的是merkle根。
看完上述内容,你们对circom/snarkjs实战zk rollup的示例分析有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注亿速云行业资讯频道,感谢大家的支持。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。