【译】用例子来介绍zk-SNARKs

原文链接

在本文中我们的目标是从一个实用的角度对 zk-SNARKs做一个总览。我们将把实际会用到的数学当作一个黑盒子,但是我们会围绕如何使用这些数学知识建立起一些直观的认识。最后我们给出一个简单的应用,就是最近集成进以太坊的zk-SNARKs。

零知识证明

零知识证明的目的是让验证者能够让自己相信证明者知道某个称作证词(witness)的秘密参数。这个秘密参数事先不暴露给验证者或任何其他人,而且这个参数满足某种关系。

你可以更具体地认为有一个用C表示的程序,它带2个参数:C(x, w)。输入x是公共输入,w是秘密证词输入。程序的输出是一个布尔值,为true或者false。我们的目标是给定一个具体的公共输入x,证明证明者知道一个秘密输入,使得C(x, w) == true。

我们会专门去讨论非交互零知识证明。这个证明本身是一块数据,这块数据可以在不跟证明者有任何交互的情况下被验证。

例子程序

假定Bob得到某个值的哈希H,他希望能证明 Alice知道这个哈希H的原值s。通常Alice可以这样来证明:把s给Bob,然后Bob计算s的哈希值,检查是否跟H相等。

然而,假设Alice不想把值s暴露给Bob,而只是想证明自己知道这个值。她可以用zk-SNARK来帮助达成这个目标。

我们可以用下面这段Javascript函数来描述Alice的场景:

function C(x, w) {
  return ( sha256(w) == x );
}

换句话说:这段程序取一个公共哈希x和一个秘密值w,如果w的SHA-256哈希值等于x,就返回true。

把Alice的问题用函数C(x, w)来表达,我们可以看到Alice需要创建一个证明,在不暴露s的情况下,证明她拥有s,使得C(H, s) == true。这就是 zk-SNARKs 要解决的一般问题。

zk-SNARK的定义

【译】用例子来介绍zk-SNARKs_第1张图片

一个zk-SNARK由如下定义的3个算法G,P,V组成:

密钥发生器G取一个秘密参数lambda和程序C,生成2个公共可获得的密钥,一个证明密钥pk,和一个验证密钥vk。这些密钥是公共参数,只需为程序C生成一次。

证明者P把证明密钥pk,一个公共输入x和一个私有证词作为输入。算法生成一个证明 prf = P(pk, x, w),其中证明者知道一个证词w而且证词满足这个程序C。

验证者V计算函数V(vk, x, prf)。如果证明prf是正确的,V返回true,否则返回false。于是,如果这个证明者知道一个证词w满足C(x, w)==true,这个函数V就返回true。

注意这里在发生器中用到的秘密参数lambda。这个参数有时候让在真实世界中使用zk-SNARKs有些不大容易。原因是任何知道这个参数的人都可以生成假的证明。具体来说,给定任何程序C和公共输入x,一个知道lambda的人可以生成一个证明fake_prf,使得在不知道秘密证词w的情况下,V(vk, x, fake_prf)的值为true。

实际上运行发生器需要一个非常安全的过程,以保证没有人知道或保存这个秘密参数。这就是为什么Zcash团队为了生成证明密钥和验证密钥而执行一个极其周密的仪式,来确保“有毒废物”参数lambda在过程中被消灭。

我们例子程序的一个zk-SNARK

在实践中,Alice为了证明她知道上面例子中的秘密证词,Alice和Bob要如何使用zk-SNARK呢?

首先,如上面讨论过的,我们将使用下面的函数定义的一个程序:

function C(x, w) {
  return ( sha256(w) == x );
}

第一步是让Bob运行发生器G来创建证明密钥pk和验证密钥vk。具体做法是,先随机生成lamdba,然后用它作为输入:

(pk, vk) = G(C, lambda)

如同上面讨论的,参数lambda必须要小心处理,因为如果 Alice知道了这个lambda值,她将能够创建假的证明。这里,Bob将同Alice分享pk和vk。

Alice现在要扮演证明者的角色。她需要去证明她知道已知哈希值H的原值s。她用输入pk,H,和s来运行证明算法P,得到证明prf:

prf = P(pk, H, s)

然后Alice把证明prf给到Bob。Bob运行验证函数V(vk, H, prf)。当Alice知道秘密s的时候,函数V返回true。Bob可以相信Alice知道这个秘密,同时Alice不需要把秘密暴露给Bob。

可重用的证明和验证密钥

在上面的例子中,如果Bob希望向Alice证明他知道一个秘密,就不能用zk-SNARK。这是因为Alice不能确定Bob没有保存这个lambda参数,而Bob因此可以令人信服地造一个假的证明。

如果一个程序对很多人是有用的(就像Zcash这个例子 ),一个被信任的独立的区别于Alice和Bob的一组人可以运行这个发生器并且创建证明密钥 pk和验证密钥vk,使得没有人知道lambda。

任何相信这个小组不会欺骗的人可以在将来的交互中使用这些密钥。

以太坊中的zk-SNARKs

开发者们已经开始把zk-SNARKs整合到以太坊中。这看起来会是什么样的呢?具体地,验证算法构件以预先编译好的合约的形式被添加进了以太坊。用法如下:发生器在链外运行产生证明密钥和验证密钥。任何证明者可以使用这个证明密钥来在链外创建一个证明。通用的验证算法可以在智能合约里面来运行,这个合约用这个证明、验证密钥、和公共输入来作为输入参数。验证算法的结果可以用来触发其他链上行为。

例子:秘密交易

【译】用例子来介绍zk-SNARKs_第2张图片

这里有一个简单例子,演示zk-SNARKs如何能帮助到以太坊的隐私性。假定我们有一个简单的代币合约。通常一个代币合约在其核心有一个地址到余额的映射:

mapping (address => uint256) balances;

我们将保留相同的基本核心,只是用余额的哈希来代替余额 :

mapping (address => bytes32) balanceHashes;

我们不去隐藏交易的发送者和接收者,但是我们能够隐藏余额和发送的交易额。这个特性有时被称为秘密交易。

两个zk-SNARKs会被用来从一个账户向另一账户发送代币,一个证明由发送者创建,另一个由接收者创建。

通常在一个代币合约中,一个大小为value的交易若为有效,我们需要做如下验证:

balances[fromAddress] >= value

于是我们的zk-SNARKs需要证明的是若更新后的哈希值匹配更新后的余额,上面的验证就成立。

这里主要的想法是,发送者将用他们的起始余额和交易额作为私有输入,把起始余额、结束余额、和交易额的哈希作为公共输入。类似地,接收者用起始余额和交易额作为秘密输入,起始余额、结束余额、和交易额的哈希作为公共输入。

下面是我们用作发送者zk-SNARK的程序,如同之前一样,这里x代表公共输入,w代表私有输入。

function senderFunction(x, w) {
  return (
    w.senderBalanceBefore > w.value &&
    sha256(w.value) == x.hashValue &&
    sha256(w.senderBalanceBefore) == x.hashSenderBalanceBefore &&
    sha256(w.senderBalanceBefore - w.value) == x.hashSenderBalanceAfter
  )
}

接收者用的程序如下:

function receiverFunction(x, w) {
  return (
    sha256(w.value) == x.hashValue &&
    sha256(w.receiverBalanceBefore) == x.hashReceiverBalanceBefore &&
    sha256(w.receiverBalanceBefore + w.value) == x.hashReceiverBalanceAfter
  )
}

这些程序检查发送方余额大于交易额 ,检查哈希值匹配。一组受到信任的人们会为我们的zk-SNARKs生成证明和验证密钥,让我们称这些密钥为confTxSenderPk,confTxSenderVk,confTxReceiverPk 和 confTxReceiverVk。

在一个代币合约中使用zk-SNARKs的情形,如下所示:

function transfer(address _to, bytes32 hashValue, bytes32 hashSenderBalanceAfter, bytes32 hashReceiverBalanceAfter, bytes zkProofSender, bytes zkProofReceiver) {
  bytes32 hashSenderBalanceBefore = balanceHashes[msg.sender];
  bytes32 hashReceiverBalanceBefore = balanceHashes[_to];
  
  bool senderProofIsCorrect = zksnarkverify(confTxSenderVk, [hashSenderBalanceBefore, hashSenderBalanceAfter, hashValue], zkProofSender);

  bool receiverProofIsCorrect = zksnarkverify(confTxReceiverVk, [hashReceiverBalanceBefore, hashReceiverBalanceAfter, hashValue], zkProofReceiver);

  if(senderProofIsCorrect && receiverProofIsCorrect) {
    balanceHashes[msg.sender] = hashSenderBalanceAfter;
    balanceHashes[_to] = hashReceiverBalanceAfter;
  }
}

于是在区块链上面唯一更新的是余额哈希值而不是余额本身。然后,我们可以知道所有的余额都被正确地更新,因为我们可以自己检查,这些证明都被验证过。

细节

上面的秘密交易方案主要是为了给一个实际的例子,演示人们可以如何在以太坊上使用 zk-SNARKs。为了创建一个基于zk-SNARKs的健壮的秘密交易方案,我们可能需要解决几个问题:

  • 用户们可能需要在客户端跟踪他们的余额。如果失去余额,那些代币将无法恢复。余额也许可以用一个从签名密钥派生出来的密钥加密然后存在链上。
  • 余额需要用到32字节数据,并把熵编码进部分余额,以防止从哈希中反向算出余额。
  • 需要处理像发送到没有用过的地址这样的边缘情况。
  • 因为发送的需要,发送者会跟接收者交互。我们将来可以有一个系统,其中发送者用他们的证明来初始化交易,然后接收者可以在区块链上看到他们有一个“等候中的入帐交易”,之后可以完成这个交易。

你可能感兴趣的:(零知识证明,区块链,zk-SNARKs)