零知识证明是实现去中心化的一个重要工具。当平台透明存储数据时 我们应当如何保证隐私?当为了扩容而引入链下交易时,我们如何在 链上进行验证?零知识证明在解决这些问题时可以发挥重要的作用。 这个教程的目的是帮助你探索circom/snarkjs的更多可能性,我们 将学习如何开发一个基于零知识证明的扑克牌游戏。
当谈及零知识电路开发时,我们有很多可选的库。实现我发现iden3开发的circom和snarkjs 对于零知识证明的新手很友好:电路很容易编写,整个零知识证明框架 的运行也很简单,不需要复杂的环境。
建议你在学习本教程之前先阅读circom/snarkjs的官方教程。 本教程的完整代码可以从这里下载。
用自己熟悉的语言学习以太坊DApp开发: Java | Php | Python | .Net / C# | Golang | Node.JS | Flutter / Dart
1、零知识扑克牌游戏规则简介
为了突出重点,我们简化了扑克牌游戏的规则:
- 每个选手发5张牌
- 不可以换牌
- 当比较牌大小时,只考虑对子(pairs),不考虑顺子(straights)、同花(flushes)、满堂红(full houses)等等情况
- 选手可选的动作只有:不跟(fold)、下注(see)、加注(raise)
- 如果一手牌里没有对子,选手就不能叫牌(bid)
还有最后一个规则 —— 不允许虚张声势(bluffing) —— 这就是我们要用零知识证据来保证的。 叫牌的选手可以避免泄露自己的牌,但同时依然可以向其他选手证明自己的确有对子,不是 虚张声势。在下面的教程中,我们将忽略游戏机制而聚焦于零知识证明电路的设计与实现。
2、零知识扑克牌游戏的电路概述
我们这个基于零知识证明的扑克牌游戏的电路,大致应该是这样的:
- 采集输入:牌型、选手的叫牌选择
- 确认牌面中至少有一个对子
- 评估叫牌类型,例如跟进或弃牌
- 设置一个约束来检查是否已经进行选择
- 设置一个约束来检查所选择的选项符合当前的牌型
下面就让我们分步骤学习这个电路的实现代码。
3、引入circom提供的基础电路
首先我们需要引入一些circom提供的基础电路:
1 | include “../node_modules/circomlib/circuits/gates.circom”; |
需要指出的是,我们的文件夹结构看起来是这样:
1 | project-root\poker |
因此我们使用上面的路径来引入基础电路。这些基础电路时circomlib 提供的,因此需要进行额外的安装:
1 | npm install --save circomlib |
4、定义扑克牌游戏的零知识电路模板
1 | template Poker() { |
这个结构基本和官方教程里一样,只有一个模板并且定义了入口点。
5、定义扑克牌游戏的输入、输出和中间结果
1 | signal private input cards[5]; // Each 2..14 |
牌型信息定义为一个数组:cards[5],每一张牌都用其牌面值 表示,我们忽略花色。例如 Ace=14、King=13、Queen=12、Jack=11… 依次类推直至2。选手的牌型是电路的私有输入,这表示我们要求 这个输入保持隐秘。
叫牌选项当然是公开的,因为该信息需要向所有其他选手公布。 弃牌或者跟随下注选项都用布尔值表示,加注选项则是整数 类型,表示实际增加的注数。
唯一的输出是out,值为1或0。
我们还定义了其他一些中间值,稍后在进行介绍。
6、检查牌型中的对子数量
这一部分的代码用于决定牌型中是否包含对子。注意这部分代码看起来 和普通javascript非常像,不包含任何约束定义或者信号操作。
这里是关于电路工作的一个直觉。现实生活中的电路可能需要一些预处理 或者中间评估,但是注意,这样的代码不会包含于正式的证据中,只有那些 约束会嵌入零知识证明公共参数中,并在证据生成和验证节点 。一个不诚实 的证明人可能会修改中间结果来伪造见证。
预处理环节的结果是一个单独的变量numPairs,在后面部分会用到。
这里的实现逻辑就是两重循环进行配对并计算对子的总数:
1 | // Count pairs |
7、零知识扑克牌游戏中的电路约束
在之前的入门教程中我们介绍了电路信号的操作符号: <–, –>, <==, ==> , 和 === 。 这个教程中我们引入新的符号:组件。组件就是类的实例,注意我们调用组件的方法, 组件的输入信号(a、b、c、in等等)被赋值并使用信号操作符。组件的数据信号也可以 类似方法使用。
在这里我们使用组件进行布尔运算,circomlib提供了不少电路组件可供我们利用。
同样,我们定义了约束,其中一个约束是检查选手是否做出了选择(fold、see或raise)。 另一个要检查的约束就是看是否存在对子。最终输出信号的值为1。
1 | // isRaise = (raise != 0) |
8、零知识电路的公共参数设置、证据生成和验证
首先利用电路进行设置,生成公共参数:
1 | circom poker.circom -o poker.json |
接下来需要提供输入,例如下面这个简单的输入文件input.json:
1 | {“cards”: [8, 7, 4, 7, 13], “isFold”: 0, “isSee”: 0, “raise”: 10 } |
这手牌里包含一个对子,因此所有的叫牌选项都可用。选手选择了加注10。 我们预期这个输入是有效的。输入信息中只有cards是私有的,而其他西悉尼都是 公开的。
现在来生成证据:
1 | snarkjs calculatewitness -c poker.json |
证据生成的性能复杂度为O(n),其中n表示电路中约束的个数。因为 我们的电路很简单,因此约束很少,证据生成也快。如果你的电路中有 非常多的约束就需要更多的时间生成证据,你可以查看这个测试 来了解电路性能与约束数量的关系。
现在用验证器来验证证据:
1 | PS C:\dev\snarks\poker> snarkjs verify |
的确和预期一样!