昨天介紹完了所有我會使用到Go-ethereum的功能後,今天要來介紹一個新的工具,我們既然已經有私有鏈,也能做交易,還可以部屬合約跟與合約互動,那現在就只差有個可以實作零知識證明的工具了!而我在這次實驗所要用的就是gnark,gnark是由ConsenSys開發出來的zk-SNARKs工具,而ConsenSys就是做小狐狸錢包的那間公司,之所以用gnark是因為我覺得使用這個來做開發可能會比較好懂,加上我整體都是使用Golang語言做開發,讓整體更加一致,但是gnark到現在還沒有一個穩定的版本,而且在官方的公開文件中有說到不要使用此工具來開發商業價值的東西,因為現在這東西還有些危險性在,但我只是拿來做示範,千萬不要看到我這邊的教學就一股腦的拿這個東西下去開發,這邊我通常還是建議使用之前我所推薦的工具。
我將使用的是他0.7.0版本來做示範,至於要如何將gnark納入自己的Golang專案,可以進自己的Golang專案資料夾,下以下指令:
$ go get github.com/consensys/gnark
這樣便可以將gnark放進你的Golang專案中。
之前有介紹過,在使用zk-SNARKs工具的時候,在設計零知識證明的過程都稱為寫電路,原因之前也有探討過了,這邊就不再贅述,而開發零知識證明的項目最重要的要先把這次要證明的命題訂出來,而在定命題的時候,gnark是要定義出一個struct作為命題,而struct裡面含的成員變數為該命題的輸入,這個輸入包含公開資料與私有資料,而在gnark中,有限定這些成員變數的型別都要是在"github.com/consensys/gnark/frontend"
中的Variable
,接下來我就隨便定一個命題作為示範,並且先示範程式怎麼寫,再來講解裡面的細節,先上程式:
import (
"github.com/consensys/gnark/frontend"
)
type Circuit struct {
X frontend.Variable `gnark:"x"`
Y frontend.Variable `gnark:"y"`
Z frontend.Variable `gnark:"z,public"`
}
這邊可以簡單的發現到,我這次的命題有三個輸入,分別是x、y跟z,而這個z是公開資料,代表著之後在驗證的時候,是要用z跟產生出來的證明去做驗證的,訂好命題的輸入之後,就可以定義這個命題的規則長怎麼樣了,而定義這個命題的規則,在gnark中便是要寫一個這個struct的member function,而且這個函式名稱必須要是Define,而且必須帶入"github.com/consensys/gnark/frontend"
中的API
,而這個API
裡面定義了許多給命題輸入的運算子跟限制條件,我們就是要用這個API
來去定義出我們的命題,程式如下:
func (circuit *Circuit) Define(api frontend.API) error {
sum := api.Add(circuit.X, circuit.Y)
api.AssertIsEqual(circuit.Z, sum)
return nil
}
透過程式的名稱,其實我們也容易了解這個命題在做什麼,這個命題很顯然的是在說,現在有個公開的Z,而有人有他私有的X跟Y,他想要在不公開X跟Y的情況下,向其他人證明出自己私有的X跟Y相加會等於Z,雖然根據之前的探討,這個並不構成零知識證明的命題,但是今天只是帶大家看gnark怎麼使用,而非真的要做一個零知識證明,所以這個命題僅能拿來示範,當我們定義好這個命題之後,根據之前所說的zk-SNARKs的流程,接著就要跑zk-SNARKs的G、P跟V演算法了。
一旦定義好了命題,接著按照之前所說的流程這邊要跑G演算法產生出證明鑰匙跟驗證鑰匙,然而在zk-SNARKs有許多種不同的證明系統,不同的證明系統將命題轉換成的數學式不太一樣,我這邊是使用Groth16,程式碼如下:
import (
"github.com/consensys/gnark-crypto/ecc"
"github.com/consensys/gnark/backend/groth16"
"github.com/consensys/gnark/frontend"
"github.com/consensys/gnark/frontend/cs/r1cs"
)
func main() {
var circuit Circuit
ccs, err := frontend.Compile(ecc.BN254, r1cs.NewBuilder, &circuit)
if err != nil {
panic(err)
}
pk, vk, err := groth16.Setup(ccs)
if err != nil {
panic(err)
}
}
所以看到上面程式,在產生證明鑰匙跟驗證鑰匙之前,要先將電路做一次編譯,而編譯要帶進去的參數除了剛剛寫的命題之外,還有額外帶入兩個,一個是在選擇這次證明的橢圓曲線,另一個是在選擇這次證明的證明系統,如果聽不懂沒關係,因為我沒有講很深,基本上這邊就按照上面設定的去寫,編譯好之後會產生一個叫做Constraint System的東西,接著才會拿他進我們所選的證明系統產生出證明鑰匙跟驗證鑰匙,由於一次命題可能會多次證明,而一把證明鑰匙跟一把驗證鑰匙是對應的,不同次生成的證明鑰匙跟驗證鑰匙即使是命題一模一樣,也不能混著用,所以如果要證明多次要將他們連同著Constraint System一起存下來之後才不會重複跑這個過程,如果有看之前的文章可能會懷疑Power of Tau去哪了,在gnark實作中,他是使用自己定的Power of Tau帶入,所以實作上有一定的危險性在。
得到了證明鑰匙,我們可以拿他來產生出證明,而拿他產出證明的程式碼如下:
import (
"github.com/consensys/gnark-crypto/ecc"
"github.com/consensys/gnark/backend/groth16"
"github.com/consensys/gnark/frontend"
)
func main() {
var circuit Circuit
circuit.X = 2
circuit.Y = 3
circuit.Z = 5
witness, err := frontend.NewWitness(&circuit, ecc.BN254)
if err != nil {
panic(err)
}
proof, err := groth16.Prove(ccs, pk, witness)
if err != nil {
panic(err)
}
}
我們將Circuit填入我們想帶入的輸入,接著透過"github.com/consensys/gnark/frontend"
提供的func frontend.NewWitness(assignment frontend.Circuit, curveID ecc.ID, opts ...frontend.WitnessOption) (*witness.Witness, error)
來產生出證明所需要的型別,將這個東西連同著編譯好的Constraint System跟證明鑰匙帶入證明函式,就可以產生出證明,如果當代入的輸入是錯的,就不會產生出對應的證明。
當我們有了證明,就可以去驗證這個證明,程式如下:
import (
"github.com/consensys/gnark/frontend"
"github.com/consensys/gnark/backend/groth16"
)
func main() {
var circuit Circuit
circuit.Z = 5
witness, err = frontend.NewWitness(&circuit, ecc.BN254)
if err != nil {
panic(err)
}
publicDate, err := witness.Public()
if err != nil {
panic(err)
}
err = groth16.Verify(proof, vk, publicDate)
if err != nil {
panic(err)
}
}
這邊基本上就是拿著之前的witness做出來的公開資料以及之前產生出的證明還有之前生成出的驗證鑰匙來進行驗證,一旦沒有錯誤就代表著這次驗證成功。
但是不太有可能就像這樣子自己產生證明給自己驗證的,之前說過,常見應用是將驗證鑰匙寫進一個能驗證證明的智能合約中,透過執行該智能合約,讓鏈上的礦工進行驗證,那要怎麼做到呢?以下便帶你們來做做看。
在鏈上驗證證明之前,我們應該要寫一個智能合約來驗證,至於要怎麼寫這個合約,其實gnark有辦法自動幫你產生出來,在剛開始編譯電路的時候,會產生驗證鑰匙跟證明鑰匙,當時就可以將驗證鑰匙變成一個智能合約了,程式碼如下:
import (
"os"
"github.com/consensys/gnark-crypto/ecc"
"github.com/consensys/gnark/backend/groth16"
"github.com/consensys/gnark/frontend"
"github.com/consensys/gnark/frontend/cs/r1cs"
)
func main() {
var circuit Circuit
ccs, err := frontend.Compile(ecc.BN254, r1cs.NewBuilder, &circuit)
if err != nil {
panic(err)
}
pk, vk, err := groth16.Setup(ccs)
if err != nil {
panic(err)
}
f, err := os.OpenFile("./constraintSystem", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
panic(err)
}
_, err = ccs.WriteTo(f)
if err != nil {
panic(err)
}
err = f.Close()
if err != nil {
panic(err)
}
f, err = os.OpenFile("./provingKey", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
panic(err)
}
_, err = pk.WriteRawTo(f)
if err != nil {
panic(err)
}
err = f.Close()
if err != nil {
panic(err)
}
f, err = os.OpenFile("./verifier.sol", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0777)
if err != nil {
panic(err)
}
err = vk.ExportSolidity(f)
if err != nil {
panic(err)
}
err = f.Close()
if err != nil {
panic(err)
}
}
透過以上程式,基本上不只產生出智能合約,還順便將Constraint System跟證明鑰匙一起匯出,這樣之後只要直接讀取檔案就好,不用在進行編譯。
而這個智能合約可以透過昨天講的方式,使用Abigen變成一個Golang的package,然後使用一個程式進行部署,部屬好後可以得到他的合約地址,之後就可以寫一個鏈上驗證的程式了!
完整驗證程式如下:
package main
import (
"bytes"
"context"
"fmt"
"math/big"
"os"
"專案名稱/contracts"
"github.com/consensys/gnark-crypto/ecc"
"github.com/consensys/gnark/backend/groth16"
"github.com/consensys/gnark/frontend"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
func main() {
// Read Constraint System and Proving Key
f, err := os.Open("./constraintSystem")
if err != nil {
panic(err)
}
ccs := groth16.NewCS(ecc.BN254)
_, err = ccs.ReadFrom(f)
if err != nil {
panic(err)
}
err = f.Close()
if err != nil {
panic(err)
}
f, err = os.Open("./provingKey")
if err != nil {
panic(err)
}
pk := groth16.NewProvingKey(ecc.BN254)
_, err = pk.ReadFrom(f)
if err != nil {
panic(err)
}
err = f.Close()
if err != nil {
panic(err)
}
// Prove
var circuit Circuit
circuit.X = 2
circuit.Y = 3
circuit.Z = 5
witness, err := frontend.NewWitness(&circuit, ecc.BN254)
if err != nil {
panic(err)
}
proof, err := groth16.Prove(ccs, pk, witness)
if err != nil {
panic(err)
}
const fpSize = 32
var buf bytes.Buffer
proof.WriteRawTo(&buf)
proofBytes := buf.Bytes()
var (
a [2]*big.Int
b [2][2]*big.Int
c [2]*big.Int
input [1]*big.Int
)
a[0] = new(big.Int).SetBytes(proofBytes[fpSize*0 : fpSize*1])
a[1] = new(big.Int).SetBytes(proofBytes[fpSize*1 : fpSize*2])
b[0][0] = new(big.Int).SetBytes(proofBytes[fpSize*2 : fpSize*3])
b[0][1] = new(big.Int).SetBytes(proofBytes[fpSize*3 : fpSize*4])
b[1][0] = new(big.Int).SetBytes(proofBytes[fpSize*4 : fpSize*5])
b[1][1] = new(big.Int).SetBytes(proofBytes[fpSize*5 : fpSize*6])
c[0] = new(big.Int).SetBytes(proofBytes[fpSize*6 : fpSize*7])
c[1] = new(big.Int).SetBytes(proofBytes[fpSize*7 : fpSize*8])
input[0] = new(big.Int).SetUint64(5)
// Verify
client, err := ethclient.Dial("連線網址,如果是要連你的私有鏈的話,就是 http://127.0.0.1:[你設定的PORT]")
if err != nil {
panic(err)
}
verifyContract, err := contracts.NewVerifyContract(common.HexToAddress("驗證合約的地址(含0x字首)"), client)
if err != nil {
panic(err)
}
result, err := verifyContract.VerifyProof(nil, a, b, c, input)
if err != nil {
panic(err)
}
if result {
fmt.Println("Success")
} else {
fmt.Println("Fail")
}
}
當然以上驗證程式還不能算使用鏈上幫忙驗證證明,但已經算是有個雛型了,基本上這個驗證的智能合約還要寫一層智能合約的外皮去套,但這就不是今天的範疇了,今天主要就是在講述如何使用gnark的功能做出一個小小的零知識證明的程式,基本上我這次要實作的ZK Rollup所要使用的工具都已經介紹完了,明天開始就來建立我們簡單的ZK Rollup吧!