はじめに
ゼロ知識証明 (zero knowledge proof) というものがあります。ある事を知っていることを、それを示さずに証明する方法です。ただし、これは対話型です。
そして、zk-SNARKsとは、それを非対話に行う感じのものです。詳しくはわかりません。
じゃぁどう使うの?っていうのをやっていきます。
回路を書く
ある事を知っていることを、それを示さずに証明する、といったけど、何でもできるわけじゃありません。
circomという言語で、回路を書きます。回路っていうのは、いくつかのインプットとアウトプットを持つ関数です。ただし整数しか扱えません(たぶん)。秘密鍵とかハッシュ値ってのは、整数とみなせるので扱えます。
このとき、インプットは公開か非公開かを選ぶことができます。
回路を実行する
で、その回路を実行すると、実行したという証明データができあがります。
証明を検証する
で、証明データと公開インプットを使って検証のための計算をすると、成功か失敗かが出力され、その回路で計算したかどうかが検証できる、ていう感じです。
非公開インプットは、誰も知ることができません。
不思議ですね。
やっていく(circom編)
じゃぁやっていきます。
インストール
Node.jsをインストールします。方法は省略します。
以下のページに従って、circomとsnarkjsをインストールします。ページの手順に従えばいいですが、rustのインストールがあります。
Windowsの人は、WSLを使った方がいいです。
Trusted Setup
今回は省略して、既にセットアップ済みのものをダウンロードして使用します。
$ wget https://storage.googleapis.com/zkevm/ptau/powersOfTau28_hez_final_12.ptau
このファイルは何なのか、イメージとしては、公開鍵なんだけど、だれもその秘密鍵を知らないような公開鍵。
(読み飛ばし可能)ecdsaで考えていきます。複数の人がそれぞれ秘密鍵を生成し、最初の人が自分の秘密鍵から公開鍵を生成(pub1 = priv1 * G
)。次の人は、その公開鍵に自分の秘密鍵をかけ合わせていく(pub2 = priv2 * pub1 = priv2 * priv1 * G
)。そうすると、秘密鍵のわからない公開鍵が出来上がっていく(pub = privN * ... * priv1 * G
)。こうしてできた公開鍵の秘密鍵を知るには、全員が自分の秘密鍵を明かさなければならない(=現実的に不可能)ということになります。正しいかわかりません。
あと、zk-S"T"ARKsは、このTrusted Setupが不要みたいです。
回路を書く
ハッシュを計算する回路を書きます。
circomlibというライブラリを利用します。
pragma circom 2.0.0;
include "../circomlib/circuits/eddsamimc.circom";
template Hash () {
signal input in;
signal output out;
component hasher;
hasher = MiMC7(91);
hasher.x_in <== in;
hasher.k <== 0;
out <== hasher.out;
}
component main = Hash();
インプットが1つ、アウトプットが1つです。
今回は、インプットを隠しています。アウトプットは隠すことができません。
MiMC7(補足)
MiMC7というのは、たくさんあるハッシュ関数のうちの1つです。sha256もたくさんあるハッシュ関数のうちの1つですよね。
ただ、sha256はビットシフトなどを使っているため、circomの回路でそれを表そうとすると、処理が膨大になってしまうため、向いていないそうです。
そのため、向いているといわれるMiMC7を使います。
回路をコンパイルする
回路を、r1csという形式に変換します。
$ circom circuit/hash_mimc.circom --r1cs --wasm --sym --c --output work/hash_mimc
$ snarkjs r1cs export json work/hash_mimc/hash_mimc.r1cs work/hash_mimc/hash_mimc.r1cs.json
証明鍵と検証鍵を生成する
Trusted Setupで作成したファイルを元に、証明鍵と検証鍵を作成します。
$ snarkjs plonk setup work/hash_mimc/hash_mimc.r1cs work/hash_mimc/powersOfTau28_hez_final_12.ptau work/hash_mimc/circuit_final.zkey
$ snarkjs zkey export verificationkey work/hash_mimc/circuit_final.zkey work/hash_mimc/verification_key.json
witnessを作成する
回路とインプットを元に、witnessを作成します。
witnessは何なのかはうまく説明できません。
$ node work/hash_mimc/hash_mimc_js/generate_witness.js work/hash_mimc/hash_mimc_js/hash_mimc.wasm input/hash_mimc.json work/hash_mimc/witness.wtns
証明を作成する
witnessと証明鍵をもとに、証明ファイルを作成します。
$ snarkjs plonk prove work/hash_mimc/circuit_final.zkey work/hash_mimc/witness.wtns work/hash_mimc/proof.json work/hash_mimc/public.json
出来上がったファイル1
インプット&アウトプットの情報です。
(確か)公開インプット、アウトプットの順で並ぶようになってたと記憶しています。
今回は、公開インプット無しなので、これがアウトプットです。
アウトプットは何かというと、ハッシュを計算した結果です。
[
"14679259071774619046386573817170363262487274857611378774265407135291943327326"
]
出来上がったファイル2
こちらは証明。素人が見てもよくわかりませんね。
{
"A": [
"18210713584741413304383708883859678051026675788060908320929257456570350568287",
"1036889844104614135523994644813325634626570150606663278810876136176094507111",
"1"
],
"B": [
"9161135791785572360214430884186479872338301529860189325476701014496934277240",
"9894973322615344526420675581281925853620748181597015424085029252865772737422",
"1"
],
"C": [
"19583245311386550022307596618908908544022106777332374638905369591257019082694",
"17698565667462464574371305190495087867275782987885439778991332447707312822183",
"1"
],
"Z": [
"20775064365748932645112709478519758808129146137729687674374687750877181597486",
"8763863666341414270052704322525065700849438144109816728091971217484217029364",
"1"
],
"T1": [
"20560393141745625465339335683811806079554264645613916751419667839961745267247",
"8806677551366950589757912338378842190148264001300842316660392704226251862149",
"1"
],
"T2": [
"19299763566619768999807704669414445124089777759895183224241822593975854178252",
"17911120820116807207903587510252579655444199026850365328557662362700033641016",
"1"
],
"T3": [
"19020861239251074761483383364823043123395352994585687875180755737806322161425",
"19801380951875078868018747398818363697435137520936739306717215779478607984449",
"1"
],
"Wxi": [
"7026295265879087211911024520616175044034812336018387543646832081735541548036",
"19858993904662303214945148582568302790782910848777759594984386017362386972277",
"1"
],
"Wxiw": [
"29103668930354044492321474899809209742867065041919983871145720135392075268",
"11612640336453353020697739507687668964372258894264111214629431185809452314597",
"1"
],
"eval_a": "15851879281267726743291985744008606962219757569239526240313680152508291823287",
"eval_b": "16928458435945260515153096218920728116218914427258460062826788273141944700922",
"eval_c": "12656881608715779817311922031156016431654134537238518104272980268959206216520",
"eval_s1": "8067125843144445157597197724581775127257822184350603857170286307628870400338",
"eval_s2": "14803237852001390991619914342432673532241533958843497467479526885013894612187",
"eval_zw": "18728414295798306862860575957989204664131135788145062424435611251570038826831",
"protocol": "plonk",
"curve": "bn128"
}
証明を検証する
OKと出ればいいと思います。
$ snarkjs plonk verify work/hash_mimc/verification_key.json work/hash_mimc/public.json work/hash_mimc/proof.json
[INFO] snarkJS: PLONK VERIFIER STARTED
[INFO] snarkJS: OK!
これで、ハッシュの計算をしたということを、元の値を示すことなく証明することができました。
やっていく(symbol編)
さて、ここからタイトルに書いてあることをの後半です。
zk-SNARKsの証明ができたので、Symbolブロックチェーンに書き込んでいきます。
ファイルの用意とデータの用意
さて、上記で生成したファイルをsymbolに書き込んでみます。
とりあえず検証に必要なのは、verification_key.json
とpublic.json
とproof.json
みたいなので、これを書き込んでいきます。
まずは、ファイルを結合しておきます。使いやすくするために。
結合したもの
{
"public.json": [
"14679259071774619046386573817170363262487274857611378774265407135291943327326"
],
"proof.json": {
"A": [
"18210713584741413304383708883859678051026675788060908320929257456570350568287",
"1036889844104614135523994644813325634626570150606663278810876136176094507111",
"1"
],
"B": [
"9161135791785572360214430884186479872338301529860189325476701014496934277240",
"9894973322615344526420675581281925853620748181597015424085029252865772737422",
"1"
],
"C": [
"19583245311386550022307596618908908544022106777332374638905369591257019082694",
"17698565667462464574371305190495087867275782987885439778991332447707312822183",
"1"
],
"Z": [
"20775064365748932645112709478519758808129146137729687674374687750877181597486",
"8763863666341414270052704322525065700849438144109816728091971217484217029364",
"1"
],
"T1": [
"20560393141745625465339335683811806079554264645613916751419667839961745267247",
"8806677551366950589757912338378842190148264001300842316660392704226251862149",
"1"
],
"T2": [
"19299763566619768999807704669414445124089777759895183224241822593975854178252",
"17911120820116807207903587510252579655444199026850365328557662362700033641016",
"1"
],
"T3": [
"19020861239251074761483383364823043123395352994585687875180755737806322161425",
"19801380951875078868018747398818363697435137520936739306717215779478607984449",
"1"
],
"Wxi": [
"7026295265879087211911024520616175044034812336018387543646832081735541548036",
"19858993904662303214945148582568302790782910848777759594984386017362386972277",
"1"
],
"Wxiw": [
"29103668930354044492321474899809209742867065041919983871145720135392075268",
"11612640336453353020697739507687668964372258894264111214629431185809452314597",
"1"
],
"eval_a": "15851879281267726743291985744008606962219757569239526240313680152508291823287",
"eval_b": "16928458435945260515153096218920728116218914427258460062826788273141944700922",
"eval_c": "12656881608715779817311922031156016431654134537238518104272980268959206216520",
"eval_s1": "8067125843144445157597197724581775127257822184350603857170286307628870400338",
"eval_s2": "14803237852001390991619914342432673532241533958843497467479526885013894612187",
"eval_zw": "18728414295798306862860575957989204664131135788145062424435611251570038826831",
"protocol": "plonk",
"curve": "bn128"
},
"verification_key.json": {
"protocol": "plonk",
"curve": "bn128",
"nPublic": 1,
"power": 9,
"k1": "2",
"k2": "3",
"Qm": [
"11946481510570302830130428022820583260039923701075609452714919326271399825150",
"18674272822088357201121684104660678764600858262913533363642287945237683306772",
"1"
],
"Ql": [
"5655003747452304112532596273550199456904050324321084952199051618234631575224",
"354465451658473780395016751528776433442565501713466257930113646289963098056",
"1"
],
"Qr": [
"12352673245599067024134189052881344678568166985132347510732629760088561933935",
"3157179485577694994299100863892523357356603542881352109293603658524294029880",
"1"
],
"Qo": [
"11946481510570302830130428022820583260039923701075609452714919326271399825150",
"3213970049750918021124721640596596324095452894384290299046749949407542901811",
"1"
],
"Qc": [
"4970286826185715328969237704352137004730555364384202513058826543781634185740",
"20110152650648207581229515591006720968750823610772215292984125393896944882725",
"1"
],
"S1": [
"4724603855190408772215100257409659672868011346196930387900810968189803467756",
"16520100844249415892204693633999648441081840808139070959689298732048187420176",
"1"
],
"S2": [
"11624634493122308789510066132845345735570823801316626155341914609445792783943",
"13719579607841840505155637631389615570873621949916778000253468977856726036410",
"1"
],
"S3": [
"20141086937811269661448922668828390151779112603497127808778414524194728053702",
"10487127928380283000594984473349992107851979546370953071365252967802272336772",
"1"
],
"X_2": [
[
"21831381940315734285607113342023901060522397560371972897001948545212302161822",
"17231025384763736816414546592865244497437017442647097510447326538965263639101"
],
[
"2388026358213174446665280700919698872609886601280537296205114254867301080648",
"11507326595632554467052522095592665270651932854513688777769618397986436103170"
],
[
"1",
"0"
]
],
"w": "6837567842312086091520287814181175430087169027974246751610506942214842701774"
}
}
そして、このjsonファイルを読み込んで、途中の空白とかをカットしてから、ブロックチェーンに書き込みます。
どうやって書き込むのかというと、転送トランザクションのメッセージを使おうと思ってます。
なので、1トランザクションあたり、1023バイトまでしか書き込めないので、(全部1バイト文字だと思い込んでおいて)1023文字ずつ区切って、Uint8Array形式に変換します。
import fs from 'fs';
function splitStringByLength(str, length) {
const result = [];
for (let i = 0; i < str.length; i += length) {
result.push(str.slice(i, i + length));
}
return result;
}
const all = JSON.parse(fs.readFileSync('./all.json', { encoding: 'utf-8' }));
const allText = JSON.stringify(all);
const messages = splitStringByLength(allText, 1023)
.map((chunk) => {
const encoder = new TextEncoder();
const encoded = encoder.encode(chunk);
return new Uint8Array([0x00, ...encoded]);
});
アグリゲートコンプリートトランザクションにて送信する
import {
PrivateKey,
} from 'symbol-sdk';
import {
KeyPair,
Network,
SymbolFacade,
} from 'symbol-sdk/symbol'
import {
NODE_URL,
PRIVATE_KEY,
} from './env.js';
const network = Network.TESTNET
const facade = new SymbolFacade(network.name);
const deadline = facade.now().addHours(2).timestamp;
const privateKey = new PrivateKey(PRIVATE_KEY);
const keyPair = new KeyPair(privateKey);
const address = network.publicKeyToAddress(keyPair.publicKey);
const innerTransactions = messages
.map((message) => {
return facade.transactionFactory.createEmbedded({
type: 'transfer_transaction_v1',
signerPublicKey: keyPair.publicKey,
recipientAddress: address,
mosaics: [],
message,
});
});
const transactionsHash = SymbolFacade.hashEmbeddedTransactions(innerTransactions)
const transaction = facade.transactionFactory.create({
type: 'aggregate_complete_transaction_v2',
signerPublicKey: keyPair.publicKey,
fee: 1000000n,
deadline,
transactions: innerTransactions,
transactionsHash,
});
const signature = facade.signTransaction(keyPair, transaction);
const jsonPayload = facade.transactionFactory.static.attachSignature(transaction, signature);
const hash = facade.hashTransaction(transaction).toString();
console.log(jsonPayload);
console.log(hash);
const sendRes = await fetch(
new URL('/transactions', NODE_URL),
{ method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: jsonPayload }
)
.then((res) => res.json());
console.log(sendRes);
await new Promise((resolve) => setTimeout(resolve, 1000));
const statusRes = await fetch(new URL("/transactionStatus/" + hash, NODE_URL))
.then((res) => res.json());
console.log(statusRes);
結果
ブロックエクスプローラーで確認します。
ええと、1つ目のメッセージの終わりが、5124089777759895183224241
で、
all.json
を見ると、5124089777759895183224241
に続いて、822593975854178252
がありますね、
2つ目のメッセージが822593975854178252
で始まっているから
これで、途切れなく入っていますね!
はい、これで書き込むだけが終わりました!
やっていく(Symbol編おまけ)
書き込んだものを取得して、証明を検証します。
import fs from 'fs';
import {
NODE_URL,
TRANSACTION_HASH,
} from './env.js';
const result = await fetch(
new URL(`/transactions/confirmed/${TRANSACTION_HASH}`, NODE_URL),
)
.then((res) => res.json());
const aggregateTransaction = result.transaction;
const innerTransactions = aggregateTransaction.transactions;
const allText = innerTransactions
.map(({ meta, transaction }) => {
const message = transaction.message;
const buffer = Buffer.from(message.slice(2), 'hex');
return buffer.toString('utf8');
})
.join('');
const all = JSON.parse(allText);
const publicJson = all['public.json'];
const proofJson = all['proof.json'];
const verificationKeyJson = all['verification_key.json'];
fs.writeFileSync('./public.json', JSON.stringify(publicJson, null, 2));
fs.writeFileSync('./proof.json', JSON.stringify(proofJson, null, 2));
fs.writeFileSync('./verification_key.json', JSON.stringify(verificationKeyJson, null, 2));
これでファイルが取得できました。検証します。
$ snarkjs plonk verify verification_key.json public.json proof.json
[INFO] snarkJS: PLONK VERIFIER STARTED
[INFO] snarkJS: OK!
おわりに
ここまで超ざっくりとzk-SNARKsの証明をSymbolブロックチェーンに書き込んできました。
とりあえずやればできました。ただし、理解は浅いです。私は何を証明しているんだろうという気分になります。
何はともあれ、動かしてみることから理解のきっかけにつなげていければいいんじゃないかと思います。
今回のコードはこちらのリポジトリにおいてあります。