25 / 11 / 06

零知识证明(ZK)入门教程:从魔法洞穴到区块链隐私

零知识证明 (Zero-Knowledge Proofs, ZK) 是近年来密码学领域最激动人心的突破之一,它正在从根本上改变我们对隐私和信任的看法。这份教程将为您揭开ZK的神秘面纱,并解释它如何在Privacy Cash这样的项目中发挥核心作用。

第一部分:什么是零知识证明?

1. 核心思想:只证明“是”,不解释“为什么”

想象一下,您想向您的朋友证明您知道某个保险箱的密码,但您绝对不想把密码告诉他。您会怎么做?

•传统方法:您把密码告诉朋友,他去打开保险箱,验证成功。但这样一来,您的秘密就泄露了。

•零知识方法:您当着朋友的面,用密码打开了保险箱。朋友亲眼看到保险箱被打开,从而100%相信您知道密码。在这个过程中,他没有看到密码的任何一位数字。您证明了您知道密码这一事实,但没有透露任何关于密码本身的信息。

这就是零知识证明的精髓:向验证者 (Verifier) 证明某个论断 (Statement) 是正确的,但除了“这个论断是正确的”这一结论外,不泄露任何额外信息。

2. 经典比喻:阿里巴巴的魔法洞穴

这是一个密码学界最著名的比喻,用于解释ZK的核心概念。

想象一个环形的洞穴,它有一个入口和两条通道A和B,但在深处有一扇魔法门,只有知道咒语才能打开,从而从一条通道进入,从另一条通道出来。

•证明者 (Prover):Peggy,她声称自己知道开门的咒语。

•验证者 (Verifier):Victor,他想验证Peggy的说法,但他不想让Peggy把咒语告诉他。

证明过程如下:

1.Victor站在洞穴外,看不到里面。Peggy独自进入洞穴,随机选择了通道A或B。

2.Peggy进入后,Victor走到洞口,随机喊出“从A通道出来!”或“从B通道出来!”。

3.情况一:如果Peggy恰好在Victor喊出的那条通道,她可以直接走出来。

4.情况二:如果Peggy在另一条通道,她就必须念出咒语,打开魔法门,穿过去,然后从Victor指定的通道走出来。

我们来分析一下:

•如果Peggy不知道咒语,她只有50%的概率能碰巧从正确的通道走出来。但只要有一次她走错了,她的谎言就会被揭穿。

•如果他们重复这个过程20次,Peggy每次都能从Victor指定的通道走出来,那么她碰巧猜对的概率是 (1/2)^20,这是一个可以忽略不计的极小概率。此时,Victor可以高度确信Peggy确实知道咒语。

在这个过程中,Victor得到了他想要的证明,但他一次也没有听到或看到咒语本身。这就是一个典型的零知识证明系统。

3. ZK证明的三大特性

一个严谨的零知识证明系统必须满足以下三个特性:

特性

解释

在魔法洞穴比喻中

完整性 (Completeness)

如果证明者的论断是真的,那么一个诚实的证明者总能成功说服一个诚实的验证者。

如果Peggy真的知道咒语,她总能从Victor指定的任意通道出来。

可靠性 (Soundness)

如果证明者的论断是假的,那么一个欺骗的证明者几乎不可能成功说服一个诚实的验证者。

如果Peggy不知道咒语,她猜对的概率会随着重复次数的增加而指数级下降。

零知识性 (Zero-Knowledge)

验证者在验证过程中,除了“论断为真”这一结论外,学不到任何其他信息。

Victor只知道Peggy能打开门,但他不知道咒语是什么,甚至不知道门在洞穴的哪个位置。

第二部分:ZK证明是如何“炼成”的?(给开发者的视角)

好了,告别魔法洞穴,我们来看看在计算机世界里,一个ZK证明是如何从代码变成一个可验证的“证明”的。这个过程就像一个编译流程,将我们熟悉的程序逻辑转化为复杂的数学问题。

我们将以一个简单的例子贯穿整个流程:“我有一个秘密数字x,它的平方是25”。我们想向世界证明我们知道x,但不想透露x是5还是-5。

第1步:将程序逻辑转化为“算术电路”

计算机程序可以被分解为最基本的操作。对于ZK证明,我们需要将程序逻辑转化为一系列的数学方程式。这个过程称为算术化 (Arithmetization)。

我们的程序非常简单:x * x == 25

这本身就是一个数学方程式。对于更复杂的程序,比如计算一个哈希值,它会被分解成数千个类似的简单方程式。这个由方程式组成的网络,我们称之为算术电路 (Arithmetic Circuit)。

类比:这就像我们将高级语言(如Python)编译成汇编语言。算术电路就是ZK证明系统的“汇编语言”。 工具:Circom就是一种专门用于编写算术电路的语言。

第2步:约束系统 (R1CS)

为了让证明系统能够高效处理,这些方程式会被转换成一种标准格式,最常见的就是R1CS (Rank-1 Constraint System)。简单来说,它要求每个方程式都必须是 A * B - C = 0 的形式,其中A, B, C是所有变量(包括秘密变量和公开变量)的线性组合。

我们的例子 x x = 25 可以写成 x x - 25 = 0。这就完全符合R1CS的格式。

•秘密变量 (Witness):x,这是我们不想泄露的秘密输入。

•公开变量 (Public Input):25,这是大家都知道的输出。

第3步:证明系统登场 (zk-SNARK)

现在,我们有了标准格式的数学问题。接下来就需要一个“引擎”来为它生成和验证证明。Privacy Cash使用的是zk-SNARK,这是目前最流行的一种ZK证明技术。

zk-SNARK代表:

•Zero-Knowledge: 零知识,我们已经了解了。

•Succinct: 简洁。证明本身非常小(通常只有几百字节),无论原始计算有多复杂。

•Non-interactive: 非交互式。证明者生成一个证明后,可以发布出去,任何人随时都可以独立验证,无需与证明者再进行任何通信。这对于区块链这种异步环境至关重要。

•Argument of Knowledge: 知识论证。它不仅证明了论断为真,还证明了证明者确实“知道”能使论断为真的秘密输入(Witness)。

第4步:可信设置 (The Ceremony)

这是zk-SNARK(特别是Groth16算法)中最受争议也最容易被误解的一步。在生成证明之前,我们需要创建两个关键的公共参数:

•证明密钥 (Proving Key, proving_key.zkey): 用于生成证明。

•验证密钥 (Verification Key, verification_key.json): 用于验证证明。

这两个密钥的生成过程被称为可信设置仪式。在这个仪式中,系统会生成一些随机数,我们称之为“有毒废料 (Toxic Waste)”。仪式结束后,必须销毁这些随机数。如果任何人保留了这份“有毒废料”,他就能伪造出任何无效论断的有效证明,从而摧毁整个系统的安全性。

类比:想象一下,一个锁匠制造了一把独一无二的锁和对应的钥匙。在制造过程中,他使用了一个模具。如果这个模具被销毁了,那么这把锁就是安全的。如果有人偷走了模具,他就能复制出万能钥匙。

现代的可信设置仪式通常采用多方计算 (MPC) 的方式进行,只要参与者中至少有一个人是诚实的并销毁了自己的那部分随机数,整个系统就是安全的。

第5步:生成证明 (Proving)

现在,证明者 (Prover) 拥有:

1.证明密钥 (proving_key.zkey)

2.秘密输入 (Witness):在他的例子中是 x = 5

3.公开输入 (Public Input):25

他将这三者输入到snarkjs这样的库中,经过大量的密码学计算(主要是椭圆曲线上的多标量乘法和快速傅里叶变换),最终生成一个非常小的证明文件 proof.json。

这个过程非常消耗计算资源,可能需要几秒到几十秒。这就是为什么在Privacy Cash SDK中,这个步骤需要通过WASM在客户端高性能地执行。

第6步:验证证明 (Verifying)

验证者 (Verifier)——在我们的场景中是链上的智能合约——拥有:

1.验证密钥 (verification_key.json)

2.证明文件 (proof.json)

3.公开输入 (Public Input):25

验证者将这三者输入到验证算法中。这个过程非常快速,通常只需要几毫秒。算法会输出true或false。

如果结果为true,智能合约就确信:证明者确实知道一个x,它的平方等于25,但他不知道x具体是什么。合约的逻辑得以继续执行。

第三部分:ZK如何驱动Privacy Cash?

现在,我们已经掌握了ZK证明的基本原理和制作流程。让我们将这些知识应用到Privacy Cash项目中,看看它是如何利用ZK技术实现隐私交易的。

1. 目标:打破交易链接

在像比特币或以太坊这样的公共区块链上,所有的交易都是透明的。如果你知道某个人的钱包地址,你就可以在区块链浏览器上看到他所有的交易历史:他收到了多少钱,发送了多少钱,钱包里还剩多少钱。这被称为可追溯性 (Traceability)。

Privacy Cash的核心目标就是打破这种可追溯性。当Alice向Bob转账时,系统需要保证外部观察者无法将Alice的存款操作和Bob的取款操作关联起来。它们看起来就像两个完全独立的事件。

2. 核心组件:构建隐私的基础

为了实现这一目标,Privacy Cash引入了几个核心的密码学概念,这些概念将共同构成我们ZK证明的内容。

a) UTXO:匿名的数字票据

UTXO代表“未花费的交易输出 (Unspent Transaction Output)”。您可以把它想象成一张匿名的、不记名的数字现金或票据。每一张票据(UTXO)都包含:

•金额 (Amount):这张票据值多少钱。

•所有者 (Owner):一个专属于这张票据的、一次性的公钥。注意:这不是用户的钱包地址,而是一个为该UTXO新生成的密钥对的公钥,从而隐藏了用户的真实身份。

•盲化因子 (Blinding):一个随机数,确保即使两张票据的金额和所有者都相同,它们的公开形式也完全不同。

b) 承诺 (Commitment):UTXO的公开“化身”

我们不能直接把UTXO的明文信息放到链上,否则隐私就无从谈起。相反,我们只在链上发布一个UTXO的“承诺”。

承诺 = PoseidonHash(金额, 所有者公钥, 盲化因子)

•PoseidonHash:这是一个ZK友好的哈希函数。我们之前提到过,它在电路中计算效率很高。

•单向性:从UTXO的明文信息计算出承诺非常容易,但从承诺反推出金额、所有者或盲化因子在计算上是不可能的。

当用户存款时,他实际上是在链上发布了一个新的承诺。这就好比将一张票据锁进了一个透明的保险箱里,外面的人只能看到保险箱的编号(承诺),但看不到里面的票据信息。

c) 默克尔树 (Merkle Tree):高效的承诺“数据库”

随着存款越来越多,链上的承诺数量会变得非常庞大。智能合约需要一种高效的方式来证明某个承诺确实存在于这个集合中。

这就是默克尔树的作用。您可以把它想象成一个巨大的、只进不出的承诺数据库。所有的承诺都被作为“叶子节点”插入到这棵树中。智能合约只需要存储一个值:默克尔树的根 (Merkle Root),这是一个32字节的哈希值。

•当一个新的承诺被添加时,树会更新,智能合约也只更新这个根哈希。

•如果你想证明你的UTXO(即你的承诺)确实在这棵树里,你只需要提供一条从你的叶子节点到根节点的“路径”(称为默克尔证明),而无需暴露整棵树的内容。

d) Nullifier:防止双花的“作废标记”

当你想要花费一张UTXO时,你如何向系统证明你没有重复花费它?这就是Nullifier (作废标记) 的作用。

Nullifier = PoseidonHash(承诺, 默克尔树索引, 签名)

•每个UTXO都有一个唯一的、与之对应的Nullifier。

•它由UTXO的秘密信息(通过签名)和公开信息(索引)共同生成。这意味着只有UTXO的真正所有者才能计算出正确的Nullifier。

•当你花费一个UTXO时,它的Nullifier会被发布到链上并记录下来。如果有人想再次花费同一个UTXO,他会计算出相同的Nullifier,但智能合约会发现这个Nullifier已经被记录过了,从而拒绝这笔交易。

3. ZK证明在取款中的应用:串联一切

现在,我们拥有了所有的拼图。让我们看看当一个用户(比如Alice)想要从Privacy Cash协议中取款1 ETH时,ZK证明是如何将这一切串联起来的。

Alice需要向智能合约提交一个ZK证明,这个证明需要证实以下所有论断:

“我,Alice,在不透露任何具体细节的情况下,郑重声明:”

1.我知道秘密:我知道一个或多个我自己的、未花费的UTXO的秘密信息(金额、盲化因子、私钥)。

2.它们在树上:这些UTXO对应的承诺确实存在于当前链上记录的默克尔树中(我拥有有效的默克尔路径)。

3.金额正确:这些UTXO的总金额,正好等于我想要取出的1 ETH,加上我给自己找零的一个新UTXO的金额。(输入总金额 = 1 ETH + 找零金额)

4.防止双花:我已经根据这些UTXO的秘密计算出了它们唯一的Nullifier,并且这些Nullifier从未在链上出现过。

5.找零正确:我已经正确地为我的找零UTXO生成了一个新的承诺。

这就是我们的“算术电路”需要验证的核心逻辑!

开发者视角下的证明流程

1.秘密输入 (Witness) - Alice的客户端SDK准备:

•她要花费的UTXO的完整信息(金额、盲化因子、私钥)。

•这些UTXO在默克尔树中的索引和对应的默克尔路径。

•她要创建的找零UTXO的秘密信息。

2.公开输入 (Public Inputs) - 所有人都能看到的信息:

•当前链上的默克尔树根。

•Alice要花费的UTXO的Nullifier。

•Alice新生成的找零UTXO的承诺。

•取款金额(1 ETH)和接收地址。

3.生成证明 (Proving):

•Alice的客户端(在浏览器中使用WASM)调用snarkjs.groth16.fullProve()函数。

•输入:上述所有秘密输入和公开输入,以及proving_key.zkey。

•输出:一个proof.json文件。

4.验证交易 (Verifying):

•Alice将公开输入和证明文件提交给链上的智能合约。

•智能合约调用verifier.verifyProof()函数。

•输入:verification_key.json(已硬编码在合约中)、公开输入、证明文件。

•如果函数返回true,则证明有效。

5.状态更新:

•智能合约将Alice提交的Nullifier记录下来,防止她再次使用。

•将Alice的找零承诺插入默克尔树,并更新树根。

•将1 ETH发送到Alice指定的接收地址。

交易完成!在这个过程中,区块链上只记录了一些看似毫无关联的数据:一个有效的证明、几个Nullifier哈希值、几个新的承诺哈希值。没有人能将这次取款与之前的任何一次存款联系起来,Alice的隐私得到了完美的保护。

总结

希望这份教程能帮助您理解零知识证明的魅力以及它在实际项目中的强大应用。从本质上讲,ZK技术允许我们在一个不信任的环境(如公共区块链)中,有选择性地、可编程地验证信息的正确性,而无需暴露信息本身。这为构建下一代注重隐私的应用程序打开了无限可能。