Box

Box

3BoxTech Founder / Web3Box Founder/ solidity dev./Several years of experience in Solidity and full-stack development.
twitter

zk-SNARKs 在 Solidity 中的使用

zk-SNARKs 在 Solidity 中的使用#

文章作者: @BoxMrChen,歡迎轉載,轉載請注明出處。
文章 Github 倉庫: https://github.com/nishuzumi/blog 歡迎 Star。如果文章有誤,歡迎提 PR。
進入交流群:歡迎添加個人微信 Im3boxtech,備註進群,我會拉你進入交流群。

本文章主要講述了如何在 Solidity 中使用 zk-SNARKs,以及如何使用 ZoKrates 編譯器來生成證明和驗證合約。

這文章不會過於深入 zk-SNARKs 的技術原理,這文章目的是為讓讀者能夠理解 zk-SNARKs 的技術能在 EVM 中達到什麼效果,如何使用,並且能在代碼中運用。

zk-SNARKs 簡介#

關於 zk-SNARKs 的簡短描述為,我們需要在 zk 電路中編寫一段代碼,這段代碼的輸入是一些公開的數據,輸出是一些私有的數據。zk-SNARKs 的驗證算法可以驗證這段代碼的輸出是否正確,但是驗證算法不會洩露任何私有數據。而 Solidity 合約的主要目的是驗證 zk-SNARKs 的驗證算法的結果,如果驗證算法的結果正確,那麼合約會執行一些操作。

也就是說,在 EVM 上,只是進行了結果的驗證,並沒有進行一些複雜的計算,這些計算都是在 zk 電路中進行的。而這部分 zk 電路,則是在鏈下進行的,然後將結果提交到鏈上。

在 Solidity 中使用 zk-SNARKs#

首先,我們需要知道 zk-SNARKs 可以完成什麼功能,其實很簡單,我們可以簡單的認為,zk-SNARKs 可以完成對一個函數運算結果的校驗,比如說,我們有一個函數,輸入是三個數字,輸出是一個數字,我們可以使用 zk-SNARKs 來校驗這個函數的輸出是否正確。但是我們並不需要知道輸入的三個數字是什麼,只需要知道這個函數的輸出即可,也就是說,在一個函數完成計算時,我們可以知道確實是有這麼三個數能符合這個函數的輸入,並且能輸出正確結果,但是我們並不知道這三個數是什麼。

在 Solidity 中,我們可以使用 zk-SNARKs 來完成對一個函數的校驗,但是我們需要知道這個函數的輸入和輸出,然後我們可以使用 ZoKrates 編譯器來生成 zk 電路,然後將 zk 電路的代碼放到 Solidity 合約中,然後在合約中完成對 zk 電路的驗證。

安裝 ZoKrates 編譯器#

安裝 ZoKrates

curl -LSfs get.zokrat.es | sh

也可以選擇其他安裝方式,具體選擇查看他們的 Github 頁面。

image

編寫 zk 電路#

從上一章節我們淺顯的知道,一個 zk-SNARKs 電路需要的最基本的東西為:

  • 一個函數 - 我們需要有一個函數對數據進行運算,也就是程序 C
  • lambda - 所謂的 “有毒廢料”,其實就是一個 root key,我們需要通過它來生成 pk 和 vk

有了這兩個基礎條件,用戶就可以通過 pk,目標值,輸入值來生成證明 w。
隨後,我們的驗證程序通過 vk,目標值,證明 w 來驗證證明的正確性。

我們先假設有這麼一個第三方,他可以安全的生成 lambda,然後安全的將程序和 lambda 進行運算生成 vk 和 pk。

那麼現在有兩個新的角色,user 和 project。user 是用戶,他確確實實擁有著一些數據,project 是項目合約,他需要驗證用戶的數據是否正確。

一個函數#

我們首先需要一個函數,但是我並不打算舉一些簡單例子,因為我覺得這樣做非常沒有意義,因為 zk-SNARKs 的主要目的是為了驗證一些複雜的函數,而不是一些簡單的函數。

比如,我們現在需要生成一個存款憑證,有這個憑證,我們可以在任何地方取出這筆錢,但是我們並不知道這筆錢是誰的,我們只知道這筆錢是誰存的,存了多少,以及存款的時間。

首先我們需要一個存款函數,這個函數的輸入為存款的金額,和一個隨機數,然後輸出為一個存款憑證。任何擁有這個憑證的人都可以取出這筆資金。所以,實際上,我們只需要編寫驗證知道這個憑證的驗證函數即可。

import "hashes/sha256/512bit" as sha256;
import "utils/pack/u32/nonStrictUnpack256" as unpack256;

// deposit_amount: 存款金額
// secret: 隨機數
// returns: 用於取款的commitment
def main(field deposit_amount, private field secret) -> u32[8] {
    return sha256(unpack256(deposit_amount), unpack256(secret));
}

關於 Zok 的語法和用法這裡不過多描述,具體可以參考官網,這裡簡單解釋一下,這個函數的輸入為兩個數字,一個是存款金額,一個是隨機數,然後輸出為一個 u32 [8],實際上就是 uint256。同時我們注意一下,參數中 deposit_amount 沒有 private 關鍵詞,說明這個參數是公開數據。

編譯文件#

這部分內容在 zokrates 中有講述方式為

# compile
zokrates compile -i deposit.zok
# perform the setup phase
zokrates setup
# execute the program
zokrates compute-witness -a 337 113569
# generate a proof of computation
zokrates generate-proof
# export a solidity verifier
zokrates export-verifier
# or verify natively
zokrates verify

運行完成後會生成一堆文件,我們需要的是 proof.json, proving.key, verification.key, verifier.sol, out。

大部分其實都是模板文件生成文件可能不一樣的地方在於 Verifier 合約中 verifyingKey,當然,我們閱讀這個文件其實意義也不大,因為這裡面全是一大堆數字和運算。
實際上我們需要看的內容就是這些 ∑

function verifyTx(
        Proof memory proof, uint[8] memory input
    ) public view returns (bool r) {
    uint[] memory inputValues = new uint[](8);

    for(uint i = 0; i < input.length; i++){
        inputValues[i] = input[i];
    }
    if (verify(inputValues, proof) == 0) {
        return true;
    } else {
        return false;
    }
}

可以看到,我們需要兩個參數,proof 和 input。至於這兩個參數是幹嘛的,我們暫時不過多深究。不過我們需要注意的是,在 inputs 中,所有的共有參數都會被加入到這個數組中,在數字最開頭部分被推入。

比如,自動生成的 proof.json 文件就是一個有效的數據。

{
  "scheme": "gm17",
  "curve": "bn128",
  "proof": {
    "a": [
      "0x05a83e3c3b3ff9d59bdffdcf7aa655f42b941b0063f82cf26516846056d09aa6",
      "0x018039b7de92979ef6251c877971888ae049d09a6b48e5aa98c23ef91550ed36"
    ],
    "b": [
      [
        "0x1e88e783456a27e4f02dde8c742610339e395eb0bbf7f7efc1113815dcf0a16f",
        "0x1cc9de9e60c6519ea69c9b3a71c0809ac7ae3389a598d66fc27d378738d5de29"
      ],
      [
        "0x0715544abbc18e741620ff7c76cb2a7d3558ee157d23f275ab65c43c25357d07",
        "0x0344257236ba33a3ce7ce34b8d518f7572984036db6f77fc2fc13f51c548a837"
      ]
    ],
    "c": [
      "0x177113e528c76661a03a8f3f072f29e684244297a62926a0000d3a7135c1441f",
      "0x18cf275d0bc621473688848946584af771afca42e4f2bd0ef1e5d06e0adefd0f"
    ]
  },
  "inputs": [
    "0x0000000000000000000000000000000000000000000000000000000000000151",
    "0x00000000000000000000000000000000000000000000000000000000bb3eada7",
    "0x000000000000000000000000000000000000000000000000000000004b704815",
    "0x00000000000000000000000000000000000000000000000000000000cddda451",
    "0x00000000000000000000000000000000000000000000000000000000ca701d2a",
    "0x000000000000000000000000000000000000000000000000000000001f278e64",
    "0x00000000000000000000000000000000000000000000000000000000ef16f074",
    "0x0000000000000000000000000000000000000000000000000000000040e13298",
    "0x0000000000000000000000000000000000000000000000000000000026c5da72"
  ]
}

至此,我們可以寫一個簡單合約。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Verifier} from "./verifier.sol";

contract Master {
    event Despoit(uint256 commitment, uint amount);

    mapping(uint => uint) public proofs;

    Verifier v;

    constructor() {
        v = new Verifier();
    }

    function deposit(uint commitment) public payable {
        proofs[commitment] = msg.value;
        emit Despoit(commitment, msg.value);
    }

    function withdraw(
        uint commitment,
        Verifier.Proof memory proof,
        uint[9] memory inputs
    ) public {
        uint amount = inputs[0];
        require(v.verifyTx(proof, inputs));
        require(proofs[commitment] == amount);

        payable(msg.sender).transfer(amount);
    }
}

要注意的是 Verifier 合約中會出現兩個pragma solidity,記得刪掉中間那一個,保留最上面的那個,否則編譯無法通過。

測試#

首先我們需要明白一下標準流程,我們需要先進行 compile,setup,然後再進行 compute-witness,然後再進行 generate-proof,最後再進行 export-verifier。

但是這套流程並不是每次都必須的,因為這是一個完整流程。我們需要進行一下區分。

必要條件#

  • compile - 編譯 zk 電路 - 只需要執行一次 這個功能會生成 out 文件和 abi.json 文件,這兩個是編譯後的程序。
  • setup - 生成 zk 電路的 pk 和 vk - 只需要執行一次 這個功能會生成 proving.key 和 verification.key 文件,這兩個文件是 zk 電路的公鑰和私鑰。實際上在進行 setup 的時候會產生 lambda,但是這些過程我們不需要太過於關心。

提交證明條件#

  • compute-witness - 生成證明 - 這個功能會生成 witness 文件,這個文件是一個中間文件。
  • generate-proof - 生成證明的 Proof - 這個功能會生成 proof.json 文件,這個文件是證明需要提交的內容,一般來說裡面的內容就是我們需要提交到鏈上的參數。

接受證明條件#

  • export-verifier - 生成 verifier.sol - 這個功能會生成 verifier.sol 文件,這個文件是一個合約,我們需要將這個合約部署到鏈上,然後在我們的合約中調用這個合約來驗證證明的正確性。
  • verify - 本地驗證 - 這個功能會驗證證明的正確性,但是這個功能並不會生成任何文件。

編寫文件#

根據上面內容,我們可以寫出一些用於測試的單元測試邏輯。

import { expect } from "chai";
import { ethers } from "hardhat";
import { Verifier } from "../typechain-types/Verifier";
import { CompilationArtifacts, ZoKratesProvider } from "zokrates-js";
import { readFileSync } from "fs";
import { Master } from "../typechain-types";
import { resolve } from 'path'

describe("Verifier", function () {
  let master: Master;
  let zokratesProvider: ZoKratesProvider

  const zokArtifacts: CompilationArtifacts = {
    program: readFileSync(resolve(__dirname, '../zok/out')),
    abi: JSON.parse(readFileSync(resolve(__dirname, '../zok/abi.json'), 'utf-8'))
  }

  const provingKey = readFileSync(resolve(__dirname, '../zok/proving.key'))

  beforeEach(async () => {
    const { initialize } = await import("zokrates-js");
    zokratesProvider = (await initialize()).withOptions({
      backend: 'ark',
      curve: 'bn128',
      scheme: 'gm17'
    });

    const bn256 = await ethers.getContractFactory("BN256G2").then((f) => f.deploy());

    master = await ethers.getContractFactory("Master", {
      libraries: {
        "contracts/verifier.sol:BN256G2": bn256.address,
      }
    }).then((f) => f.deploy());
  })

  it("should verify a proof", async () => {
    const { witness, output } = zokratesProvider.computeWitness(
      zokArtifacts,
      [`${ethers.constants.WeiPerEther}`, '23'],
    )

    const commitment = hexListToUint256BigEndian(JSON.parse(output)).toString();

    await master.deposit(
      commitment,
      { value: ethers.constants.WeiPerEther }
    )

    const proof = zokratesProvider.generateProof(
      zokArtifacts.program,
      witness,
      provingKey);


    const sender = (await ethers.getSigners())[0];
    expect(() => master.connect(sender).
      withdraw(commitment, proof.proof as Verifier.ProofStruct, proof.inputs)
    ).to.changeEtherBalance(sender, ethers.constants.WeiPerEther);
  })
});


function hexListToUint256BigEndian(hexList: string[]) {
  let uint256Data = "0x";
  for (const hex of hexList) {
    const cleanedHex = hex.replace("0x", "");
    uint256Data += cleanedHex;
  }
  const uint256BigNumber = ethers.BigNumber.from(uint256Data);
  return uint256BigNumber;
}

image

項目的基礎文件都放在: https://github.com/nishuzumi/blog/tree/main/sources/zk 中,歡迎 Star。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。