零知识证明实战:libsnark

zk-SNARKs这项技术早在2013年就已经被提出来了。这项技术随即就被MIT和JHU的研究者们拿来尝试解决Bitcoin的隐私保护问题,也就是所谓的Zerocash协议(Zcash的前身)。


Zerocash协议较为复杂,不适合在一篇入门文章中来讲解。因此,本文将以数独游戏为例,介绍如何用zk-SNARKs技术来实现一个有趣的应用。


之所以拿数独为例,是因为知乎上有一篇介绍零知识证明的文章,

Xiaoyao Qian:一个数独引发的惨案:零知识证明(Zero-Knowledge Proof)zhuanlan.zhihu.com图标

它以数独游戏为例,讲了什么是交互式证明,如何将交互式证明转化成非交互式的。本文就算是对该文章的实现吧~


问题描述--数独游戏

在一张桌子上,有n x n张扑克牌,每个扑克牌背后的数字是从1到9,有些扑克牌是翻开的,有些是盖着的。Alice是一个解题高手,她很快就计算出一个可行解。Bob是一个数独爱好者,他想了很久还是解出这个数独谜题。Bob怀疑这个谜题其实根本无解,出题者故意耍大家。于是Alice就打算向Bob证明这个谜题不但有解,而且她知道一个解,但是不打算把这个秘密直接告诉bob。


图片来源: https://medium.com/qed-it/the-incredible-machine-4d1270d7363a

问题分析

简单分析一下,Alice是想证明她知道

a_{i, j} 是数字1到9;
每一行的数字不重复;
每一列的数字不重复;
9个3x3的子方格中的数字不重复。

应用zk-SNARKs技术实现一个非交互式零知识证明应用的开发顺序大体上是:

  1. 编写计算的验证逻辑,必须是R1CS的形式;这一步是关键
  2. 生成证明密钥(proving key)和验证密钥(verification key);
  3. Alice使用proving key和她的可行解构造证明;
  4. Bob使用verification key和翻开的扑克牌,验证Alice发过来的证明。


目前有两个工具可以用来将计算转换成R1CS:

  • gadgetlib1:这个一个用于手写R1CS的工具包,包含在libsnark项目中。gadgetlib1提供了一些基本运算的R1CS,比如:sha256函数、整数的二进制分解、大小比较等等。除了使用这些工具函数,gadgetlib1还提供了pb_variable、protoboard等类型。
    • pb_variable:对应R1CS中变量,记录了变量的下标;
    • protoboard:这个对象记录了整个R1CS,包括:一组R1CS约束、每个变量的值,可以判断R1CS方程组的解是否合法。
template<typename FieldT>
class protoboard {
   void add_r1cs_constraint(const r1cs_constraint<FieldT> &constr, const std::string &annotation="");
   bool is_satisfied() const;

   void set_input_sizes(const size_t primary_input_size);
}
  • ZoKrates:这个项目定义了一个python-like的语言,并且用Rust实现了从高级语言到R1CS的编译器。提供了几个命令行工具来编译代码、生成证明秘钥与验证密钥、计算R1CS witness、生成证明、验证证明。
./zokrates compile -i 'add.code'
./zokrates setup
./zokrates compute-witness -a 1 2 3
./zokrates generate-proof
./zokrates export-verifier

实现细节 - gadgetlib1-based

验证每个输入来自于 \{1,2, 3, 4\}

/*
 * validate Input comes from the set {v1, ..., vn}
 * equivalent to constraint (x - v1) ... (x - vn) = 0
 */
template<typename FieldT>
class validateInput_gadget : public gadget<FieldT> {
private:
public:
    std::vector<int> values;
    pb_variable<FieldT> x;
    pb_variable_array<FieldT> intermediates;

    validateInput_gadget(protoboard<FieldT> &pb,
                         const pb_variable<FieldT> &x,
                         const std::vector<int> &values,
                         const std::string &annotation_prefix = "validate Input") :
            gadget<FieldT>(pb, annotation_prefix), x(x), values(values) {
        intermediates.allocate(pb, values.size() - 1, FMT(annotation_prefix, "intermediates"));
    }

    void generate_r1cs_constraints() {
        this->pb.add_r1cs_constraint(r1cs_constraint<FieldT>(
                {ONE * (-values[0]), x},
                {ONE * (-values[1]), x},
                {intermediates[0]}));

        for (size_t i = 1; i < values.size() - 1; i++) {
            this->pb.add_r1cs_constraint(r1cs_constraint<FieldT>(
                    {ONE * (-values[i]), x},
                    {intermediates[i - 1]},
                    {intermediates[i]}));
        }
    }
}


验证每一行、每一列、子网格的输入不重复

/*
 * validate that the elements in inputs are different from each other
 * which is equivalent to the constraint that \prod (inputs[i] - inputs[j]) * inv = 1
 */
template<typename FieldT>
class checkEquality_gadget : public gadget<FieldT> {
private:
public:
    pb_variable<FieldT> inv;
    pb_variable_array<FieldT> inputs;
    pb_variable_array<FieldT> intermediates;

    checkEquality_gadget(protoboard<FieldT> &pb,
                         const pb_variable_array<FieldT> &inputs,
                         const std::string &annotation_prefix = "checkEquality") :
            gadget<FieldT>(pb, annotation_prefix), inputs(inputs) {

        inv.allocate(pb, FMT(annotation_prefix, "inv"));

        int num = inputs.size() * (inputs.size() - 1) / 2;
        intermediates.allocate(pb, num, FMT(annotation_prefix, "intermediates"));
    }

    void generate_r1cs_constraints() {
        size_t counter = 0;
        for (size_t i = 0; i < inputs.size() - 1; i++) {
            for (size_t j = i + 1; j < inputs.size(); j++) {
                if (i == 0 && j == 1) {
                    this->pb.add_r1cs_constraint(r1cs_constraint<FieldT>(
                            {ONE},
                            {inputs[i], inputs[j] * (-1)},
                            {intermediates[0]}));
                    counter++;
                } else {
                    this->pb.add_r1cs_constraint(r1cs_constraint<FieldT>(
                            {intermediates[counter - 1]},
                            {inputs[i], inputs[j] * (-1)},
                            {intermediates[counter]}
                    ));
                    counter++;
                }
            }
        }
        this->pb.add_r1cs_constraint(r1cs_constraint<FieldT>(
                {intermediates[intermediates.size() - 1]},
                {inv},
                {ONE}
        ));
    }
}



总结

借助于最近几年这个领域的发展,实现一个复杂逻辑的非交互式零知识证明协议,已经是很容易的事情。本文希望能够揭开zkp的神秘面纱,真实可用的例子可以让大家对零知识证明更容易理解。如果熟悉整套工具链的开发的话,zkp真的可以构造一些特别有意思的应用。


我已将此样例的代码上传到Github上了。传送门:sudoku-zk-snarks


PS:以太坊于2017年10月份进行的Byzantinum硬分叉中,已经激活了椭圆曲线点加、倍点、pairing计算功能【EIP196、EIP197】,因此基于这三个precompiled contracts可以实现Pinocchio协议中的证明验证功能。

编辑于 2018-11-08

文章被以下专栏收录