
本文作者東澤,來自安比技術社區的小伙伴,目前就讀於斯坦福大學,研究方向密碼學,本系列文章來源於作者在斯坦福著名的課程《CS 251: Cryptocurrencies and blockchain technologies》上的學習筆記,該課程授課老師是密碼學大拿Dan Boneh。
上一期文章發表之後,非常驚訝有那麼多小伙伴讀完了表示喜歡。那麼我們接著這期繼續吧!這次,我們專注的聊一聊SNARK。
一級標題
一級標題
二級標題
二級標題
第一步:確定有限域
在構造之前,我們先需要確定一個可以包含所有我們想要計算的數字的大小的一個有限域(Finite Field)。用通俗易懂的話來說,我們需要選一個很大很大的數字p,確保這個數字比我們要解決的問題裡的所有數字都大。
如果以前了解過類似於RSA密碼學的朋友們可能對這個概念不陌生。有限域就是給我們所有數字加了一個天花板,確定了整個數學系統的計算空間,類似於在電腦裡如果我們想創建一個存數字的變量,我們需要思考到底是需要一個uint32_t(32位),還是一個uint64_t(64位)一樣。所有超過有限域的值,都會直接溢出之後再倒回去從0開始。
二級標題
二級標題
第二步:構建數學運算電路(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。也就是說,我們就可以把上文提到的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之後,我們可以計算出這個運算電路的複雜度。
二級標題
二級標題
第四步:非交互簡短證明體系(SNARK)
當我們通過第三步得到了最終的證明電路C,還有對應的x和w之後,我們的準備工作已經做的差不多了。剩下來的事情,我們就可以交給SNARK算法來生成和驗證我們的證明了。
首先,我們看看整個非交互式證明體系的官方定義。
生成算法Setup:
生成算法Setup:
Setup算法會把我們實現確定好的電路C拿進來進行一系列的預處理(preprocessing),然後生成兩組參數。 Sp是給證明方的參數,Sv是給驗證方的。這些參數的用處就是方便雙方來生成和驗證簡短證明。一般來說,生成算法的複雜度和電路C的複雜度是等比例的,O(|C|)。
證明算法Prove:
證明算法相比不用多講了,證明方會用Prove這個算法來生成一個證明,然後把這個證明發送給驗證方。 Prove算法再生成證明的時候會用到幾乎所有的數據:預處理數據Sp,公有輸入x,還有私密輸入w。最後生成的證明的空間複雜度非常簡短:|Π| = O(log|C|)。
驗證算法Verify:
驗證算法也非常的直白,驗證方會用Verify這個算法驗證我們收到的證明。這個算法會返回一個1/0的數值,代表驗證是否通過。驗證的過程中除了需要對方提供的證明,我們還需要預處理數據,還有公有輸入x。驗證的複雜度也非常小,一般來說是O(|x| + log|C|) 。
一級標題
一級標題
實例:私密交易的輸入輸出取值區間(Range Proof)
讀過上一篇文章的朋友們應該還會記得,如果我們要進行私密交易(Confidential Transaction)的話,我們需要在交易最後附帶三組證明:
第一組是Pederson承諾,證明了輸入和輸出之間的數學關係。
第二組是區間證明,需要證明輸入和輸出的值全部取值於正整數的範圍。
第三組是所有權證明,證明交易的發起人真的有這麼多token作為輸入。
Pederson承諾的實現我們已經在上一篇文章中討論過了。現在了解完簡短證明的四步構造,我們可以來看看如何具體實現區間證明。
首先,我們需要找到一個合適的數學運算電路來描述我們想要證明的內容。 (我們默認使用正整數的有限域,選取一個非常大的質數p進行求模。)
假如我們有一個數字w,我們想要證明這個數字w不是一個負數,那麼我們可以先辦法證明它一定會取值於正整數區間。因為考慮到計算機系統裡的正整數一般不會超過256位,所以我們可以把問題弱化一下:證明一個數字w取值於0-2^256之間。(根據此條件,有限域選擇的質數p需要大於2^256。)
現在確定了要解決的問題之後,我們可以開始思考如何用加法和乘法來表達這個取值關係。還記得在前面的章節,當我們在講一個值n會取值在0和1之間的時候,我們用n *(n - 1) = 0來約束這個取值範圍。同理可得,如果我們想約束必須要取值於0和5之間的話,我們就可以用更長的一串乘法來約束它:
看到這裡,大家可能心裡已經知道如何約束這個w的值了:我們只要用同樣的辦法擴大這個等式,從(w - 1)一直連續乘到(w - 2^{256}),不就可以了?
有2^256有2^256個乘法門。光是這麼多乘法,還沒有算加法,這個電路的複雜度已經是天文數字了。就算是跑Setup可能就不知道跑到猴年馬月,所以我們說這種約束的方法是不實際的。
那還有什麼方法來約束這個數字w的空間呢?我們可以巧妙借助二進制數的結構。既然我們想要規定w是個整數,並且大於0但小於2^256,那麼我們就可以在二進制裡,把w拆分成256位,然後分別約束每一位。這樣的話,我們最後得到的電路大小只會和這個數字有多少位成正比,而不會和這個數字的最大上限有關係。複雜度一下子就下來了一大個等級。
具體是怎麼實現的呢?我們首先把w拆分成256位:
因為每一位都是二進制的,所以我們需要約束每一位的取值空間為0和1:
這個約束需要256份,每一份對應每一位。當我們把這些約束準備好之後,我們最後確定所有的位組在一起可以還原成原來的w:
我們把上文提及的256+1=257組約束電路全部拼接在一起,合併成一個輸出。最後生成的電路就是我們可以用來構建證明系統的電路C了。如果C的輸出為0,那麼代表了輸入的數字一定要滿足:
這個數字是一個正整數,可以被256位二進制數表達。
這256位二進制數的確是二進制數。 (只能取值0或者1)
這256位二進制數全部拼在一起可以重新還原輸入進來的數字。
一級標題一級標題
實例:私密交易輸入的所有權
解決了區間證明,我們來完成私密交易的最後一組證明:所有權證明,證明私密交易的輸入值合理。
看過前一篇文章的朋友應該會知道,我們講了兩種私密交易所有權證明的體系:
第一種方案是一筆交易可以直接引用上一筆交易的輸出,但是這會帶來雞和蛋的問題:難度在如何創造出第一筆私密交易來。第二種方案是在每一筆交易的下面附加另一個新的證明,證明交易發起的用戶的賬戶餘額真的有那麼多錢。
我想著重講一下第二種證明方案,也就是證明交易發起者在世界狀態中的賬戶餘額。
在很多區塊鏈的場景中,整個世界的狀態就是一個巨大的賬本。賬本的每一行都記錄了某一個用戶的賬戶餘額。
為了更方便的表示整個世界狀態,我們往往會使用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證明在密碼學上非常有用,只需要的大小就可以證明某個東西在長度為N的列表之內。往往我們都會用Merkle證明來證明一組數據屬於一個很大文件,一個用戶屬於一個很大的組織,等等。
了解完Merkle證明的原理之後,我們來看看如何證明在私密交易中,A可以證明她的餘額。
Merkle證明的精髓就是我們可以從要證明的值開始,一路往上算哈希值,並且和相鄰的節點的哈希值不斷的合併。但是我們發現一個非常致命的問題:雖然我們可以隱去世界狀態裡別人的餘額,但是我們還是必須要暴露自己的餘額(不然沒有辦法算出第一個哈希值H1)。這一點和我們私密交易的本質是違背的!
二級標題二級標題
第一步:證明餘額哈希值
第一步,我們通過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算法生成簡短證明。
通過這一步,二級標題二級標題
第二步:從H1開始完成Merkle證明
前一步讓我們得到了H1,並且也提供了證明代表H1真的是從原本的世界狀態中生成的。我們現在要做的事情,就是從H1開始,分別與相鄰的H0和H5結合,生成一個新的世界狀態承諾。如果我們比較這個生成的承諾和區塊鏈上保存的老的承諾是相同的,那麼我們就可以相信賬戶1的餘額真的在這個世界狀態之中。
總結起來看,我們把所有權證明分成了兩步一級標題一級標題
私密交易總結
看到這裡,想必大家已經對私密交易是如何實現的已經有了一個大概的了解。現在我來總結一下如果A要向B支付一筆私密交易,她一共要做哪些事。
首先,A需要向全網提交一筆私密交易,這一筆交易裡面有交易的發起人和收款人,但是並沒有任何數量,原本的輸入輸出的數量被幾串亂碼代替了。
在這筆交易的下面,A需要附加一項Pederson承諾,證明輸入和輸出的數字相加起來是相等的。
然後A需要再附加一個通過SNARK生成的區間證明(Range Proof),證明輸入和輸出的數字都是大於0的正整數。
最後,A需要附加一個所有權證明一級標題
一級標題
未完待續
因為篇幅的原因,這次就講到這裡啦。想必大家看到這裡,對於零知識證明的「證明」部分已經有所了解了,大概知道了數學運算電路是個啥,然後我們如何把現實生活中的問題轉化為電路。
相信很多人會好奇,那麼究竟Setup,Prove和Verify這三個算法是如何工作的呢?下一期,我們繼續deep dive一下,深入了解一下SNARK證明系統背後的原理。
更多閱讀
和往常一樣,在文末給出一部分reading list,有興趣的朋友可以深入了解一下: