零知识扑克牌游戏开发教程【ZK Poker】

零知识证明是实现去中心化的一个重要工具。当平台透明存储数据时 我们应当如何保证隐私?当为了扩容而引入链下交易时,我们如何在 链上进行验证?零知识证明在解决这些问题时可以发挥重要的作用。 这个教程的目的是帮助你探索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
2
include “../node_modules/circomlib/circuits/gates.circom”;
include “../node_modules/circomlib/circuits/comparators.circom”;

需要指出的是,我们的文件夹结构看起来是这样:

1
project-root\poker

因此我们使用上面的路径来引入基础电路。这些基础电路时circomlib 提供的,因此需要进行额外的安装:

1
npm install --save circomlib

4、定义扑克牌游戏的零知识电路模板

1
2
3
4
5
template Poker() {   
... circuit body ...
}

component main = Poker();

这个结构基本和官方教程里一样,只有一个模板并且定义了入口点。

5、定义扑克牌游戏的输入、输出和中间结果

1
2
3
4
5
6
7
8
9
10
signal private input cards[5]; // Each 2..14
signal input isSee; // 1 or 0
signal input isFold; // 1 or 0
signal input raise; // int
signal output out; // 1 or 0

// Intermediate results
signal isBid;
signal isRaise;
signal hasChosen;

牌型信息定义为一个数组:cards[5],每一张牌都用其牌面值 表示,我们忽略花色。例如 Ace=14、King=13、Queen=12、Jack=11… 依次类推直至2。选手的牌型是电路的私有输入,这表示我们要求 这个输入保持隐秘。

叫牌选项当然是公开的,因为该信息需要向所有其他选手公布。 弃牌或者跟随下注选项都用布尔值表示,加注选项则是整数 类型,表示实际增加的注数。

唯一的输出是out,值为1或0。

我们还定义了其他一些中间值,稍后在进行介绍。

6、检查牌型中的对子数量

这一部分的代码用于决定牌型中是否包含对子。注意这部分代码看起来 和普通javascript非常像,不包含任何约束定义或者信号操作。

这里是关于电路工作的一个直觉。现实生活中的电路可能需要一些预处理 或者中间评估,但是注意,这样的代码不会包含于正式的证据中,只有那些 约束会嵌入零知识证明公共参数中,并在证据生成和验证节点 。一个不诚实 的证明人可能会修改中间结果来伪造见证。

预处理环节的结果是一个单独的变量numPairs,在后面部分会用到。

这里的实现逻辑就是两重循环进行配对并计算对子的总数:

1
2
3
4
5
6
7
8
9
10
11
12
// Count pairs
var numPairs = 0;
for (var i=0; i<4; i++) {
for (var j=i+1; j<5; j++) {
if (cards[i] == cards[j]) {
numPairs++;
// break doesn’t work. Just force j and i to exit
j = 5;
i = 5;
}
}
}

7、零知识扑克牌游戏中的电路约束

在之前的入门教程中我们介绍了电路信号的操作符号: <–, –>, <==, ==> , 和 === 。 这个教程中我们引入新的符号:组件。组件就是类的实例,注意我们调用组件的方法, 组件的输入信号(a、b、c、in等等)被赋值并使用信号操作符。组件的数据信号也可以 类似方法使用。

在这里我们使用组件进行布尔运算,circomlib提供了不少电路组件可供我们利用。

同样,我们定义了约束,其中一个约束是检查选手是否做出了选择(fold、see或raise)。 另一个要检查的约束就是看是否存在对子。最终输出信号的值为1。

1
2
3
4
5
6
7
8
9
10
11
// isRaise = (raise != 0)
isRaise <-- (raise > 0);
isBid <-- (isRaise || isSee); // Constraint: Must be either bid or fold: isBid XOR isFold = 1
hasChosen <-- isBid + isFold — 2*isBid*isFold;
hasChosen === 1; // Constraint: numPairs must be > 0 if isBid = 1
var hasPairs = (numPairs > 0);
component not3 = NOT();
not3.in <-- isBid; component or2 = OR();
or2.a <-- hasPairs;
or2.b <-- not3.out;
or2.out === 1; out <-- or2.out;

8、零知识电路的公共参数设置、证据生成和验证

首先利用电路进行设置,生成公共参数:

1
2
circom poker.circom -o poker.json
snarkjs setup -o poker.json

接下来需要提供输入,例如下面这个简单的输入文件input.json:

1
{“cards”: [8, 7, 4, 7, 13], “isFold”: 0, “isSee”: 0, “raise”: 10 }

这手牌里包含一个对子,因此所有的叫牌选项都可用。选手选择了加注10。 我们预期这个输入是有效的。输入信息中只有cards是私有的,而其他西悉尼都是 公开的。

现在来生成证据:

1
2
snarkjs calculatewitness -c poker.json
snarkjs proof

证据生成的性能复杂度为O(n),其中n表示电路中约束的个数。因为 我们的电路很简单,因此约束很少,证据生成也快。如果你的电路中有 非常多的约束就需要更多的时间生成证据,你可以查看这个测试 来了解电路性能与约束数量的关系。

现在用验证器来验证证据:

1
2
PS C:\dev\snarks\poker> snarkjs verify
OK

的确和预期一样!


原文链接:ZK Poker — A Simple ZK-SNARK Circuit