Skip to content

零知识证明

最近学习了一下零知识证明 Zero-knowledge proof,看了好多文章,大部分都没有特别清晰的说明他的基本能力和是如何应用的,下面就以奶爸的视角做个简单的总结。

零知识证明最初是由 Shafi Goldwasser、Silvio Micali 和 Charles Rackoff 于 1985 年在他们的论文「交互式证明系统的知识复杂性」中提出的,已经存在了较长时间。区块链算是目前零知识证明的最广泛应用。

本文主要探讨非交互式零知识证明 Non-interactive zero-knowledge proof 中的一种:零知识简洁非交互式知识论证 Zero-Knowledge Succinct Non-Interactive Argument of Knowledge(zk-SNARK)。

zk-SNARK 具有以下特点:

  • 零知识性:证明者在证明过程中不会向验证者泄露任何秘密信息。
  • 简洁性:证明的长度很短,可以快速验证。
  • 非交互性:证明者和验证者之间只需要进行一次交互,即可完成证明过程。

zk-SNARK 主要有三个角色:

  1. 可信设置:首先,需要进行可信设置,生成一些用于证明和验证的参数。这个过程需要在安全的环境中进行,并且需要保证参数的安全性。
  2. 证明者:证明者使用秘密信息和可信设置生成证明。
  3. 验证者:验证者使用公开的信息和证明进行验证。

可信设置就是制定一个怎么校验秘密的、证明者和验证者同时接受的验证逻辑,比如需要验证用户的余额大于 100,可信设置就设置检查 balance > 100。下面拿 Consensys 公司的 gnark 作为实例对比说明一下 zk-SNARK 的应用。

gnark

gnark 是一个高性能的 zk-SNARK 库,提供高级 API 来设计电路。他的电路设计非常直观简洁。

下面我们来拿检验「某人资产大于 10000」这个例子来做解释。

这个例子的完整代码在 https://github.com/naiba-archived/use-gnark

电路约束 Circuit

要检验「某人资产大于 10000」,我们在电路中检查要求资产大于 10000。

package circuit

import "github.com/consensys/gnark/frontend"

type CheckBalanceCircuit struct {
	Balance frontend.Variable `gnark:"balance"`
}

func (circuit *CheckBalanceCircuit) Define(api frontend.API) error {
	ret := api.Cmp(10000, circuit.Balance)
	api.AssertIsEqual(ret, -1)
	return nil
}

这样一个简单的电路定义就完成了,Balance 字段是秘密字段,不会被验证者得知。

0x0 可信设置(编译电路)

package main

import (
	"fmt"
	"use-gnark/circuit"
	"use-gnark/utils"

	"github.com/consensys/gnark-crypto/ecc"
	"github.com/consensys/gnark/backend/groth16"
	"github.com/consensys/gnark/frontend"
	"github.com/consensys/gnark/frontend/cs/r1cs"
)

func main() {
	var circuit circuit.CheckBalanceCircuit
	cs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &circuit)
	if err != nil {
		panic(err)
	}

	pk, vk, err := groth16.Setup(cs)
	if err != nil {
		return
	}

	utils.WriteFile(cs, "01-cs.bin")
	utils.WriteFile(pk, "01-pk.bin")
	utils.WriteFile(vk, "02-vk.bin")

	fmt.Printf("cs sha256: %x\n", utils.FileSHA256("01-cs.bin"))
	fmt.Printf("pk sha256: %x\n", utils.FileSHA256("01-pk.bin"))
	fmt.Printf("vk sha256: %x\n", utils.FileSHA256("02-vk.bin"))
}

因为编译参数每次都随机生成,每次生成的 电路 cs、证明密钥 pk 和 验证密钥 vk 都是不同的。

我们需要将 02-vk 提供给验证者,01-cs 和 01-pk 提供给证明者。

01-cs 是经混淆后的电路约束定义二进制文件,几乎无法去混淆得到电路定义。

0x1 证明者 Prover

当可信设置完成后,拿到了电路定义和证明密钥,加上证明者手中持有的秘密,可以生成一个证明供验证者进行验证。

package main

import (
	"fmt"
	"use-gnark/circuit"
	"use-gnark/utils"

	"github.com/consensys/gnark-crypto/ecc"
	"github.com/consensys/gnark/backend/groth16"
	"github.com/consensys/gnark/frontend"
	_ "github.com/consensys/gnark/std/math/bits"
)

func main() {
	cs := groth16.NewCS(ecc.BN254)
	utils.ReadFile(cs, "01-cs.bin")

	pk := groth16.NewProvingKey(ecc.BN254)
	utils.ReadFile(pk, "01-pk.bin")

	assignment := &circuit.CheckBalanceCircuit{
		Balance: 10001,
	}
	witness, err := frontend.NewWitness(assignment, ecc.BN254.ScalarField())
	if err != nil {
		panic(err)
	}

	proof, err := groth16.Prove(cs, pk, witness)
	if err != nil {
		panic(err)
	}
	utils.WriteFile(proof, "02-proof.bin")

	publicWitness, err := witness.Public()
	if err != nil {
		panic(err)
	}
	utils.WriteFile(publicWitness, "02-public_witness.bin")

	fmt.Printf("proof sha256: %x\n", utils.FileSHA256("02-proof.bin"))
	fmt.Printf("public witness sha256: %x\n", utils.FileSHA256("02-public_witness.bin"))
}

证明者将生成的 Proof 加上公开的参数 public_witeness 提供给验证者进行验证。

0x2 验证者 Verifier

验证者通过可信设置提供的 vk 和证明者提供的 proof、public_witeness 三个要素来验证证明是否真实。

package main

import (
	"use-gnark/utils"

	"github.com/consensys/gnark-crypto/ecc"
	"github.com/consensys/gnark/backend/groth16"
	"github.com/consensys/gnark/backend/witness"
)

func main() {
	vk := groth16.NewVerifyingKey(ecc.BN254)
	utils.ReadFile(vk, "02-vk.bin")

	proof := groth16.NewProof(ecc.BN254)
	utils.ReadFile(proof, "02-proof.bin")

	publicWitness, err := witness.New(ecc.BN254.ScalarField())
	if err != nil {
		panic(err)
	}
	utils.ReadFile(publicWitness, "02-public_witness.bin")

	if err := groth16.Verify(proof, vk, publicWitness); err != nil {
		panic(err)
	}
}

在智能合约中使用

如果在 gnark 中使用的是 ecc.BN254 曲线加 groth16 算法的搭配,就可以将 vk 导出为一个 Solidity 合约将合约作为一个 verifier 进行验证。

err = vk.ExportSolidity(to_solidity_file_path)

案例:Binance 的 PoR 储备金证明

使用了 Poseidon Hash 来固定执行 CreateUserOperation 前后的 CexAssetInfo 列表,然后通过 Merkel Tree 固定每个 账户+资产 的信息,电路对每个阶段的状态进行了约束。

账户树的深度设置为了 28,大约能容纳 2 亿 6 千万条记录。

源代码:https://github.com/binance/zkmerkle-proof-of-solvency/blob/main/circuit/batch_create_user_circuit.go

扩展阅读

Comments

Commenting as a guest. A nickname is required; email is optional. Only new comments posted after verifying your email and signing in will appear on your profile. Existing guest comments will not be linked automatically.

Share article

零知识证明

Email X

View original ↗