在这个教程中,我们将学习如何开发一个基于以太坊的零知识身份证明DApp,学习如何开发Circom零知识电路、如何生成并方法Solidity零知识验证智能合约,以及如何利用Javascript在链下生成零知识证据,并在教程最后提供完整的源代码下载。
区块链开发教程链接:以太坊 |比特币 | EOS | Tendermint | Hyperledger Fabric | Omni/USDT | Ripple
1、零知识身份证明DApp概述
我们将开发一个零知识应用来证明一个用户属于特定的群组而无需透露用户的具体信息,使用流程如下图所示:
我们的开发过程分为以下几个步骤:
- 开发零知识电路
- 生成用于验证零知识电路的Solidity库
- 开发智能合约并集成上述Solidity库
- 本地生成证据并在链上进行验证
2、零知识证明以太坊DApp开发环境搭建
就像你不需要完全理解HTTP协议也可以开发web应用一样,已经有很多工具可以帮助开发基于零知识的DApp而无需密码学或数学基础。
我推荐如下的开发语言和工具链:
- JavaScript/TypeScript:应用采用javascript/typescript开发,因为这两者在以太坊生态中得到很好的支持
- Solidity: 智能合约用Solidity开发,因为它很成熟并且社区很好
- Truffle:使用Truffle作为智能合约开发和部署框架
- Circom:使用Circom来开发零知识证明电路
3、CIRCOM零知识电路开发:判断私钥是否匹配公钥集
我们的目标是创建一个电路,该电路可以判别输入的私钥是否对应于输入的公钥集合之一。该电路的伪代码如下:
// 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的语法可以查阅其官方文档。
首先创建项目文件夹并安装必要的依赖包:
npm install circom circomlib snarkjs websnark
mkdir contracts
mkdir circuits
mkdir -p build/circuits
touch circuits/circuit.circom
现在编写电路文件circuit.circom,首先引入(incluude)必要的基础电路并定义PublicKey模板:
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曲线上找出私钥(电路输入)对应的公钥(电路输出)。注意在上面的电路中,我们将输入私钥声明为私有信号,因此在生成的证据中不会包含任何可以重构该输入私钥的信息。
一旦完成上述的基础模块,现在就可以构建我们的零知识证明电路的主逻辑了 —— 验证指定的用户是否属于一个群组:
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验证器:
$(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库。如果你查看这个文件的内容,就会看到其中包含如下的函数:
...
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参数,我们将使用它在智能合约代码中验证用户的身份。让我们看一下具体的实现代码:
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代码:
// 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代码就可以证明你属于一个群组而无需透露你是谁!
教程的完整代码下载地址。
原文链接:零知识证明DApp开发实践 — 汇智网