零知识证明DApp开发实践【身份证明/以太坊】

在这个教程中,我们将学习如何开发一个基于以太坊的零知识身份证明DApp, 学习如何开发Circom零知识电路、如何生成并方法Solidity零知识验证智能合约, 以及如何利用Javascript在链下生成零知识证据,并在教程最后提供完整的源代码下载。

1、零知识身份证明DApp概述

我们将开发一个零知识应用来证明一个用户属于特定的群组 而无需透露用户的具体信息,使用流程如下图所示:

我们的开发过程分为以下几个步骤:

  • 开发零知识电路
  • 生成用于验证零知识电路的Solidity库
  • 开发智能合约并集成上述Solidity库
  • 本地生成证据并在链上进行验证

2、零知识证明以太坊DApp开发环境搭建

就像你不需要完全理解HTTP协议也可以开发web应用一样,已经有很多 工具可以帮助开发基于零知识的DApp而无需密码学或数学基础。

我推荐如下的开发语言和工具链:

  • JavaScript/TypeScript:应用采用javascript/typescript开发,因为这两者在以太坊生态中得到很好的支持
  • Solidity: 智能合约用Solidity开发,因为它很成熟并且社区很好
  • Truffle:使用Truffle作为智能合约开发和部署框架
  • Circom:使用Circom来开发零知识证明电路

3、CIRCOM零知识电路开发:判断私钥是否匹配公钥集

我们的目标是创建一个电路,该电路可以判别输入的私钥是否对应 于输入的公钥集合之一。该电路的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
// Note that a private key is a scalar value (int)
// whereas a public key is a point in space (Tuple[int, int])
const zk_identity = (private_key, public_keys) => {
// derive_public_from_private is a function that
// returns a public key given a private key
derived_public_key = derive_public_from_private(private_key)
for (let pk in public_keys):
if derived_public_key === pk:
return true
return false
}

我们现在要开始用circom编写零知识电路了。circom的语法可以查阅 其官方文档

首先创建项目文件夹并安装必要的依赖包:

1
2
3
4
5
6
7
npm install circom circomlib snarkjs websnark

mkdir contracts
mkdir circuits
mkdir -p build/circuits

touch circuits/circuit.circom

现在编写电路文件circuit.circom,首先引入(incluude)必要的基础电路 并定义PublicKey模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
include "../node_modules/circomlib/circuits/bitify.circom";
include "../node_modules/circomlib/circuits/escalarmulfix.circom";
include "../node_modules/circomlib/circuits/comparators.circom";

template PublicKey() {
// Note: private key needs to be hashed, and then pruned
// to make sure its compatible with the babyJubJub curve
signal private input in;
signal output out[2];

component privBits = Num2Bits(253);
privBits.in <== in;

var BASE8 = [
5299619240641551281634865583518297030282874472190772894086521144482721001553,
16950150798460657717958625567821834550301663161624707787222815936182638968203
];

component mulFix = EscalarMulFix(253, BASE8);
for (var i = 0; i < 253; i++) {
mulFix.e[i] <== privBits.out[i];
}

out[0] <== mulFix.out[0];
out[1] <== mulFix.out[1];
}

PublicKey模板的作用是在babyJubJub曲线上找出私钥(电路输入)对应的公钥(电路输出)。 注意在上面的电路中,我们将输入私钥声明为私有信号,因此在生成的证据中不会包含 任何可以重构该输入私钥的信息。

一旦完成上述的基础模块,现在就可以构建我们的零知识证明电路的主逻辑了 —— 验证 指定的用户是否属于一个群组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
include ...

template PublicKey() {
...
}

template ZkIdentity(groupSize) {
// Public Keys in the smart contract
// Note: this assumes that the publicKeys
// are all unique
signal input publicKeys[groupSize][2];

// Prover's private key
signal private input privateKey;

// Prover's derived public key
component publicKey = PublicKey();
publicKey.in <== privateKey;

// Make sure that derived public key needs to
// matche to at least one public key in the
// smart contract to validate their identity
var sum = 0;

// Create a component to check if two values are
// equal
component equals[groupSize][2];
for (var i = 0; i < groupSize; i++) {
// Helper component to check if two
// values are equal
// We don't want to use ===
// as that will fail immediately if
// the predicate doesn't hold true
equals[i][0] = IsEqual();
equals[i][1] = IsEqual();

equals[i][0].in[0] <== publicKeys[i][0];
equals[i][0].in[1] <== publicKey.out[0];

equals[i][1].in[0] <== publicKeys[i][1];
equals[i][1].in[1] <== publicKey.out[1];

sum += equals[i][0].out;
sum += equals[i][1].out;
}

// equals[i][j].out will return 1 if the values are equal
// and 0 if the values are not equal
// Therefore, if the derived public key (a point in space)
// matches a public keys listed in the smart contract, the sum of
// all the equals[i][j].out should be equal to 2
sum === 2;
}


// Main entry point
component main = ZkIdentity(2);

现在我们编译、设置并生成该电路的Solidity验证器:

1
2
3
4
5
6
7
8
9
10
11
$(npm bin)/circom circuits/circuit.circom -o build/circuits/circuit.json

# snarkjs setup might take a few seconds
$(npm bin)/snarkjs setup --protocol groth -c build/circuits/circuit.json --pk build/circuits/provingKey.json --vk build/circuits/verifyingKey.json

# Generate solidity lib to verify proof
$(npm bin)/snarkjs generateverifier --pk build/circuits/provingKey.json --vk build/circuits/verifyingKey.json -v contracts/Verifier.sol

# You should now have a new "Verifier.sol" in your contracts directory
# $ ls contracts
# Migrations.sol Verifier.sol

注意我们使用groth协议生成证明密钥和验证密钥,因为我们希望使用websnark来生成证据, 因为websnark要比snarkjs性能好的多。

一旦完成上面的环节,我们就已经实现了零知识证明逻辑。下面的部分我们将 介绍如何使用生成的Solidity零知识验证合约。

4、Solidity零知识验证合约

在完成零知识电路的设置之后,会生成一个名为Verifier.sol的solidity库。如果 你查看这个文件的内容,就会看到其中包含如下的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...

function verifyProof(
uint[2] memory a,
uint[2][2] memory b,
uint[2] memory c,
uint[4] memory input
) public view returns (bool r) {
Proof memory proof;
proof.A = Pairing.G1Point(a[0], a[1]);
proof.B = Pairing.G2Point([b[0][0], b[0][1]], [b[1][0], b[1][1]]);
proof.C = Pairing.G1Point(c[0], c[1]);
uint[] memory inputValues = new uint[](input.length);
for(uint i = 0; i < input.length; i++){
inputValues[i] = input[i];
}
if (verify(inputValues, proof) == 0) {
return true;
} else {
return false;
}
}

...

这是用于验证零知识证据有效性的辅助函数。verifyProof函数接收4个参数,但是 我们只关心其中表示电路公共输入的input参数,我们将使用它在智能合约代码中 验证用户的身份。让我们看一下具体的实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
pragma solidity 0.5.11;

import "./Verifier.sol";

contract ZkIdentity is Verifier {
address public owner;
uint256[2][2] public publicKeys;

constructor() public {
owner = msg.sender;
publicKeys = [
[
11588997684490517626294634429607198421449322964619894214090255452938985192043,
15263799208273363060537485776371352256460743310329028590780329826273136298011
],
[
3554016859368109379302439886604355056694273932204896584100714954675075151666,
17802713187051641282792755605644920157679664448965917618898436110214540390950
]
];
}

function isInGroup(
uint256[2] memory a,
uint256[2][2] memory b,
uint256[2] memory c,
uint256[4] memory input // public inputs
) public view returns (bool) {
if (
input[0] != publicKeys[0][0] &&
input[1] != publicKeys[0][1] &&
input[2] != publicKeys[1][0] &&
input[3] != publicKeys[1][1]
) {
revert("Supplied public keys do not match contracts");
}

return verifyProof(a, b, c, input);
}
}

我们创建一个新的合约ZkIdentity.sol,它继承自生成的Verifier.sol, 有一个包含2个成员公钥的初始群组,以及一个名为isInGroup的函数,该函数 首先验证电路的公开输入信号与智能合约中的群组一致,然后返回对输入 证据的验证结果。

逻辑并不复杂,不过的确也满足了我们的目标:验证一个用户属于特定 的群组而无需透露用户是谁。

在继续下面的部分之前,需要先部署合约到链上。

5、用JavaScript生成零知识证据并与智能合约交互

一旦我们完成了零知识电路并实现了智能合约逻辑,就可以生成证据 并调用智能合约的isInGroup方法进行验证了。

下面的伪代码展示了如何生成证据并利用智能合约进行验证,你可以 访问这里 查看完整的js代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Assuming below already exists
const provingKey // provingKey.json
const circuit // zero-knowledge circuit we wrote
const zkIdentityContract // Zk-Identity contract instance

const privateKey // Private key that corresponds to one of the public key in the smart contract
const publicKeys = [
[
11588997684490517626294634429607198421449322964619894214090255452938985192043n,
15263799208273363060537485776371352256460743310329028590780329826273136298011n
],
[
3554016859368109379302439886604355056694273932204896584100714954675075151666n,
17802713187051641282792755605644920157679664448965917618898436110214540390950n
]
]

const circuitInputs = {
privateKey,
publicKeys
}

const witness = circuit.calculateWitness(circuitInputs)
const proof = groth16GenProof(witness, provingKey)

const isInGroup = zkIdentityContract.isInGroup(
proof.a,
proof.b,
proof.c,
witness.publicSignals
)

运行js代码就可以证明你属于一个群组而无需透露你是谁!

教程的完整代码下载地址


原文链接:A Practical Guide To Building Zero Knowledge dApps

汇智网翻译整理,转载请标明出处