libsnark教程

原文出自:libsnark库
作者:libsnark贡献者
译者:Matter实验室

gadgetlib2库的使用以及如何整合ppzkSNARK的教程和范例。这分文档专为小零件(gadget)写的,推荐从上至下作为教程来阅读。

1. 面包板(protoboard)的使用和范例

这个测试给出构造系统限制(constraint)的第一个范例。我们将时不时交替使用 ‘系统限制’(Constraint) 和 ‘电路’(Circuit) 两个术语. 用输入和输出形象化一个电路非常容易。每个门强制规定输入和输出线之间的逻辑。例如,AND(inp1,inp2) 会强制 (inp1 & inp2 = out) 这个限制。因此,我们也能认为这个电路是一个限制系统。每个门是一个数学限制,每个线是一个变量。在AND的例子中,通过boolean类型{0,1}我们将写出一个像(inp1*inp2==out)这样的限制。如果我们指派值到{inp1,inp2,out},并且这个限制对于这样的赋值是满足的,因此对这个限制的评估就是TRUE。所有接下来的例子是要么是领域不可知或素数字段的特殊形式。

  1. 领域不可知情况:在这些例子中,我们用低级电路来创建一个高级电路。这种方法我们可以忽略一个字段的具体的情况,并假定低级电路会处理这个。如果我们必须在这些电路中显示的写出限制。这种情况常常是一下非常基础的限制,并可以定义在每个字段上,例如(e.g. x+y=0)。

  2. 所有字段都具体的例子,在这个库中,是对素数性质的字段来说的,采用的是‘二次秩1多项式’或者叫R1P 。这是当前SNARKs实现的唯一形式。这种形式专门处理像(Linear Combination) * (Linear Combination) == (Linear Combination)这样的限制。这个库已经被设计成允许未来加入其他的特性或形式,目前只为这样的形式实现了低级电路。

1. 代码与过程

初始化素数字段参数,R1P总是需要这么做。

initPublicParamsFromDefaultPp();

面包版是一个“内存管理器”,它维护所有的限制(当创建验证电路的时候)和变量赋值(当创建证明证人的时候)。我们特别说明这个R1P的类型,能在未来增加对BOOLEAN或GF2_EXTENSION字段的支持。

ProtoboardPtr pb = Protoboard::create(R1P);

现在我们创建三个输入和一个输出

VariableArray input(3, "input");
Variable output("output");

我们现在可以添加一些限制,其中字符串的目的是为了调试方便,并且能够给这个限制一个文本说明。

pb->addRank1Constraint(
    input[0],
    5 + input[2],
    output,
    "Constraint 1: input[0] * (5 + input[2]) == output"
);

第二种形式是添加一个一元限制,这个意味着(LinearCombination == 0)

pb->addUnaryConstraint(
    input[1] - output,
    "Constraint 2: input[1] - output == 0"
);

注意到这样写也是可以的:

pb->addRank1Constraint(1, input[1] - input[2], 0, "");

对于更加一般化形式的字段,可以一次性实现,我们可以使用addGeneralConstraint(Polynomial1, Polynomial2, string)会转换成(Polynomial1 == Polynomial2)限制。例如:

pb->addGeneralConstraint(
  input[0] * (3 + input[1]) * input[2], 
  output + 5,
  "input[0] * (3 + input[1]) * input[2] == output + 5"
);

现在我们能将值赋给变量了,并且看一看限制是否满足。之后,当我们运行SNARK(或者任何其他证明系统)的时候,这些限制将会被验证者(verifier)使用,并且指派的值被证明者(prover)使用。

注意到面包版(protoboard)存储了被指派的值。

pb->val(input[0]) =
pb->val(input[1]) =
pb->val(input[2]) =
pb->val(output) = 42;

EXPECT_FALSE(pb->isSatisfied());

这个限制系统是没有被满足的,现在让我们尝试一下可以满足上面两个等式的值。
The constraint system is not satisfied. Now let's try values which satisfy the two equations above:

pb->val(input[0]) = 1;
pb->val(input[1]) = 
pb->val(output) = 42;    // input[1]-output == 0
pb->val(input[2]) = 37;  // 1*(5+37)==42

EXPECT_TRUE(pb->isSatisfied());

2. Gadget与非的例子

在上面的例子中,我们清晰的写了所有的限制和赋值。

在这个例子中,我们将构造一个非常简单的gadget,一个实现了与非(NAND)的门。这个gadget是字段无关的,因为它只使用了低级的gadget以及字段元素 '0' & '1'

Gadget是一个允许我们委托复杂电路元件到低层的框架。依靠限定与赋值以及利用子gadget,每一个gadget能够构造一个限定系统或证明者,或是两者一起。。

1. 主过程

下面这个测试用来说明使用方法:

初始化字段

    initPublicParamsFromDefaultPp();

用R1P系统的类型创建一个面包板。

    ProtoboardPtr pb = Protoboard::create(R1P);

创建5个变量inputs[0]...inputs[4]。字符串“inputs”是调试信息。

    FlagVariableArray inputs(5, "inputs");
    FlagVariable output("output");
    GadgetPtr nandGadget = NAND_Gadget::create(pb, inputs, output);

现在我们能生成一个限定系统(或电路)

    nandGadget->generateConstraints();

如果我们现在尝试去计算这个电路,一个异常会被抛出来,因为我们企图计算没有赋值的变量。

    EXPECT_ANY_THROW(pb->isSatisfied());

因此让我们对这个输入变量赋值,这么就可以进行与非(NAND)计算,并且在创建证据之后,再次尝试计算这个电路。

    for (const auto& input : inputs) {
        pb->val(input) = 1;
    }
    nandGadget->generateWitness();
    EXPECT_TRUE(pb->isSatisfied());
    EXPECT_TRUE(pb->val(output) == 0);

现在让我们毁坏某些东西,并看看发生了什么?

    pb->val(inputs[2]) = 0;
    EXPECT_FALSE(pb->isSatisfied());

现在让我们尝试欺骗一下。如果我们没有强制要求boolean化,这应该是能运行的。

    pb->val(inputs[1]) = 2;
    EXPECT_FALSE(pb->isSatisfied());

现在让我们重新设置inputs[1]为一个正确的值。

    pb->val(inputs[1]) = 1;

之前我们设置了inputs和output。注意到output仍然是'0'

    // before, we set both the inputs and the output. Notice the output is still set to '0'
    EXPECT_TRUE(pb->val(output) == 0);

现在我们将使gadget利用generateWitness() 创建证据计算出结果并且看发生了什么?

    nandGadget->generateWitness();
    EXPECT_TRUE(pb->val(output) == 1);
    EXPECT_TRUE(pb->isSatisfied());

3. hash难度执行者例子

另外一个例子展示双重变量的使用。一个双重变量是一个具有下面两种特性的变量,字的安位表现形式,以及可包装的展现形式(例如,包装值 {42} 和非包装值 {1,0,1,0,1,0} )。如果这个字够短(比如,小于它主要特征的任何整数),那么包装起来的表达方式将被存储在一个元素字段中。字在这个上下文中的意思是一组二进制位,这是一个惯例,意味着我们期待一些语义能力去分解包装的值到二进制位去。

使用双重变量是为了提高效率。更多的展示在例子的结尾。在这个例子中我们将构造一个gadget,这个gadget接收一个包装的整数作为输入称为'hash',和一个二进制位的‘难度’级别,以及构造一个用来证明‘hash’上第一个‘难度’位是0的电路。为了简单,我们将假定‘hash’总是64位长度。

1. 主过程

记得我们指出,双重变量被用来提升效率。现在就是详细说明这个的时候。就像你看到的,我们需要一个位表达来检查hashValue的第一个位。但是hashValue可能被用于很多其他的地方,因为我们想要检查实例是否与另一个值相等。在包装的表达上检查相等会‘消耗’我们一个限定,当在没有包装的值上检查相等的时候,将‘消耗’我们64个限制。在ppzkSNARK证明系统中,这个转换会加重构造证明的时间和内存消耗。

    initPublicParamsFromDefaultPp();
    auto pb = Protoboard::create(R1P);
    const MultiPackedWord hashValue(64, R1P, "hashValue");
    const size_t difficulty = 10;
    auto difficultyEnforcer = HashDifficultyEnforcer_Gadget::create(pb, hashValue, difficulty);
    difficultyEnforcer->generateConstraints();

现在限制以及创建,但是还没有赋值。在计算是会抛出异常。

    EXPECT_ANY_THROW(pb->isSatisfied());
    pb->val(hashValue[0]) = 42;
    difficultyEnforcer->generateWitness();

42的起始的10个位(当以64位数形式展示的时候)都是'0',因此应该可以工作。

    EXPECT_TRUE(pb->isSatisfied(PrintOptions::DBG_PRINT_IF_NOT_SATISFIED));
    pb->val(hashValue[0]) = 1000000000000000000;

这个值大于2^54,因此我们期待限制系统不会被满足。
在断言之前,generateWitness应该就失败了。

    difficultyEnforcer->generateWitness();
    EXPECT_FALSE(pb->isSatisfied());

4. R1P验证交易金额例子

在这个例子中,我们将构造一个gadget,这个gadget构建了一个证明(证据)电路,并且认可(限制)一个比特币交易输入的数量必须等于输出+矿工费。证明的限制将包括找到矿工的费用。这个费用能够作为电路的输出被考虑。

这是特定领域的gadget,就像我们将要用的 '+'操作一样自由。在主特性领域而非在扩展领域,加法操作像所期望的一样工作在整数上。如果你不熟悉扩展领域,不用担心。应该简单的意识到 +* 在不同的领域表现得不一样,并且不一定给出你期望的整数值。

由于要用到不同的场景下,这个库的设计支持多域构造。当其他的应用使用主要域的时候一些加密图应用可能需要扩展域,但是,用使用非rank-1的限制,并且有些可能还需要boolean电路。在新域或限制构造上,这个库的设计使高层次的gadget能够重用底层的实现。

之后通过使用不可知接口,我们将提供一个创建这种特殊领域gadget的诀窍。我们在这儿使用一些惯例,采用宏将这个过程变得容易。

1. Gadget定义

这是一个为所有的特定域创建一个继承自gadget接口类的宏。
惯用法是:class {GadgetName}_GadgetBase

CREATE_GADGET_BASE_CLASS(VerifyTransactionAmounts_GadgetBase);

注意这里的多继承。我们必须指定接口以及基础gadget的特殊域。这允许类工厂在编译期决定,特定的类需要为每个面包板实例化哪种域。可以在"gadget.hpp"中看到这个设计的信息。

惯用法:class {FieldType}_{GadgetName}_Gadget

#1: 我们给出工厂类的友元访问为了实例化私有的构造函数。

class R1P_VerifyTransactionAmounts_Gadget : public VerifyTransactionAmounts_GadgetBase,
                                            public R1P_Gadget {
public:
    void generateConstraints();
    void generateWitness();

    friend class VerifyTransactionAmounts_Gadget;  // #1
private:
    R1P_VerifyTransactionAmounts_Gadget(ProtoboardPtr pb,
                                        const VariableArray& txInputAmounts,
                                        const VariableArray& txOutputAmounts,
                                        const Variable& minersFee);
    void init();

    const VariableArray txInputAmounts_;
    const VariableArray txOutputAmounts_;
    const Variable minersFee_;

    DISALLOW_COPY_AND_ASSIGN(R1P_VerifyTransactionAmounts_Gadget);
};

使用宏CREATE_GADGET_FACTORY_CLASS_XX创建工厂类(用构造函数的参数数量替换XX,protoboard不算)。有时候我们像要多构造函数,可以参照AND_Gadget。在这个场景下,我们将手工写出工厂类。

CREATE_GADGET_FACTORY_CLASS_3(
          VerifyTransactionAmounts_Gadget,
          VariableArray, txInputAmounts,
          VariableArray, txOutputAmounts,
          Variable, minersFee
);
2. Gadget实现

为基类实现空析构函数

VerifyTransactionAmounts_GadgetBase::~VerifyTransactionAmounts_GadgetBase() {}

看起来下面这点实现就足够了,但是一个对抗者可能在域的模数上引起等式的一边溢出。事实上,对于每个输入/输出的和我们总是会找到一个矿工的服务费满足这个限制。只剩下一个给读者的练习,实现一个加法限制(和证据),用来检查每个数量(输入,输出,费用)在0和21,000,000 * 1E8 中本村之间。用最大的输入/输出数量组合这个数防止域溢出。

提示:使用Comparison_Gadget创建gadget,这个gadget将一个变量的值与常数做比较。使用这些新gadget的数组去检查各自的值。
不要忘记:

  1. 在init()中连接这些gadgets。
  2. 在generateConstraints()中调用这些gadgets的限定。
  3. 在generateWitness()中调用这些gadgets的证据。

#1 注意我们必须初始化3个基类(菱形继承)

void R1P_VerifyTransactionAmounts_Gadget::generateConstraints() {
    addUnaryConstraint(
        sum(txInputAmounts_) - sum(txOutputAmounts_) - minersFee_,
        "sum(txInputAmounts) == sum(txOutputAmounts) + minersFee"
    );
}

void R1P_VerifyTransactionAmounts_Gadget::generateWitness() {
    FElem sumInputs = 0;
    FElem sumOutputs = 0;
    for (const auto& inputAmount : txInputAmounts_) {
        sumInputs += val(inputAmount);
    }
    for (const auto& outputAmount : txOutputAmounts_) {
        sumOutputs += val(outputAmount);
    }
    val(minersFee_) = sumInputs - sumOutputs;
}

R1P_VerifyTransactionAmounts_Gadget::R1P_VerifyTransactionAmounts_Gadget(
        ProtoboardPtr pb,
        const VariableArray& txInputAmounts,
        const VariableArray& txOutputAmounts,
        const Variable& minersFee
):
        Gadget(pb),
        VerifyTransactionAmounts_GadgetBase(pb),
        R1P_Gadget(pb),   // #1
        txInputAmounts_(txInputAmounts), 
        txOutputAmounts_(txOutputAmounts),
        minersFee_(minersFee) {}

void R1P_VerifyTransactionAmounts_Gadget::init() {}
2. 主流程

按照约定,用不可知接口创建一个特定域gadgets的诀窍是:

  1. 用宏创建一个基类:
    CREATE_GADGET_BASE_CLASS({GadgetName}_GadgetBase);

  2. 为基类创建一个构造函数:
    {GadgetName_Gadget}Base::~{GadgetName}_GadgetBase() {}

  3. 使用多继承创建你需要的特定域gadgets。
    class {FieldType}_{GadgetName}_Gadget :
    public {GadgetName}_GadgetBase,
    public {FieldType_Gadget}

    注意到为了使用工厂类宏,构造函数的所有参数必须是 const&。构造参数必须与所有的特定域实现一致。

  4. 给工厂类{GadgetName}_Gadget友元访问特定域类的权限。

  5. 采用宏创建工厂类。
    CREATE_GADGET_FACTORY_CLASS_XX(
    {GadgetName}_Gadget,
    type1, input1, type2, input2, ... ,
    typeXX, inputXX
    );

void examples_r1p_verify_transaction_amounts() {
    initPublicParamsFromDefaultPp();
    auto pb = Protoboard::create(R1P);
    const VariableArray inputAmounts(2, "inputAmounts");
    const VariableArray outputAmounts(3, "outputAmounts");
    const Variable minersFee("minersFee");
    auto verifyTx = VerifyTransactionAmounts_Gadget::create(
        pb,
        inputAmounts,
        outputAmounts,
        minersFee
    );
    verifyTx->generateConstraints();
    pb->val(inputAmounts[0]) = pb->val(inputAmounts[1]) = 2;
    pb->val(outputAmounts[0]) = 
            pb->val(outputAmounts[1]) = 
                     pb->val(outputAmounts[2]) = 1;
    verifyTx->generateWitness();
    EXPECT_TRUE(pb->isSatisfied());
    EXPECT_EQ(pb->val(minersFee), 1);
    pb->val(minersFee) = 3;
    EXPECT_FALSE(pb->isSatisfied());
}

5. 整合gadgetlib2和ppzkSNARK

下面是一个整合gadgetlib2所构造的限制系统和ppzkSNARK的例子。

1. 主流程
    initPublicParamsFromDefaultPp();

创建一个限制系统的例子,并翻译成libsnark的格式化。

const libsnark::r1cs_example<
        libff::Fr<
            libff::default_ec_pp
        >
> example = libsnark::gen_r1cs_example_from_gadgetlib2_protoboard(100);
const bool test_serialization = false;

运行ppzksnark,跳转到分析函数。

const bool bit = libsnark::run_r1cs_ppzksnark<
    libff::default_ec_pp
>(example, test_serialization);

EXPECT_TRUE(bit);
2. r1cs创建

限制系统在gen_r1cs_example_from_gadgetlib2_protoboard中创建,我们来看看里面的过程是什么。

注意:这个例子确实创建了一个限制,这个限制至少说明说明了QAP限制的健全性。

r1cs_example<
    libff::Fr<
        libff::default_ec_pp
    >
>
gen_r1cs_example_from_gadgetlib2_protoboard(const size_t size)
{
    typedef libff::Fr FieldT;

    gadgetlib2::initPublicParamsFromDefaultPp();

必要的情况下,在之前就建立一个面包板,libsnark假定变量索引总是从0开始,因此我们必须在创建被libsnark使用的限制之前重设索引。

    gadgetlib2::GadgetLibAdapter::resetVariableIndex();

创建一个gadgetlib2的gadget。这个部分靠生成器和证明器实现。

    auto pb = gadgetlib2::Protoboard::create(gadgetlib2::R1P);
    gadgetlib2::VariableArray A(size, "A");
    gadgetlib2::VariableArray B(size, "B");
    gadgetlib2::Variable result("result");
    auto g = gadgetlib2::InnerProduct_Gadget::create(pb, A, B, result);

创建一个限制,这个部分靠生成器完成。

    g->generateConstraints();

创建赋值(证据)。这个部分是证明者完成的。

    for (size_t k = 0; k < size; ++k)
    {
        pb->val(A[k]) = std::rand() % 2;
        pb->val(B[k]) = std::rand() % 2;
    }
    g->generateWitness();

将限制系统转换成libsnark格式。

    r1cs_constraint_system cs = 
                            get_constraint_system_from_gadgetlib2(*pb);

转换所有的变量的赋值到libsnark格式。

    const r1cs_variable_assignment full_assignment =
                             get_variable_assignment_from_gadgetlib2(*pb);

提取主要的和辅助的输入

    const r1cs_primary_input primary_input(
               full_assignment.begin(), 
               full_assignment.begin() + cs.num_inputs()
    );
    const r1cs_auxiliary_input auxiliary_input(
               full_assignment.begin() + cs.num_inputs(), 
               full_assignment.end()
    );

    assert(cs.is_valid());
    assert(cs.is_satisfied(primary_input, auxiliary_input));

    return r1cs_example(cs, primary_input, auxiliary_input);
}
3. 运行ppzksnark

下面的代码提供了一个以R1CS的形式运行ppzkSNARK的全场景的例子。
当然,在实际的情景中,我们有三个明显的实体,下面将乱入到一个演示实例中。这三种实体如下:

  1. 生成器:它运行ppzkSNARK生成器,输入一个给定的限制系统CS来创建一个证明过程和一个对于CS来说可验证的key。

  2. 证明者:运行ppzkSNARK的证明者,输入一个证明用的key。

  3. 验证者:运行ppzkSNARK验证者,输入一个可验证的key,一个CS的主输入,和一个证据。

template
bool run_r1cs_ppzksnark(const r1cs_example > &example,
                        const bool test_serialization)
{
    //libff::enter_block("Call to run_r1cs_ppzksnark");

    //libff::print_header("R1CS ppzkSNARK Generator");
    r1cs_ppzksnark_keypair keypair = 
                   r1cs_ppzksnark_generator(example.constraint_system);
    //printf("\n");
    //libff::print_indent();
    //libff::print_mem("after generator");

    //libff::print_header("Preprocess verification key");
    r1cs_ppzksnark_processed_verification_key pvk = 
                   r1cs_ppzksnark_verifier_process_vk(keypair.vk);

    if (test_serialization) {
        //libff::enter_block("Test serialization of keys");
        keypair.pk = 
                 libff::reserialize<
                      r1cs_ppzksnark_proving_key 
                 >(keypair.pk);
        keypair.vk = 
                 libff::reserialize<
                      r1cs_ppzksnark_verification_key 
                 >(keypair.vk);
        pvk = 
                libff::reserialize<
                      r1cs_ppzksnark_processed_verification_key 
                >(pvk);
        //libff::leave_block("Test serialization of keys");
    }

    //libff::print_header("R1CS ppzkSNARK Prover");
    r1cs_ppzksnark_proof proof = 
                r1cs_ppzksnark_prover(
                       keypair.pk, 
                       example.primary_input, 
                       example.auxiliary_input
    );
    //printf("\n"); 
    //libff::print_indent(); 
    //libff::print_mem("after prover");

    if (test_serialization)
    {
        //libff::enter_block("Test serialization of proof");
        proof = libff::reserialize<
               r1cs_ppzksnark_proof 
        >(proof);
        //libff::leave_block("Test serialization of proof");
    }

    //libff::print_header("R1CS ppzkSNARK Verifier");
    const bool ans = 
               r1cs_ppzksnark_verifier_strong_IC(
                   keypair.vk, 
                   example.primary_input, 
                   proof
    );
    //printf("\n"); 
    //libff::print_indent(); 
    //libff::print_mem("after verifier");
    //printf("* The verification result is: %s\n", (ans ? "PASS" : "FAIL"));

    //libff::print_header("R1CS ppzkSNARK Online Verifier");
    const bool ans2 = 
                  r1cs_ppzksnark_online_verifier_strong_IC(
                         pvk, 
                         example.primary_input, 
                         proof
    );
    assert(ans == ans2);

    test_affine_verifier(
                  keypair.vk, 
                  example.primary_input, 
                  proof, 
                  ans
    );

    //libff::leave_block("Call to run_r1cs_ppzksnark");

    return ans;
}

译者总结:

结构图

你可能感兴趣的:(libsnark教程)