淺談零知識證明之二:簡短無交互證明(SNARK)

本文作者東澤,來自安比技術社區的小夥伴,目前就讀於斯坦福大學,研究方向密碼學,本系列文章來源於作者在斯坦福著名的課程《CS 251: Cryptocurrencies and blockchain technologies》上的學習筆記,該課程授課老師是密碼學大拿 Dan Boneh。

上一期文章發表之後,非常驚訝有那麼多小夥伴讀完了表示喜歡。那麼我們接着這期繼續吧!這次,我們專注的聊一聊SNARK。
相信看完前一篇文章的朋友們會有一點很不解的地方:爲什麼我們可以如此簡短的創建一個證明,並且證明很長的信息呢?在上課前我也有這同樣的疑惑,甚至覺得這個是一個“黑科技”,不過相信大家看完這篇文章,就會知道如何去駕馭這個“黑科技”了。

在詳細討論之前,我們得稍微嚴肅一點,系統性的學習一下SNARK的基本構造。

SNARK的四步構造

因爲SNARK的“黑科技”特性,想要構建一套簡短證明系統,其實流程是略微有些複雜的。總結一下一共可以分爲四步,我們來一步一步詳細描述。

第一步:確定有限域

在構造之前,我們先需要確定一個可以包含所有我們想要計算的數字的大小的一個有限域(Finite Field)。用通俗易懂的話來說,我們需要選一個很大很大的數字p,確保這個數字比我們要解決的問題裏的所有數字都大。

如果以前瞭解過類似於RSA密碼學的朋友們可能對這個概念不陌生。有限域就是給我們所有數字加了一個天花板,確定了整個數學系統的計算空間,類似於在電腦裏如果我們想創建一個存數字的變量,我們需要思考到底是需要一個uint32_t(32位),還是一個uint64_t(64位)一樣。所有超過有限域的值,都會直接溢出之後再倒回去從0開始。

在數學界,符合這個特性的計算空間其實有很多種,最常見的一種就是RSA算法裏面用到的正整數求模一個大質數(integer mod p)。上文說到的找到一個數字p,其實就是一個很大的質數,然後我們可以用它來生成一個計算空間。

在確定計算空間之後,我們就可以真正開始SNARK的搭建了。

第二步:構建數學運算電路(Arithmetic Circuit)

當我們找到一個數字空間之後,我們下一步要做的事情,就是要把我們想要證明解決的問題用數學運算電路表示出來。

什麼是數學運算電路呢?我們先來看一看傳統的邏輯電路。

在這裏插入圖片描述
上圖表述的是一個與非門(NAND)的電路。先不用過多瞭解電路的用處,我們可以發現的是,兩組輸入信號分別通過了AND和OR這兩個基礎邏輯門**。像AND和OR這樣的基礎邏輯門是邏輯電路的基礎模塊,通過拼加和堆疊我們可以實現任何複雜邏輯。**

數學運算電路也是同理,只不過基礎模塊從邏輯門變成了數學運算。因爲加法和乘法是最基礎的數學運算,通過加法和乘法我們也可以近乎實現所有其他的運算方法,所以我們可以**選用“加法門”和“乘法門”作爲我們數學運算電路的基礎模塊。**通過疊加運算門,我們可以搭建一個複雜多項式的“電路”。

爲了能更好的理解運算門,我們來試試看把上面的NAND門從邏輯電路轉換成數學運算電路。(假設Inp0和Inp1和原來邏輯電路一樣,還是輸入1/0的邏輯信號。 )

我們先來看AND門:AND其實很簡單,因爲只有當Inp0和Inp1都是1的時候,AND的結果纔會是1。我們很快發現,一個乘法門可以完美的代替AND門:只有當兩個輸入是1的時候,相乘得到的結果纔會是1。

解決了AND之後,我們來轉換NOT:NOT其實也非常簡單,因爲輸入信號只會是0或者1,只要用1減去輸入的信號,得到的結果就是相反的了。注意有一個問題是,因爲我們只有加法和乘法,所以如果需要實現減法的話,我們需要先把輸入信號乘上一個常數-1。
在這裏插入圖片描述
經過如此轉換,我們就成功的把一個邏輯電路轉換爲數字邏輯電路了。同時因爲我們已經知道如何組建AND和NOT了,理論上我們就可以把這兩個部分模塊化,拼接任意的複雜邏輯出來。

看到這裏,你可能會覺得運算電路非常簡單,和邏輯電路轉化也非常直白。但是其實這是因爲我們一直在假設運算電路的輸入和邏輯電路一樣,都是0或者1。在真實場景下,一個運算門的輸入值可能上限非常大(取決於我們有限域選擇的大小)。所以我們必須要想辦法約束我們運算電路的輸入,使其不僅能夠在功能性上和邏輯電路相同,並且在輸入取值上只可以取[0,1]這兩個數字。

但是怎麼用運算門來表示這麼一個關係呢?因爲數學運算電路其實就是一個複雜的多項式(比如上圖的NAND電路就變成了Out = 1 - Inp0 * Inp1),我們可以把這個問題轉化一下:**能否創造出一個多項式,只有當一個輸入滿足[0,1]的取值範圍的時候,纔會輸出0?**最後我們發現,有這麼一個多項式可以滿足這個要求:
在這裏插入圖片描述
上面這串表達式的意思是,只有當數字n取值於[0,1]這個範圍的時候,後面的多項式n * (n-1)纔會輸出0。也就是說,我們就可以把上文提到的Inp0和Inp1接到這個多項式轉換成的運算電路內,只要這個運算電路的輸出結果是0,那麼我們就可以確信上文的NAND運算電路的輸出也是對的。

有人可能會問,上文限制取值的多項式只能限制一個輸入,但是我們有兩個輸入,如何同時限制他們的取值範圍呢?其實答案很簡單, 只需要把同樣的多項式複製一份,兩者加起來就好了。
在這裏插入圖片描述
有了這兩個電路之後,我們只要確定約束電路的輸出是0,那麼NAND門電路就會正常運轉了。

有一點需要注意的是,因爲我們的邏輯門是從運算門搭建起來的,我們會發現其實邏輯運算非常的慢:任何邏輯運算至少得做一次乘法,然而熟悉計算機硬件的朋友們會知道,乘法其實是相對來說比較昂貴的運算。這樣一來有一點顛倒黑白的感覺,在計算機裏邏輯運算是最便宜也是最簡單的計算,然而在數學運算電路里,邏輯運算反而是一個比較繁瑣的過程了。

第三步:轉換爲可證明數學運算電路

當我們有了數字運算電路這個概念之後,我們就可以把不同的電路模塊拼接起來,生成一個可以用作證明的運算電路出來。

首先,我們先定義一下可以用作證明的數字運算電路C(x, w),具體構造如下:

在這裏插入圖片描述
簡單的來說,這個電路C有兩組輸入。

第一組輸入標記爲x,我們稱之爲公有輸入(public input),也就是說所有人都會知道x的值。這個x一般來說表達了想要證明的問題的特性和一些固定的環境變量。

第二組輸入標記爲w,我們稱之爲私密輸入(secret input),或者又稱之爲witness。這一組數據就是真正提交證明的一方的謎底,只有證明的一方纔會知道,其他人是不可以得之的。

當我們有C這一個電路之後,我們的目標就是證明C(x, w) = 0。換句話來說,在A和B已知數學運算電路C輸出爲0,並且公有輸入爲x的情況下,A需要證明她知道能夠構成這個輸出的私密輸入值w。

如果我們把這個C(x, w)的概念代入上文提到的NAND門的例子裏,我們會發現,光是NAND門不足以成爲一個用做證明的電路。我們可以重新定義一下我們想證明的問題:**如果我們已知一個NAND門的輸出爲0,並且其中的一個輸入Inp0爲1,A想證明她知道另一個輸入Inp1的值。**這個證明的過程中,不僅要保證NAND門的輸出是對的,而且要保證所有的輸入值都取值在我們事先約定好的區間內。

在這裏插入圖片描述
我們結合上面討論的方案,把NAND的電路輸出和我們的取值約束電路接在一起拼接成運算電路C,x爲Inp0,w爲Inp1,輸出我們約束爲0,從而構成一個完整的NAND門私密輸入證明體系。

當我們得到最後的證明電路C之後,我們可以計算出這個運算電路的複雜度。

如果我們想知道一個數字運算電路C有多難算的時候,我們可以用裏面有多少個運算門來描述它的複雜度。一般來說我們會用 |C| 來表示。像是上面提到的NAND證明電路,|C|大概是在10左右。

在現實使用場景中,如果我們要把像SHA256這樣的複雜函數用數字運算電路來表示,|C| 可能會達到25000這麼大。這也就是爲什麼真正落地的證明還是非常慢的原因。

第四步:非交互簡短證明體系(SNARK)

當我們通過第三步得到了最終的證明電路C,還有對應的x和w之後,我們的準備工作已經做的差不多了。剩下來的事情,我們就可以交給SNARK算法來生成和驗證我們的證明了。

首先,我們看看整個非交互式證明體系的官方定義

一個SNARK系統,往往由三個核心算法組成:Setup,Prove和Verify。

  1. 生成算法Setup在這裏插入圖片描述
    Setup算法會把我們實現確定好的電路C拿進來進行一系列的預處理(preprocessing),然後生成兩組參數。Sp是給證明方的參數,Sv是給驗證方的。這些參數的用處就是方便雙方來生成和驗證簡短證明。一般來說,生成算法的複雜度和電路C的複雜度是等比例的,O(|C|)。

  2. 證明算法Prove:在這裏插入圖片描述
    證明算法相比不用多講了,證明方會用Prove這個算法來生成一個證明Π,然後把這個證明發送給驗證方。Prove算法再生成證明的時候會用到幾乎所有的數據:預處理數據Sp,公有輸入x,還有私密輸入w。最後生成的證明Π的空間複雜度非常簡短:|Π| = O(log|C|)。

  3. 驗證算法Verify在這裏插入圖片描述
    驗證算法也非常的直白,驗證方會用Verify這個算法驗證我們收到的證明Π。這個算法會返回一個1/0的數值,代表驗證是否通過。驗證的過程中除了需要對方提供的證明Π,我們還需要預處理數據Sv,還有公有輸入x。驗證的複雜度也非常小,一般來說是 O(|x| + log|C|)。

有了這三個算法之後,我們可以用很簡單的圖來描述一下整個證明體系。
在這裏插入圖片描述
證明方提交的這個證明Π可以充分說服驗證方,讓其相信證明方真的有這麼一個祕密的w可以滿足C(x, w) = 0。

實例:私密交易的輸入輸出取值區間(Range Proof)

讀過上一篇文章的朋友們應該還會記得,如果我們要進行私密交易(Confidential Transaction)的話,我們需要在交易最後附帶三組證明:

  1. 第一組是Pederson承諾,證明了輸入和輸出之間的數學關係。
  2. 第二組是區間證明,需要證明輸入和輸出的值全部取值於正整數的範圍。
  3. 第三組是所有權證明,證明交易的發起人真的有這麼多token作爲輸入。

Pederson承諾的實現我們已經在上一篇文章中討論過了。現在瞭解完簡短證明的四步構造,我們可以來看看如何具體實現區間證明

首先,我們需要找到一個合適的數學運算電路來描述我們想要證明的內容。(我們默認使用正整數的有限域,選取一個非常大的質數p進行求模。)

假如我們有一個數字w,我們想要證明這個數字w不是一個負數,那麼我們可以先辦法證明它一定會取值於正整數區間。因爲考慮到計算機系統裏的正整數一般不會超過256位,所以我們可以把問題弱化一下:證明一個數字w取值於0-2256之間。**(根據此條件,有限域選擇的質數p需要大於2256。)
在這裏插入圖片描述
現在確定了要解決的問題之後,我們可以開始思考如何用加法和乘法來表達這個取值關係。還記得在前面的章節,當我們在講一個值n會取值在0和1之間的時候,我們用n
(n - 1) = 0來約束這個取值範圍。同理可得,如果我們想約束n必須要取值於0和5之間的話,我們就可以用更長的一串乘法來約束它:
在這裏插入圖片描述
看到這裏,大家可能心裏已經知道如何約束這個w的值了:我們只要
用同樣的辦法擴大這個等式*,從(w - 1)一直連續乘到(w - 2^{256}),不就可以了?

聽起來很簡單,但是細心的朋友會發現,這樣最後的電路里面將會有2^256個乘法門。光是這麼多乘法,還沒有算加法,這個電路的複雜度已經是天文數字了。就算是跑Setup可能就不知道跑到猴年馬月,所以我們說這種約束的方法是不實際的

那還有什麼方法來約束這個數字w的空間呢?我們可以巧妙藉助二進制數的結構。既然我們想要規定w是個整數,並且大於0但小於2^256,那麼我們就可以在二進制裏,把w拆分成256位,然後分別約束每一位。這樣的話,我們最後得到的電路大小隻會和這個數字有多少位成正比,而不會和這個數字的最大上限有關係。複雜度一下子就下來了一大個等級。

具體是怎麼實現的呢?我們首先把w拆分成256位:
在這裏插入圖片描述
因爲每一位都是二進制的,所以我們需要約束每一位的取值空間爲0和1:
在這裏插入圖片描述
在這裏插入圖片描述
這個約束需要256份,每一份對應每一位。當我們把這些約束準備好之後,我們最後確定所有的位組在一起可以還原成原來的w:
在這裏插入圖片描述
在這裏插入圖片描述
我們把上文提及的256+1=257組約束電路全部拼接在一起,合併成一個輸出。最後生成的電路就是我們可以用來構建證明系統的電路C了。如果C的輸出爲0,那麼代表了輸入的數字一定要滿足:

  1. 這個數字是一個正整數,可以被256位二進制數表達。
  2. 這256位二進制數的確是二進制數。(只能取值0或者1)
  3. 這256位二進制數全部拼在一起可以重新還原輸入進來的數字。

在這裏插入圖片描述
通過巧妙藉助二進制的特性,我們就有一組大小適中的數學運算電路,可以調用Setup來準備生成後續的SNARK了。

實例:私密交易輸入的所有權

解決了區間證明,我們來完成私密交易的最後一組證明:所有權證明,證明私密交易的輸入值合理。

看過前一篇文章的朋友應該會知道,我們講了兩種私密交易所有權證明的體系:

第一種方案是一筆交易可以直接引用上一筆交易的輸出,但是這會帶來雞和蛋的問題:難度在如何創造出第一筆私密交易來。第二種方案是在每一筆交易的下面附加另一個新的證明,證明交易發起的用戶的賬戶餘額真的有那麼多錢。

我想着重講一下第二種證明方案,也就是證明交易發起者在世界狀態中的賬戶餘額。

在很多區塊鏈的場景中,整個世界的狀態就是一個巨大的賬本。賬本的每一行都記錄了某一個用戶的賬戶餘額。

爲了更方便的表示整個世界狀態,我們往往會使用Merkle樹把巨大的世界賬本變成一串短小精悍的Merkle哈希值。只要任何一個賬戶的任何餘額發生改動,這個Merkle哈希值就會發生改動。

Merkle樹其實就是一個二叉樹,每一個節點都會把下面的兩個子節點的值拼接在一起,並且算出對應的哈希值作爲自己的值。爲了方便構建Merkle樹,我們會把最原始的餘額數值先做一次哈希計算,然後把它們的哈希值存到Merkle樹的最底層來。
在這裏插入圖片描述
當我們如此構建一個Merkle樹之後,我們就可以把輸出的Merkle哈希值當作一個**承諾:只要承諾不發生改變,那麼當前的世界狀態就肯定不會發生改變。**在區塊鏈中,如果我們想要記錄一長串數據的狀態,一般爲了節省空間,會在鏈上記錄所有數據的Merkle承諾,而不是把數據本身存在鏈上。

當我們有了一個世界狀態的承諾之後,後續的問題就是:假如A就是上圖中的賬戶1,現在有5塊錢。A如何向B證明,在整個世界狀態中,她的餘額真的爲5塊呢?

一個很簡單的方法就是:A可以向B提交所有賬戶的餘額,然後B自己再進行一次Merkle承諾的運算。只要B算出來的承諾和當前的世界狀態承諾相等,並且A提交的她自己的賬戶餘額的確是5塊錢的話,B就可以相信A真的有5塊錢。

聽起來好像是很不錯的方法,但是這個方法存在的問題一目瞭然。加入這個世界有幾億的用戶,如果A需要向B提交幾億行存款餘額,先別說B怎麼去有效的計算這個承諾,光是網絡流量就要用爆了。並且如此證明方法將會暴露所有用戶的餘額信息,大家肯定也是不太願意的。

那怎麼有效的證明賬戶1的餘額屬於當前世界狀態呢?這個時候我們就可以利用Merkle樹的特點來構造Merkle證明
在這裏插入圖片描述

如果仔細觀察上圖經過一些修剪的Merkle樹的話,我們會發現,只要證明賬戶1的餘額可以在Merkle樹中和相鄰的節點一路組成最後的世界狀態承諾,我們也就能證明賬戶1的餘額屬於當前世界狀態了。

那具體怎麼做呢?我們先從賬戶1的餘額出發,一路沿着Merkle樹往上走。

有了賬戶1的餘額,那麼我們就可以計算出H1。當有了H1之後,我們發現,我們並不需要知道賬戶0的餘額,只要一個H0的值,就可以組合生成H4了。同理,我們可以通過和H5的值結合,最終生成頂點的Merkle承諾——H6。到頭來我們只需要提交三樣東西就可以完成這個Merkle證明了:賬戶1的餘額、H0、H5。剩下哈希值全部可以在驗證的過程中被計算出來。這就是Merkle證明

我們通過Merkle證明可以大大的壓縮證明的體積,並且也可以儘可能的保證世界狀態中的其他用戶的餘額不在證明的過程中被暴露出來。Merkle證明在密碼學上非常有用,只需要O(logN)的大小就可以證明某個東西在長度爲N的列表之內。往往我們都會用Merkle證明來證明一組數據屬於一個很大文件,一個用戶屬於一個很大的組織,等等。

瞭解完Merkle證明的原理之後,我們來看看如何證明在私密交易中,A可以證明她的餘額。

Merkle證明的精髓就是我們可以從要證明的值開始,一路往上算哈希值,並且和相鄰的節點的哈希值不斷的合併。但是我們發現一個非常致命的問題:雖然我們可以隱去世界狀態裏別人的餘額,但是我們還是必須要暴露自己的餘額(不然沒有辦法算出第一個哈希值H1)。這一點和我們私密交易的本質是違背的!

這個時候,就需要藉助SNARK的力量了。我們可以把這個問題拆分成兩步。

第一步:證明餘額哈希值

第一步,我們通過SNARK來證明,賬戶1的餘額,通過哈希函數之後,可以得到哈希值H1。

因爲我們想隱藏賬戶1的餘額,我們用w來表示這個數字。在套用SNARK之前,我們還需要變換一下問題的描述方法,使其更方便用數學運算電路表達:

假設A自己擁有一個祕密w = 賬戶1的餘額。A和B都事先知道了賬戶1的餘額的哈希值H1(我們可以用x來表示)。我們可以構造數學運算電路C:Hash(w) - x = 0。如果C(x, w) = 0,那麼代表Hash(w) = x。

在這裏插入圖片描述
爲了簡化表述,我們在圖上用一個抽象的模塊表示哈希函數。在實際運用當中,我們一般會使用SHA256哈希函數。當我們得到最終的電路C之後,我們使用Setup算法生成參數,再用Prove算法生成簡短證明。

通過這一步,**我們會得到一個公開的x和一個證明\pi,並且這個x的值就是賬戶1的餘額的哈希值。**雖然我們看不到賬戶1真正的餘額,但是因爲強大的SNARK證明,我們不得不相信x = Hash(w)。

第二步:從H1開始完成Merkle證明

前一步讓我們得到了H1,並且也提供了證明代表H1真的是從原本的世界狀態中生成的。我們現在要做的事情,就是從H1開始,分別與相鄰的H0和H5結合,生成一個新的世界狀態承諾。如果我們比較這個生成的承諾和區塊鏈上保存的老的承諾是相同的,那麼我們就可以相信賬戶1的餘額真的在這個世界狀態之中。

在這裏插入圖片描述
總結起來看,我們把所有權證明分成了兩步,第一步證明祕密的w可以被哈希函數轉換爲x,再證明公開的x屬於整個世界狀態的Merkle樹。提交證明的A知識證明她知道一個祕密賬戶1,並且這個賬戶在當前的世界狀態中。驗證證明的B或者其他人,仍然無從知曉到底A知道的是哪個賬戶,但是又不得不相信是真的。

私密交易總結

看到這裏,想必大家已經對私密交易是如何實現的已經有了一個大概的瞭解。現在我來總結一下如果A要向B支付一筆私密交易,她一共要做哪些事。

  1. 首先,A需要向全網提交一筆私密交易,這一筆交易裏面有交易的發起人和收款人,但是並沒有任何數量,原本的輸入輸出的數量被幾串亂碼代替了。
  2. 在這筆交易的下面,A需要附加一項Pederson承諾,證明輸入和輸出的數字相加起來是相等的。
  3. 然後A需要再附加一個通過SNARK生成的區間證明(Range Proof),證明輸入和輸出的數字都是大於0的正整數。
  4. 最後,A需要附加一個所有權證明,證明她真的擁有一個賬戶w,裏面存了錢。這個所有權證明分爲兩個部分,一個是通過SNARK生成的哈希值證明,另一個就是一個Merkle證明,證明了前面的哈希值真的屬於這個世界狀態。

通過這四步,A就可以把所有要生成的東西打個包,然後發到網上。礦工們看到這個包,以此把所有的證明都用Verify來驗證一下。如果沒問題的話,那麼交易就完成啦。是不是很簡單?

未完待續

因爲篇幅的原因,這次就講到這裏啦。想必大家看到這裏,對於零知識證明的「證明」部分已經有所瞭解了,大概知道了數學運算電路是個啥,然後我們如何把現實生活中的問題轉化爲電路。

相信很多人會好奇,那麼究竟Setup,Prove和Verify這三個算法是如何工作的呢?下一期,我們繼續deep dive一下,深入瞭解一下SNARK證明系統背後的原理。

更多閱讀

和往常一樣,在文末給出一部分reading list,有興趣的朋友可以深入瞭解一下:

  1. 數學運算電路: https://www.jianshu.com/p/246a69921e98
  2. Merkle證明: http://ethbook.abyteahead.com/ch4/merkle.html
  3. V神的數學電路教程: https://medium.com/@VitalikButerin/quadratic-arithmetic-programs-from-zero-to-hero-f6d558cea649
  4. Zcash7篇的中文翻譯: https://www.jianshu.com/p/b6a14c472cc1?from=timeline
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章