ZKP与智能合约的开发入门

这篇文章将以程序代码范例,说明Zero Knowledge Proofs与智能合约的结合,能够为以太坊的生态系带来什么创新的应用。

近十年来最强大的密码学科技可能就是零知识证明,或称zk-SNARKs (zero knowledge succinct arguments of knowledge)。

zk-SNARKs 可以将某个能得出特定结果(output) 的计算过程(computation),产出一个证明,而尽管计算过程可能非常耗时,这个证明却可以快速的被验证。

此外,零知识证明的额外特色是:你可以在不告诉对方输入值(input) 的情况下,证明你确实经过了某个计算过程并得到了结果。

本文则是从零知识证明(ZKP) 应用开发的角度,结合电路(circuit) 与智能合约的程序代码来说明ZKP 可以为既有的以太坊智能合约带来什么创新的突破。

基本上可以谨记两点ZKP 带来的效果:

1.扩容:链下计算的功能。
2.隐私:隐藏秘密的功能。

WithoutZK.sol

首先,让我们先来看一段没有任何ZKP 的智能合约:

这份合约的主轴在process(),我们向它输入一个秘密值secret,经过一段计算过程后会与answer 比对,如果验证成功就会改写变数greeting 为“answer to the ultimate question of life, the universe , and everything”。

Computation

而计算过程是一个简单的函式:f(x) = x**2 + 6。

我们可以轻易推出秘密就是42。

这个计算过程有很多可能的输入值(input)与输出值(output):
f(2) = 10 
f(3) = 15 
f(4) = 22 

但是能通过验证的只有当输出值和我们存放在合约的资料answer 一样时,才会验证成功,并执行process 的动作。

可以看到有一个calculate 函式,说明这份合约在链上进行的计算,以及process 需要输入参数_secret,而我们知道合约上所有交易都是公开的,所以这个_secret 可以轻易在etherscan 上被看到。

从这个简单的合约中我们看到ZKP 可以解决的两个痛点:链下计算与隐藏秘密。

Circuits

接下来我们就改写这份合约,加入ZKP 的电路语言circom,使用者就能用他的secret 在链下进行计算后产生一个proof,这proof 就不会揭露有关secret 的资讯,同时证明了当secret丢入f(x) = x**2 + 6 的计算过程后会得出1770 的结果(output),把这个proof 丢入process 的参数中,经过Verifier 的验证即可执行process 的内容。

有关电路circuits的环境配置,这里我们就先跳过去,直接来看circom的程序代码:

template Square() { 
    signal input in; 
    signal output out; 

    out <== in * in; 
} 

template Add() { 
    signal input in; 
    signal output out; 

    out <== in + 6; 
} 

template Calculator() { 
    signal private input secret; 
    signal output out; 

    component square = Square(); 
    component add = Add(); 
  
    square.in <== secret; 
    add.in <== square.out; 

    out <== add.out; 
} 

component main = Calculator();

这段就是f(x) = x**2 + 6 在circom 上的写法,可能需要时间去感受一下。

ZK.sol

circom 写好后,可以产生一个Verifier.sol 的合约,这个合约会有一个函式verifyProof,于是我们把上方的合约改写成使用ZKP 的样子:

我们可以发现ZK 合约少了calculate 函式,显然f(x) = x**2 + 6 已经被我们写到电路上了。

snarkjs

产生证明的程序代码以javascript 写成如下:

let { proof, publicSignals } = await groth16.fullProve(input, wasmPath, zkeyPath);

于是提交proof 给合约,完成验证,达到所谓链下计算的功能。

最后让我们完整看一段javascript 的单元测试,使用snarkjs 来产生证明,对合约的process 进行测试:

对合约来说, secret = 42 是完全不知情的,因此隐藏了秘密。

publicSignals

之前不太清楚publicSignals 的用意,因此在这里特别说明一下。

基本上在产生证明的同时,也会随带产生这个circom 所有的public 值,也就是publicSignals,如下:

let { proof, publicSignals } = await groth16.fullProve(input, wasmPath, zkeyPath);

在我们的例子中publicSignals 只有一个,就是1770。

而verifyProof 要输入的参数除了proof 之外,也要填入public 值,简单来说会是:

const isValid = verifyProof(proof, publicSignals);

问题来了,我们在设计应用逻辑时,当使用者要提交参数进行验证的时候,publicSignals 会是由「使用者」填入吗?或者是说,尽管是使用者填入,那它需不需要先经过检查,才可以填入verifyProof?

关键在于我们的合约上存有一笔资料:answer = 1770

回头看合约上的process 在进行verifyProof 之前,必须要检查isAnswer(publicSignals[0]):

想想要是没有检查isAnswer,这份合约会发生什么事情?

我们的应用逻辑就会变得毫无意义,因为少了要验证的答案,就只是完成计算f(42) = 1770,那么不论是f(1) = 7 或f(2) = 10,使用者都可以自己产生证明与结果,自己把proof 和publicSignals 填入verifyProof 的参数中,都会通过验证。

至此可以看出,ZKP 只有把「计算过程」抽离到链下的电路,计算后的结果仍需要与链上既有的资料进行比对与确认后,才能算是有效的应用ZKP。

应用逻辑的开发

本文主要谈到的是zk-SNARKs 上层应用逻辑的开发,关于ZKP 的底层逻辑如上述使用的groth16 或其他如plonk 是本文打算忽略掉的部分。

从上述的例子可以看到,即使我们努力用circom 实作藏住secret,但由于计算过程太过简单,只有f(x) = x**2+6,轻易就能从answer 反推出我们的secret是42,因此在应用逻辑的开发上,也必须注意circom 的设计可能出了问题,导致私密讯息容易外泄,那尽管使用再强的ZKP 底层逻辑,在应用逻辑上有漏洞,也没办法达到隐藏秘密的效果。

此外,在看circom 的程序代码时,可以关注最后一个template 的private 与public 值分别是什么。以本文的Calculator 为例,private 值有secret,public 值有out。

另外补充:

  • 如果有个signal input 但它不是private input,就会被归类为public。
  • 一个circuit 至少会有一个public,因为计算过程一定会有一个结果。

最后,在开发的过程中我会用javascript 先实作计算过程,也可以顺便产出input.json,然后再用circom 语言把计算过程实现,产生proof 和public 后,再去对照所有public 值和private值,确认是不是符合电路计算后所要的结果,也就是比较javascript 算出来的和circom 算出来的一不一样,如果不一样就能确定程序代码是有bug 的。

参考范例:https://github.com/chnejohnson/circom-playground

总结

本文的程序代码展现ZKP 可以做到链下计算与隐藏秘密的功能,在真实专案中,可想而知电路的计算过程不会这么单纯。

会出现在真实专案中的计算像是hash function,复杂一点会加入Merkle Tree,或是电子签章EdDSA,于是就能产生更完整的应用如Layer 2 扩容方案之一的ZK Rollup,或是做到匿名交易的Tornado Cash。

郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。