实践指南:构建一个零知识证明 DApp [译]

本文将构建一个zk-dApp(零知识证明 DApp),以证明用户是否属于某个特定组,而无需透露用户具体是谁。

阅读本文前,最好先对以下内容有所了解:

前言

在过去的几个月中,我在以太坊上利用了零知识证明(尤其是zk-SNARKs)创建了几个简单的dApp

我在创建DApp时,几乎没有什么零知识证明资料可供参考,因此我想在博客文章中分享一下创建一个零知识证明 dApp的经验。

这篇博客文章的目的是充当一个实践指南,以帮助读者建立他们的第一个零知识dApp。

注意:此博客文章假定读者对公钥加密有基本的了解,以及知晓如何部署合约在 JavaScript 中与合约交互

概述

我们将构建一个zk-dApp,以证明用户是否属于某个特定组,而无需透露用户具体是谁。

此 zk-dApp 的用户流程如下图:

Zk-dApp身份验证

图 1: Zk-dApp身份验证-用户流程图

开发的流程有以下几个步骤:

  1. 编写零知识电路
  2. 生成用于验证零知识电路的Solidity代码库
  3. 编写智能合约逻辑,并集成步骤 2生成的 Solidity 代码库
  4. 部署合约.
  5. 本地生成证明,并在链上进行验证。

开发环境搭建及工具

就像如今不需要了解HTTP协议来进行Web开发一样,零知识dApp开发具有足够现代的工具,可以使不一定具有密码学数学背景的开发者(例如:我)利用零知识证明构建应用程序 。

以下是我推荐的开发语言和工具:

  • 使用JavaScript/TypeScript 开发 DApp,本身有丰富的生态以及对以太坊也有非常好的支持
  • 使用Solidity 开发智能合约,足够成熟、社区很好
  • 使用Truffle 部署合约
  • 使用Circom 开发零知识证明电路

零知识证明电路

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

电路中,当且仅当私钥对应于其中一个公钥(约束)时才构造证明。电路的伪代码如下:

// 请注意,私钥是标量值(int)
// 而公钥是空间中的一个点(Tuple[int, int])
const zk_identity = (private_key, public_keys) => {
  // derive_public_from_private 是一个用来返回私钥对应公钥的函数
  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语法的介绍,请阅读circom教程

首先安装必要的依赖项及创建项目文件夹,零知识电路逻辑放在circuits/circuit.circom 文件:

npm install -g circom  snarkjs 
npm install circomlib  websnark

mkdir contracts
mkdir circuits
mkdir -p build/circuits

touch circuits/circuit.circom

我们开始创建基础电路 PublicKey 电路模板:

include "../node_modules/circomlib/circuits/bitify.circom";
include "../node_modules/circomlib/circuits/escalarmulfix.circom";
include "../node_modules/circomlib/circuits/comparators.circom";

template PublicKey() {
  // 注意:需要对私钥进行哈希,然后对其修剪以确保其与baby JubJub曲线兼容
  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];
}
项目根目录/circuits/circuit.circom

PublicKey 电路模板的作用是在baby JubJub 曲线上从提供的私钥(in)导出公钥(out)(即上述伪代码中的 derive_public_from_private 函数)。

有了基础电路之后,就可以为零知识电路构建主要逻辑:验证用户是否在组内:

include ...

template PublicKey() {
  ...
}

template ZkIdentity(groupSize) {
  // 公钥在智能合约中
  // 注意: 假设这些公钥都是唯一的
  signal input publicKeys[groupSize][2];

  // 证明者的私钥(这是一个私有的输入信号)
  signal private input privateKey;

  // 从证明者的私钥推导出的公钥
  component publicKey = PublicKey();
  publicKey.in <== privateKey;

  // 确保推导的公钥需要与智能合约中的至少一个公钥匹配以验证其身份
  var sum = 0;

  // 创建一个组件以检查两个值是否相等
  component equals[groupSize][2];
  for (var i = 0; i < groupSize; i++) {

    // 我们不使用 === 因为如果不为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 会返回 1,如果不等返回 0
  // 因此, 如果派生的公钥(空间中的某个点)与智能合约中列出的公钥匹配,则所有equals[i][j].out的总和应等于2
  sum === 2;
}

// Main 入口
component main = ZkIdentity(2);
项目根目录/circuits/circuit.circom

现在可以编译电路、进行可信设置,并生成该电路的 Solidity 验证合约:

$ circom circuits/circuit.circom --r1cs --wasm --sym

## snarkjs setup might take a few seconds
$snarkjs setup --protocol groth -r circuit.r1cs --pk build/circuits/provingKey.json --vk build/circuits/verifyingKey.json

## 生成 solidity 代码用来验证证明
$snarkjs generateverifier --pk build/circuits/provingKey.json --vk build/circuits/verifyingKey.json -v contracts/Verifier.sol

## 应该可以看到在contracts目录下生成了一个合约文件 "Verifier.sol" 
## $ ls contracts
## Migrations.sol Verifier.sol

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

完成上述操作后,我们就完成零知识逻辑。 在下一节(智能合约验证)中,我们将查看生成的Verifier.sol以及如何与其进行交互。

注意:我还在下面添加了一些有关零知识电路的常见问题解答。

问题 1:如何在电路中安全提供私钥?

注意,我们将privateKey的信号指定为“私有”(private)信号。 因此,生成的证明将不包含任何有关“私有”信号的信息,而仅包含“公共”(public)信号的信息。

问题 2:用户不能提供错误的公钥列表吗?

将在下面的“智能合约验证程序”部分中详细介绍这一点。

智能合约验证

完成上面零知识证明电路步骤之后,生成了一个名为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;
        }
    }

...
项目根目录/contracts/Verifier.sol

这就是帮助验证证明的有效性。 verifyProof函数接受4个参数,但是我们只需关注 参数input,因为它代表了公共信号(如:在<PROJECT_ROOT>/circuits/circuit.circom的 ZkIdentity 模板中非私有输入信号)。

使用input参数,我们可以针对智能合约逻辑中现有的一组公共密钥进行验证,就可以解决问题 2。

在我们编写网验证用户身份的逻辑后,概念将更加具体:

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);
    }
}
项目根目录/contracts/ZkIdentity.sol

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

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

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

生成证明并与智能合约交互

一旦您编写了零知识电路并编写了智能合约逻辑,接下来就是生成证明并调用合约函数 isInGroup

由于在JS中有很多示例代码来生成证明和实例化智能合约,因此我仅演示用于生成证明的伪代码,及在智能合约验证证明。 完整的代码在这里找到

// 假定这些都已经存在了
const provingKey // provingKey.json
const circuit // 我们编写的零知识证明电路
const zkIdentityContract // Zk-Identity 合约实例
const privateKey  // 与智能合约中的公钥之一相对应的私钥

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
)

运行完整的JavaScript代码后,你就向用户证明了你属于一个组,而没有透露该用户是谁!

结论

过去三年中,零知识工具的发展非常迅速。 现在,您不需要高深的密码学知识也可开始构建零知识应用,当然如果你了解,将在调试时有所帮助。

强调一下, 代码在这里.

原文:https://kndrck.co/posts/practical_guide_build_zk_dapps/

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 2020-06-10 14:56
  • 阅读 ( 869 )
  • 学分 ( 184 )
  • 分类:零知识证明

3 条评论

请先 登录 后评论
Tiny熊
Tiny熊

布道者

134 篇文章, 36464 学分