zk-SNARKs の Solidity での使用#
文章の著者: @BoxMrChen、転載歓迎、転載の際は出典を明記してください。
記事の GitHub リポジトリ: https://github.com/nishuzumi/blog スターを歓迎します。記事に誤りがある場合は、PR を提案してください。
交流グループに参加するには:個人の WeChat 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 は関数の計算結果を検証できると考えることができます。例えば、3 つの数字を入力として、1 つの数字を出力する関数があるとします。この関数の出力が正しいかどうかを zk-SNARKs を使用して検証できます。しかし、3 つの数字が何であるかを知る必要はありません。この関数の出力が正しいことを知るだけで済みます。つまり、関数が計算を完了する際に、実際にこの関数の入力に適合する 3 つの数が存在し、正しい結果を出力できることがわかりますが、それらの 3 つの数が何であるかはわかりません。
Solidity では、zk-SNARKs を使用して関数の検証を行うことができますが、そのためには関数の入力と出力を知る必要があります。その後、ZoKrates コンパイラを使用して zk 回路を生成し、zk 回路のコードを Solidity コントラクトに配置し、コントラクト内で zk 回路の検証を行います。
ZoKrates コンパイラのインストール#
ZoKrates をインストールします。
curl -LSfs get.zokrat.es | sh
他のインストール方法も選択できます。具体的な選択は彼らの GitHub ページを参照してください。
zk 回路の作成#
前の章から、zk-SNARKs 回路に必要な最も基本的なものは次のとおりです。
- 関数 - データを計算するための関数が必要です。つまり、プログラム C です。
- lambda - いわゆる「有毒廃棄物」で、実際にはルートキーです。これを使用して pk と vk を生成します。
これらの 2 つの基本条件があれば、ユーザーは pk、ターゲット値、入力値を使用して証明 w を生成できます。
その後、検証プログラムは vk、ターゲット値、証明 w を使用して証明の正しさを検証します。
まず、第三者が安全に lambda を生成し、安全にプログラムと lambda を計算して vk と pk を生成できると仮定します。
ここで、2 つの新しい役割が登場します。user はユーザーで、実際にいくつかのデータを持っています。project はプロジェクトコントラクトで、ユーザーのデータが正しいかどうかを検証する必要があります。
関数#
まず、関数が必要ですが、簡単な例を挙げるつもりはありません。なぜなら、zk-SNARKs の主な目的は、単純な関数ではなく、複雑な関数を検証するためだからです。
例えば、私たちは現在、預金証明書を生成する必要があります。この証明書があれば、どこでもこのお金を引き出すことができますが、このお金が誰のものであるかはわかりません。私たちはこのお金が誰によって、いくら預けられ、預金の時間がいつであるかを知っているだけです。
まず、預金関数が必要です。この関数の入力は預金額とランダム数で、出力は預金証明書です。この証明書を持っている人は誰でもこの資金を引き出すことができます。したがって、実際には、この証明書の検証関数を作成するだけで済みます。
import "hashes/sha256/512bit" as sha256;
import "utils/pack/u32/nonStrictUnpack256" as unpack256;
// deposit_amount: 預金額
// secret: ランダム数
// returns: 引き出し用のコミットメント
def main(field deposit_amount, private field secret) -> u32[8] {
return sha256(unpack256(deposit_amount), unpack256(secret));
}
Zok の構文と使用法についてはここでは詳しく説明しません。具体的には公式サイトを参照してください。ここでは簡単に説明します。この関数の入力は 2 つの数字で、1 つは預金額、もう 1 つはランダム数で、出力は 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;
}
}
ここでは、2 つのパラメータ、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 コントラクト内に 2 つの 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;
}
プロジェクトの基本ファイルは次の場所にあります: https://github.com/nishuzumi/blog/tree/main/sources/zk で、スターを歓迎します。