zk-SNARKs 在 Solidity 中的使用以及一些概念的解释

在这里插入图片描述

本文章主要讲述了如何在 Solidity 中使用 zk-SNARKs,以及如何使用 ZoKrates 编译器来生成证明和验证合约。

这文章不会过于深入 zk-SNARKs 的技术原理,这文章目的是为让读者能够理解 sk-SNARKs 的技术能在 EVM 中达到什么效果,如何使用,并且能在代码中运用。

一、zk-SNARKs 简介

关于 zk-SNARKs 的简短描述为,我们需要在 zk 电路中编写一段代码,这段代码的输入是一些公开的数据,输出是一些私有的数据。zk-SNARKs 的验证算法可以验证这段代码的输出是否正确,但是验证算法不会泄露任何私有数据。而 Solidity 合约的主要目的是验证 zk-SNARKs 的验证算法的结果,如果验证算法的结果正确,那么合约会执行一些操作。

也就是说,在 EVM 上,只是进行了结果的验证,并没有进行一些复杂的计算,这些计算都是在 zk 电路中进行的。而这部分 zk 电路,则是在链下进行的,然后将结果提交到链上。

解读

1.上述文字是正确的吗?

您提供的文字基本上是正确的,但还有一些额外的细节值得澄清:

  1. **zk-SNARKs(Zero-Knowledge Succinct Non-Interactive Argument of Knowledge)**确实用于验证某段代码的输出是否正确,而不泄露私有数据。zk-SNARKs的验证算法允许验证者验证某个计算的正确性,同时保持计算的隐私性。验证者只需要知道计算的结果是否正确,而不需要了解输入的具体值。

  2. 在zk-SNARKs中,您提到的zk电路确实是一个关键组成部分。zk电路描述了计算的约束和规则,允许验证者验证计算的正确性。这些约束是在链下生成的,并且通常使用工具如ZoKrates来创建。

  3. Solidity合约可以用于验证zk-SNARKs的验证算法的结果。如果验证算法的结果正确,合约可以执行特定的操作,这通常涉及到链上的状态更改或智能合约的调用。合约的主要作用是在链上进行验证,并执行相应的操作。

  4. 您提到的zk电路通常是在链下生成的,然后将其编译成可在链上使用的形式。这一过程通常由开发人员在本地环境中完成,然后生成的zk电路的验证逻辑会被部署到区块链上,以供链上合约验证。

总之,您的描述基本正确,它强调了zk-SNARKs的隐私保护和验证功能,以及在链上和链下的分工。不过,需要注意的是zk电路的生成和验证过程通常在链下完成,但验证结果会在链上进行。此外,验证算法的复杂性和执行可能会因具体应用而异。

2.验证算法允许验证者验证某个计算的正确性,而不需要了解具体的输入值,这个特性是否与“关于 zk-SNARKs 的简短描述为,我们需要在 zk 电路中编写一段代码,这段代码的输入是一些公开的数据”这句话中的公开的数据相矛盾?

您提出的问题很有深度,让我进一步澄清这个概念。

在 zk-SNARKs(零知识可证明系统)中,的确有一个值得注意的概念,即“公开的数据”和“私有的数据”。这两种数据的处理方式不同:

  • 公开的数据:这些是可以在链上或其他公开可访问的地方看到的数据。这些数据可以是智能合约的参数,也可以是链上交易的一部分,因此它们是公开的,所有人都可以看到它们。

  • 私有的数据:这些是不应该被公开的数据,可能包含敏感信息。在zk-SNARKs中,验证者(通常是智能合约)可以验证某个计算的正确性,同时不需要知道具体的私有输入数据的值。这是zk-SNARKs的核心特性之一,即验证者可以验证计算的正确性,但不必泄露私有数据。

因此,与您的描述一致,zk-SNARKs允许在zk电路中编写代码,其中包含公开的数据作为输入。这些公开的数据是可以被验证者(例如智能合约)访问的,但不包括私有数据的具体值。验证者只需要知道计算的结果是否正确,而不需要了解私有数据的值。

这里的关键是,zk-SNARKs允许在计算中使用公开的数据,同时保护私有数据的隐私。因此,不会有矛盾,因为公开的数据和私有数据在这种情况下是不同的概念,而zk-SNARKs的设计目标之一就是处理这种差异。

3.可以理解为:在一次“生成证明,验证”的过程中,允许同时存在公开数据和私有数据对吗?

是的,您理解得正确。在zk-SNARKs中,在一次生成证明和验证的过程中,允许同时存在公开数据和私有数据。

  • 公开数据:这些数据是在生成证明和验证时可以访问和使用的数据,它们可以是智能合约的参数或其他公开可访问的数据。验证者可以查看和使用这些数据。

  • 私有数据:这些数据是在生成证明和验证时不会泄露的数据。验证者不会知道私有数据的具体值,但可以验证基于这些私有数据计算出的结果是否正确。

zk-SNARKs的核心目标之一是允许在验证过程中使用私有数据,同时保护这些数据的隐私。这使得可以进行有效的计算验证,而不需要暴露私有信息的细节。所以,在zk-SNARKs中,公开数据和私有数据是共存的,它们的处理方式不同,但都可以在生成证明和验证的过程中使用。

二、在 Solidity 中使用 zk-SNARKs

首先,我们需要知道 zk-SNARKs 可以完成什么功能,其实很简单,我们可以简单的认为,zk-SNARKs 可以完成对一个函数运算结果的校验,比如说,我们有一个函数,输入是三个数字,输出是一个数字,我们可以使用 zk-SNARKs 来校验这个函数的输出是否正确。但是我们并不需要知道输入的三个数字是什么,只需要知道这个函数的输出即可,也就是说,在一个函数完成计算时,我们可以知道确实是有这么三个数他能符合这个函数的输入,并且能输出正确结果,但是我们并不知道这三个数是什么。

在 Solidity 中,我们可以使用 zk-SNARKs 来完成对一个函数的校验,但是我们需要知道这个函数的输入和输出,然后我们可以使用 ZoKrates 编译器来生成 zk 电路,然后将 zk 电路的代码放到 Solidity 合约中,然后在合约中完成对 zk 电路的验证。

解读

1.根据这个说法,zk电路和函数的关系可以认为是:zk电路是函数经过加工后的产物,这样理解对吗?

是的,您可以将zk电路视为经过加工后的函数的产物,但需要明确一些细节。

  1. zk电路是函数的表示:zk电路通常是对一个函数进行形式化的表示,而不是函数本身。这个表示包括了函数的输入、输出以及计算过程中的各个中间步骤,以一种可验证的方式呈现。这个表示允许验证者验证计算的正确性,而不需要知道具体的输入值。

  2. 函数的转化:为了生成zk电路,需要将函数的逻辑和计算过程转化为一系列约束和验证规则。这个过程通常由zk-SNARKs生成工具(如ZoKrates)来完成。在这个过程中,函数被分解为一系列算术和逻辑操作,以便能够生成可验证的证明。

  3. 保护隐私:zk电路的一个关键特点是它可以保护函数的输入数据的隐私。验证者可以验证计算的正确性,但不需要知道具体的输入值。这使得zk电路在保护敏感信息的同时允许验证。

所以,可以将zk电路视为函数的一种可验证表示,它允许验证函数的正确性和输入数据的隐私,但它不是函数本身,而是函数的形式化描述。这种表示允许验证者在不公开具体输入的情况下进行验证。

2.电路是公开的,而函数本身是非公开的,这样理解对吗?

是的,您的理解基本正确。

  • 电路(zk电路)通常是公开的,因为它是用于验证计算的规则和约束的一种表示形式。这些规则和约束是公开的,以便验证者可以使用它们来验证计算的正确性。电路的设计和生成通常是一个公开的过程。

  • 函数本身通常是非公开的,因为它包含具体的计算逻辑和输入数据。函数的内部实现和输入数据通常是保密的,只有函数的拥有者或授权用户才能访问它们。这些信息通常不会公开。

zk-SNARKs的一个关键目标是允许在不公开具体输入数据的情况下验证函数的正确性。为了达到这个目标,函数的逻辑被转化为公开的电路,但该电路不会公开输入数据的具体值。验证者可以使用这个公开的电路来验证计算的正确性,而不需要知道输入的敏感信息。这种方法允许保护隐私同时进行验证。

3.函数生成zk电路的过程,和“多项式”有什么关系呢?

在生成zk电路的过程中,多项式在某种程度上起到了关键作用。让我解释一下多项式与zk电路之间的关系:

  1. 多项式表示:在zk-SNARKs中,函数通常被表示为多项式。这是因为多项式具有一些有用的属性,使得它们适合于零知识证明系统的使用。具体来说,一个函数可以被表示为一个多项式,其中多项式的系数对应于函数的输入和输出。这个多项式通常是一个多项式约束系统(Polynomial Constraint System)的一部分。

  2. 多项式约束系统:多项式约束系统是一组多项式,用于表示函数的计算过程和约束。这些多项式描述了计算过程中的各个步骤,包括输入、输出和中间变量之间的关系。验证者可以使用这些多项式来验证计算的正确性。

  3. 多项式插值:在生成zk电路的过程中,通常需要进行多项式插值。这意味着将函数的计算过程转化为多项式约束系统,以便生成可验证的zk电路。多项式插值是一个数学技术,它将已知的数据点映射到多项式上,从而可以对多项式进行操作和验证。

  4. zk电路的生成:一旦多项式约束系统生成,它可以被转化为zk电路的规则和约束。这个过程包括将多项式约束系统的方程转化为可验证的算术约束,以便验证者可以使用zk-SNARKs来生成和验证证明。

总之,多项式在zk-SNARKs中用于表示和约束函数的计算过程,多项式约束系统用于描述多项式之间的关系,而多项式插值用于将函数的计算转化为多项式形式,以便生成zk电路。这些技术一起帮助构建了zk-SNARKs系统,使其能够在不公开具体输入的情况下验证函数的正确性。

4.zk电路和加法同态是什么关系?

zk电路(Zero-Knowledge Circuit)和加法同态(Additive Homomorphism)之间存在一定关系,但它们是不同的概念。

  1. zk电路(Zero-Knowledge Circuit):zk电路是一种用于零知识证明系统的表示形式,它描述了计算过程的约束和规则,允许验证者在不了解具体输入的情况下验证计算的正确性。zk电路通常是一系列约束,这些约束描述了输入、输出和中间变量之间的关系。zk电路可以用于证明计算的正确性,同时保护输入数据的隐私。

  2. 加法同态(Additive Homomorphism):加法同态是一种数学属性,它指的是当两个值相加时,它们的同态映射的结果等于它们的同态映射相加的结果。这意味着如果有两个值x和y,它们的同态映射分别是f(x)和f(y),那么f(x + y)等于f(x) + f(y)。这种属性在密码学和加密中非常有用,因为它允许在加密域中执行加法操作而不需要解密。

关系:在某些情况下,zk电路中的约束可以设计为具有加法同态性质。这意味着可以对两个已加密的值进行计算并生成一个加密的结果,而不需要暴露原始输入数据。这种方法可以用于在零知识证明系统中验证计算的正确性,同时保护输入数据的隐私。这种关系使得加法同态密码学在zk-SNARKs和其他零知识证明系统中有广泛的应用。

三、solidity实现零知识证明具体示例

1.安装 ZoKrates 编译器

安装 ZoKrates

curl -LSfs get.zokrat.es | sh

也可以选择其他安装方式,具体选择查看他们的 Github 页面。

在这里插入图片描述

2.准备一个函数

我们首先需要一个函数,但是我并不打算举一些简单例子,因为我觉得这样做非常没有意义,因为 zk-SNARKs 的主要目的是为了验证一些复杂的函数,而不是一些简单的函数。

比如,我们现在需要生成一个存款凭证,有这个凭证,我们可以在任何地方取出这笔钱,但是我们并不知道这笔钱是谁的,我们只知道这笔钱是谁存的,存了多少,以及存款的时间。

首先我们需要一个存款函数,这个函数的输入为存款的金额,和一个随机数,然后输出为一个存款凭证。任何拥有这个凭证的人都可以取出这笔资金。所以,实际上,我们只需要编写验证知道这个凭证的验证函数即可。

下面是Zok代码实现的该函数:

import "hashes/sha256/512bit" as sha256;
import "utils/pack/u32/nonStrictUnpack256" as unpack256;

// deposit_amount: 存款金额
// secret: 随机数
// returns: 用于取款的commitment
def main(field deposit_amount, private field secret) -> u32[8] {
    return sha256(unpack256(deposit_amount), unpack256(secret));
}

关于 Zok 的语法和用法这里不过多描述,具体可以参考官网,这里简单解释一下,这个函数的输入为两个数字,一个是存款金额,一个是随机数,然后输出为一个 u32[8],实际上就是 uint256.同时我们注意一下,参数中 deposit_amount 没有 private 关键词,说明这个参数是公开数据。

3.编写 zk 电路

从上一章节我们浅显的知道,一个 zk-SNARKs 电路需要的最基本的东西为:

一个函数 - 我们需要有一个函数对数据进行运算,也就是程序 C
lambda - 所谓的“有毒废料”,其实就是一个 root key,我们需要通过它来生成 pk 和 vk
有了这两个基础条件,用户就可以通过 pk,目标值,输入值来生成证明 w。
随后,我们的验证程序通过 vk,目标值,证明 w 来验证证明的正确性。

我们先假设有这么一个第三方,他可以安全的生成 lambda,然后安全的将程序和 lambda 进行运算生成 vk 和 pk。

那么现在有两个新的角色,user 和 project。user 是用户,他确确实实拥有着一些数据,project 是项目合约,他需要验证用户的数据是否正确。

3.1.在上述文字中,pk和vk分别是什么意思?

在上述文字中,pk 和 vk 分别代表:

  • pk(Public Key):这是公钥,通常用于生成证明。公钥是一个 zk-SNARKs 电路的公开部分,它可以被用于创建证明,并且可以在链上或其他公开的地方查看。公钥用于生成证明 w。

  • vk(Verification Key):这是验证密钥,用于验证生成的证明的正确性。验证密钥通常是另一个公开的部分,但不同于公钥,它用于验证证明的有效性,而不是生成证明。验证密钥用于验证证明 w 和目标值的正确性。

这些术语与传统的加密中的公钥和私钥不同。在 zk-SNARKs 中,公钥和验证密钥是用于验证计算正确性的组成部分,而不是用于加密和解密消息的密钥对。验证密钥允许任何人验证生成的证明的正确性,而不需要知道私人输入数据的详细信息。

3.2、什么是目标值和输入值?

  • 目标值(Target Value):目标值通常是您希望证明的某个计算的输出结果。在 zk-SNARKs 中,证明的目标是证明您已经执行了某个计算并获得了正确的结果,而不需要揭示输入数据的细节。因此,目标值是您希望证明的计算结果的一部分,通常是公开的。

  • 输入值(Input Values):输入值是计算的输入数据,它们是用于执行计算的数据。输入值可以包括公开数据和私有数据。公开数据是在生成证明和验证时可见的,而私有数据在验证过程中不会被揭示。生成证明的目标是证明,基于给定的输入值,您已经正确执行了计算并得出了目标值。

因此,输入值是计算的输入数据,而目标值是计算的输出结果,证明的目标是证明您已经正确执行了计算并获得了目标值,同时保护了输入数据的隐私。这是 zk-SNARKs 的核心概念之一,允许验证计算的正确性而不需要公开私有数据。

4.编译文件

zokrates 编译命令如下:

# compile
zokrates compile -i deposit.zok
# perform the setup phase
zokrates setup
# execute the program
zokrates compute-witness -a 337 113569
# generate a proof of computation
zokrates generate-proof
# export a solidity verifier
zokrates export-verifier
# or verify natively
zokrates verify

运行完成后会生成一堆文件,我们需要的是 proof.json, proving.key, verification.key, verifier.sol, out。

大部分其实都是模版文件生成文件可能不一样的地方在于 Verifier 合约中 verifyingKey,当然,我们阅读这个文件其实意义也不大,因为这里面全是一大堆数字和运算。实际上我们需要看的内容就是这些:

function verifyTx(
        Proof memory proof, uint[8] memory input
    ) public view returns (bool r) {
    uint[] memory inputValues = new uint[](8);

    for(uint i = 0; i < input.length; i++){
        inputValues[i] = input[i];
    }
    if (verify(inputValues, proof) == 0) {
        return true;
    } else {
        return false;
    }
}

可以看到,我们需要两个参数,proof 和 input。至于这两个参数是干嘛的,我们暂时不过多深究。不过我们需要注意的是,在 inputs 中,所有的共有参数都会被加入到这个数组中,在数字最开头部分被推入。

比如,自动生成的 proof.json 文件就是一个有效的数据。

{
  "scheme": "gm17",
  "curve": "bn128",
  "proof": {
    "a": [
      "0x05a83e3c3b3ff9d59bdffdcf7aa655f42b941b0063f82cf26516846056d09aa6",
      "0x018039b7de92979ef6251c877971888ae049d09a6b48e5aa98c23ef91550ed36"
    ],
    "b": [
      [
        "0x1e88e783456a27e4f02dde8c742610339e395eb0bbf7f7efc1113815dcf0a16f",
        "0x1cc9de9e60c6519ea69c9b3a71c0809ac7ae3389a598d66fc27d378738d5de29"
      ],
      [
        "0x0715544abbc18e741620ff7c76cb2a7d3558ee157d23f275ab65c43c25357d07",
        "0x0344257236ba33a3ce7ce34b8d518f7572984036db6f77fc2fc13f51c548a837"
      ]
    ],
    "c": [
      "0x177113e528c76661a03a8f3f072f29e684244297a62926a0000d3a7135c1441f",
      "0x18cf275d0bc621473688848946584af771afca42e4f2bd0ef1e5d06e0adefd0f"
    ]
  },
  "inputs": [
    "0x0000000000000000000000000000000000000000000000000000000000000151",
    "0x00000000000000000000000000000000000000000000000000000000bb3eada7",
    "0x000000000000000000000000000000000000000000000000000000004b704815",
    "0x00000000000000000000000000000000000000000000000000000000cddda451",
    "0x00000000000000000000000000000000000000000000000000000000ca701d2a",
    "0x000000000000000000000000000000000000000000000000000000001f278e64",
    "0x00000000000000000000000000000000000000000000000000000000ef16f074",
    "0x0000000000000000000000000000000000000000000000000000000040e13298",
    "0x0000000000000000000000000000000000000000000000000000000026c5da72"
  ]
}

5.写一个简单合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Verifier} from "./verifier.sol";

contract Master {
    event Despoit(uint256 commitment, uint amount);

    mapping(uint => uint) public proofs;

    Verifier v;

    constructor() {
        v = new Verifier();
    }

    function deposit(uint commitment) public payable {
        proofs[commitment] = msg.value;
        emit Despoit(commitment, msg.value);
    }

    function withdraw(
        uint commitment,
        Verifier.Proof memory proof,
        uint[9] memory inputs
    ) public {
        uint amount = inputs[0];
        require(v.verifyTx(proof, inputs));
        require(proofs[commitment] == amount);

        payable(msg.sender).transfer(amount);
    }
}

要注意的是 Verifier 合约中会出现两个pragma solidity,记得删掉中间那一个,保留最上面的那个,否则编译无法通过。

四、测试

首先我们需要明白一下标准流程,我们需要先进行 compile,setup,然后再进行 compute-witness,然后再进行 generate-proof,最后再进行 export-verifier。

但是这套流程并不是每次都必须的,因为这个是一个完整流程。我们需要进行一下区分。

必要条件

compile - 编译 zk 电路 - 只需要执行一次 这个功能会生成 out 文件和 abi.json 文件,这两个是编译后的程序。
setup - 生成 zk 电路的 pk 和 vk - 只需要执行一次 这个功能会生成 proving.key 和 verification.key 文件,这两个文件是 zk 电路的公钥和私钥。实际上在进行 setup 的时候会产生 lambda,但是这些过程我们不需要太过于关心。

提交证明条件

compute-witness - 生成证明 - 这个功能会生成 witness 文件,这个文件是一个中间文件。
generate-proof - 生成证明的 Proof - 这个功能会生成 proof.json 文件,这个文件是证明需要提交的内容,一般来说里面的内容就是我们需要提交到链上的参数。

接受证明条件

export-verifier - 生成 verifier.sol - 这个功能会生成 verifier.sol 文件,这个文件是一个合约,我们需要将这个合约部署到链上,然后在我们的合约中调用这个合约来验证证明的正确性。
verify - 本地验证 - 这个功能会验证证明的正确性,但是这个功能并不会生成任何文件。

编写文件

根据上面内容,我们可以写出一些用于测试的单元测试逻辑。

import { expect } from "chai";
import { ethers } from "hardhat";
import { Verifier } from "../typechain-types/Verifier";
import { CompilationArtifacts, ZoKratesProvider } from "zokrates-js";
import { readFileSync } from "fs";
import { Master } from "../typechain-types";
import { resolve } from 'path'

describe("Verifier", function () {
  let master: Master;
  let zokratesProvider: ZoKratesProvider

  const zokArtifacts: CompilationArtifacts = {
    program: readFileSync(resolve(__dirname, '../zok/out')),
    abi: JSON.parse(readFileSync(resolve(__dirname, '../zok/abi.json'), 'utf-8'))
  }

  const provingKey = readFileSync(resolve(__dirname, '../zok/proving.key'))

  beforeEach(async () => {
    const { initialize } = await import("zokrates-js");
    zokratesProvider = (await initialize()).withOptions({
      backend: 'ark',
      curve: 'bn128',
      scheme: 'gm17'
    });

    const bn256 = await ethers.getContractFactory("BN256G2").then((f) => f.deploy());

    master = await ethers.getContractFactory("Master", {
      libraries: {
        "contracts/verifier.sol:BN256G2": bn256.address,
      }
    }).then((f) => f.deploy());
  })

  it("should verify a proof", async () => {
    const { witness, output } = zokratesProvider.computeWitness(
      zokArtifacts,
      [`${ethers.constants.WeiPerEther}`, '23'],
    )

    const commitment = hexListToUint256BigEndian(JSON.parse(output)).toString();

    await master.deposit(
      commitment,
      { value: ethers.constants.WeiPerEther }
    )

    const proof = zokratesProvider.generateProof(
      zokArtifacts.program,
      witness,
      provingKey);


    const sender = (await ethers.getSigners())[0];
    expect(() => master.connect(sender).
      withdraw(commitment, proof.proof as Verifier.ProofStruct, proof.inputs)
    ).to.changeEtherBalance(sender, ethers.constants.WeiPerEther);
  })
});


function hexListToUint256BigEndian(hexList: string[]) {
  let uint256Data = "0x";
  for (const hex of hexList) {
    const cleanedHex = hex.replace("0x", "");
    uint256Data += cleanedHex;
  }
  const uint256BigNumber = ethers.BigNumber.from(uint256Data);
  return uint256BigNumber;
}

测试结果:
在这里插入图片描述

项目的基础文件都放在: https://github.com/nishuzumi/blog/tree/main/sources/zk 中。

参考:zk-SNARKs 在 Solidity 中的使用 登链社区

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值