Skip to main content

原文準拠 Phase 3:DApp開発

2.1 Proof Server と取引の流れ

Midnight Academy Phase 3 / Unit 2 / 2.1 の原文準拠版。ローカルで動く Proof Server が ZK 証明を作り、取引が「実行→残高調整→証明→送信→検証」と流れるしくみを、本物の識別子とコードで正確に。


📘 Academy原文準拠 | Phase 3 · Unit 2 · Lesson 2.1 The Proof Server & Transaction Flow 内容に忠実な日本語版です。原文(英語)・図・動画は公式 Academy(外部リンク・別タブで開きます)を正本に。

Lesson 1.3 で、あなたは契約をデプロイし、メッセージをオンチェーンに保存しました。

ちゃんと動きました。でもよく見ていた人は、ある事に気づいたはずです。状態を変える操作はどれも 20〜60 秒かかるのに、読み取りは一瞬だった、と。

この「間(ま)」では、いったい何が起きているのでしょう。

実はこの間こそ、ゼロ知識(zero-knowledge)の魔法が起きている場所です。そしてその中心にいるのが proof server。Lesson 1.1 からずっと動かしてきたのに、何をしているのかまだよく分かっていなかった、あのインフラです。

このレッスンを終えるころには、「submit(送信)」から「confirmed(確定)」までに何が起きるのか、なぜ proof server をローカルで動かすのか、そして取引が Midnight のシステムをどう流れるのかが、はっきり分かります。

なぜ proof server が必要なのか

ほとんどのブロックチェーンでは、取引の送信はシンプルです。秘密鍵でメッセージに署名し、ブロードキャストし、ネットワークがその署名を検証する。全部で数秒です。

Midnight はそうではありません。Midnight ノードは、土台となる Polkadot SDK 由来の標準的な署名ベースの取引もサポートしますが、実際のオンチェーン活動は主に、Midnight Ledger 自身が定義する証明ベース(proof-based)の取引フォーマットを使います。

このモデルでは、取引の中に「その操作が正しい」と裏づける暗号学的な証明が埋め込まれ、ネットワークは実行時に従来の署名だけに頼るのではなく、この証明を検証します。

そして、その証明を作るところが、いちばん重い処理です。多項式の評価、楕円曲線のペアリング、制約系(constraint system)の充足性チェックといった、重たい暗号計算が必要になります。これはブラウザがサッとこなせる類のものではなく、あなたの実際の CPU 上でネイティブコードとして走らせるべき計算です。

それを担うのが proof server です。

proof server は、最適化されたネイティブコードで書かれた専用サービスで、あなたの取引のためにゼロ知識証明を生成します。

あなたの DApp が contract.callTx.storeMessage("hello") を呼ぶと、SDK はそれをそのままネットワークに送るわけではありません。まず回路の入力を proof server に送り、proof server が計算をこなして ZK 証明を作る。それからようやく、証明つきの取引がネットワークに送られます。

proof server をローカルで動かすのは「わざと」

これは理解しておく価値のある、重要な設計判断です。

proof server は、どこかのリモートサービスではなく、あなたのマシン上で動きます。

ドキュメントもこの点をはっきり述べています。「proof server はあなたのプライバシーを守るために存在する。ネットワーク接続をいっさい開かず、割り当てられたポートで wallet からのリクエストを待つだけである」。

なぜか。proof server に送るデータには、トークン所有の詳細、DApp のプライベート状態、witness の値といったプライベート情報が含まれるからです。もしこの計算がリモートサーバーで行われたら、そのサーバーがあなたのプライベートデータを見てしまいます。

ローカルで動かすということは、あなたの秘密がマシンの外に出ないということです。

proof server はポート 6300 で待ち受け、あなたの wallet または SDK からのリクエストを受け付けます。

インターネットに自分から接続しにいくことはありません。

外へ「電話をかける(phone home)」こともありません。

ただそこに座って、証明生成のリクエストを待ち、結果を返すだけです。

circuit を呼ぶと何が起きるか

Lesson 1.3 の hello-world CLI で「オプション 1(メッセージを保存)」を選んだとき、何が起きるのかを正確に追ってみましょう。

これが、キー入力から、確定した状態変更までの、流れの全体です。

Step 1: 回路の実行(ローカル)

contract.callTx.storeMessage("Hello Midnight") を呼ぶと、SDK はあなたのマシン上でローカルに回路ロジックを実行します。ここがあなたの Compact コードが実際に走る場所です。回路は入力("Hello Midnight")を受け取り、disclose() を適用し、新しい台帳状態がどうあるべきかを決めます。

このステップの出力は “unproven transaction”(未証明の取引)。状態変更を記述してはいますが、まだ暗号学的な証明が付いていない取引です。

Step 2: 取引の残高調整(balancing、ローカル)

取引を証明する前に、balance(残高調整)が必要です。これは、取引が実行手数料をまかなえるだけの DUST トークンを持っていることを保証する作業です。wallet が入力(DUST UTXO)と出力を組み立て、計算が合うこと——入力の合計が、出力の合計+手数料以上であること——を確かめます。

ここで、Lesson 2.4 のマルチウォレット構成が登場します。

WalletFacade が、shielded wallet(ZSwap 操作用)、unshielded wallet(tNight トークン用)、DUST wallet(手数料の支払い用)を取りまとめます。1 つの取引の残高調整が、この 3 つ全部にまたがる操作になることもあります。

Step 3: 証明の生成(proof server)

ここが重い部分であり、20〜60 秒の待ち時間が生まれる理由です。

SDK は、未証明の取引データと、それに対応する証明用の素材(proving material)を、ローカルの proof server(http://localhost:6300)へ送ります。

proof server は、主要なエンドポイントとして /prove-tx(HTTP)を公開しています。これは、未証明の取引を記述したバイナリ blob(に加えて、必要な proving key・verifying key・zkIR ソース)を受け取り、証明済みの取引を記述したバイナリ blob を返します

内部では、取引とそれに付随する証明用アーティファクトがシリアライズされ(取引には Borsh、鍵や zkIR には動的なバイトの配列を使用)、/prove-tx へ送られます。そして応答として返ってくるのは、オンチェーンで検証できるゼロ知識証明が付け足された、同じ取引です。

proof server は、コンパイル時に生成された ZK の proving key(hello-world 契約なら contracts/managed/hello-world/keys/ ディレクトリ)を使います。これらの鍵は回路ごとに固有で、storeMessage 用の proving key は、別の契約の increment 用の proving key とは別物です。

proof server が作り出すものは ZK-SNARK、すなわち Succinct Non-interactive Argument of Knowledge です。

  • “succinct”(簡潔) ——もとの計算がどれだけ複雑でも、証明そのものはとても小さい。
  • “non-interactive”(非対話) ——検証者が prover とやり取りする必要がない。証明それ自体が語ってくれる。

Step 4: 取引の送信(ネットワーク)

証明がついたら、取引は RPC エンドポイント(https://rpc.preprod.midnight.network)経由で Midnight ノードへ送信されます。ノードは取引を受け取り、それをトランザクションプールに置きます。

この段階でノードは初期検証を行います。取引が well-formed(整形式)であること——つまりランタイムと台帳仕様が定める構造的・論理的な要件を満たしていること——をチェックします。

これは素早いチェックであって、証明の完全な検証ではありません

Step 5: ブロックへの取り込みと検証(ネットワーク)

block producer(ブロック生成者)がプールから取引を拾い上げ、ブロックに含めます。ここで初めて、埋め込まれた証明が完全に検証されます。

ネットワークは(こちらもコンパイル時に生成された)verifying key を使って証明をチェックします。証明が正しければ、回路が定義した状態遷移が実行され、新しい状態がオンチェーンのストレージ層にコミットされます。

ここで ZK 証明の魔法が現れます。検証者(ネットワーク)は、プライベートな入力をいっさい見ることなく、計算が正しく行われたことを確認できるのです。

ネットワークは、あなたがローカルで何を計算したのかは決して見ません。「正しく計算した」という証明だけを見るのです。

Step 6: Indexer からの通知(ネットワーク → あなたの DApp)

Midnight Indexer はチェーンを追いかけ、イベントやデータを問い合わせ可能なデータベースにインデックスします。

あなたの DApp の publicDataProvider(WebSocket で indexer に接続している)に、取引が確定したことと新しいブロック高が通知されます。SDK は Promise を解決し、CLI は取引 ID とブロック高つきの成功メッセージを表示します。

読み取りは別もの

CLI で「オプション 2(メッセージを読む)」を選ぶと、上のことは何ひとつ起きません。読み取りは、取引ではなく indexer への問い合わせ(query)だからです。

あなたの DApp は providers.publicDataProvider.queryContractState(contractAddress) を呼び、indexer の HTTP エンドポイントに対して GraphQL クエリを投げます。indexer はその契約アドレスの現在の台帳状態を引き、それを返します。

  • 証明の生成は無し。
  • 取引は無し。
  • DUST の手数料も無し。
  • 待ち時間も無し。

だから読み取りは一瞬で、無料なのです。

動画で学ぶ(公式)

🎬 Proof Server と取引の流れ(公式の補足動画) YouTubeで開く ↗

手数料(fees)の実際のしくみ

状態を変える取引はどれも DUST を消費します。これが Midnight の取引手数料のやり方で、Ethereum の gas や他チェーンの手数料とは違った動き方をします。

DUST は、Midnight 上の取引手数料の支払いにのみ使われる、shielded(秘匿された)ネットワーク資源です。

DUST は自由に取引できるトークンではなく、ふつうの意味での価値の保存手段にもなりません。取引所で直接買うものではなく、代わりに、ネイティブトークン NIGHT を保有していると、DUST アドレスを指定したあと、時間とともに DUST が生成されていきます。その DUST アドレスに紐づく NIGHT の量が、DUST の生成レートと、そのアドレスに貯められる DUST の上限(“cap”)を決めます。DUST は手数料の支払いに使われると消費(burn)され、残高が cap を下回っていて指定が有効なあいだは、生成が再開されます。

取引の残高調整のとき、wallet は DUST の “spend” を含めます。すなわち、DUST UTXO を 1 つ消費し、手数料額を公開で宣言し、残りの残高を持つ新しい DUST UTXO を作ります。この DUST の spend 自体もゼロ知識証明で証明され、あなたの DUST 総残高を明かすことなく、手数料の計算が正しいことだけを確認します。

コンパイルが実際に生成するもの

前のレッスンで npm run compile を実行したとき、コンパイラは 4 つのディレクトリを生成しました。取引の流れの文脈で、それぞれが何のためにあるのかが、いま理解できます。

ディレクトリ 役割
contract/ Compact 契約を、DApp から呼べる API として公開する TypeScript/JavaScript アーティファクト。contract.callTx.storeMessage() のように呼ぶとき、この生成されたバインディングを使っている
keys/ 各 exported circuit に紐づく暗号鍵。proving key はローカルで proof server が ZK 証明の生成に使い、対応する verifying key は台帳がオンチェーンで証明を検証するのに使う
zkir/ Zero-Knowledge Intermediate Representation。あなたの Compact コードと、proof server が使うゼロ知識バックエンドを橋わたしする回路表現
compiler/ コンパイル過程の中間ビルドアーティファクト

SDK の NodeZkConfigProvider が、実行時にこれらをすべて読み込み、必要に応じて proof server に渡します。これが、providers を作るときに zkConfigPath を渡す理由です。SDK は、回路パラメータ・proving key・verifying key をどこから探せばよいかを知る必要があるからです。

proof server の設定

proof server は Docker イメージmidnightntwrk/proof-server:8.0.3)としてパッケージされています。hello-world プロジェクトでは、それを管理するために docker-compose.yml を使いました。

-v フラグは詳細ログ(verbose logging)を有効にします。ポート 6300 はデフォルトで、変えてはいけません。SDK や wallet ツールは、このポートにいることを前提にしています。

proof server はネイティブサービスとして実装され、ゼロ知識証明の生成という計算量の重い仕事をこなすよう設計されています。これは通常のアプリケーションロジックよりはるかにリソースを食います。証明の生成は、現代のマルチコア CPU の恩恵を受けます。特に、複数の証明リクエストを次々と処理していくときに効いてきます。

ひとつ知っておくべきこと。ある回路タイプの「最初の」証明は、必ずそれ以降の証明より遅いです。

これは、proof server が回路パラメータを読み込み、証明のコンテキストをセットアップする必要があるためです。その初回セットアップのあとは、同じ回路の以降の証明は速くなります。

全体の流れ(図のテキスト版)

すべての状態変更が、この道をたどります。毎回、です。メッセージを保存しようと、カウンターを増やそうと、契約をデプロイしようと、トークンを移そうと、流れは同じです——ローカルで実行し、残高を調整し、証明し、送信し、検証する

補足(動画)

公式の補足動画があります。 https://www.youtube.com/watch?v=yNg_QCJdpXw(外部リンク・別タブで開きます)

次のレッスンへ(原文の予告)

これで、裏側で何が起きているのかが分かりました。次のレッスンでは、witness とプライベートデータに踏み込みます。あなたの TypeScript コードと Compact 回路のあいだで情報がどう流れるのか、そしてなぜ witness が Midnight のプライバシーの鍵なのか、です。

開発者として押さえる点

  • 状態変更が遅く、読み取りが速いのには理由がある。状態変更は「実行 → balance → 証明 → 送信 → 検証」を毎回たどり、読み取りは indexer への GraphQL クエリ 1 本(取引でも手数料でもない)。
  • proof server はローカルで(ポート 6300)動かす。プライベートデータ(token 所有・private state・witness 値)がマシンの外に出ないための設計。自分から外部接続はしない。
  • 重い証明生成は SDK が http://localhost:6300/prove-tx に投げる。取引は Borsh でシリアライズされ、proving key / verifying key / zkIR も一緒に渡る。返るのは ZK 証明付きの取引。
  • 鍵は回路ごとに固有keys/ の proving key はローカル生成用、verifying key はオンチェーン検証用。コンパイルが作る 4 ディレクトリ(contract/ keys/ zkir/ compiler/)を NodeZkConfigProviderzkConfigPath 経由で読み込む。
  • 手数料は DUSTNIGHT 保有から生成され、cap まで貯まり、支払いで burn される。DUST の spend 自体も ZK 証明で「総残高を見せずに手数料計算の正しさ」を示す。
  • 検証側はプライベート入力を見ない。ネットワークが見るのは「正しく計算した」という証明だけ。これが ZK の核心。

やさしい版・公式へ

つぎに読むページ

➡️ 原文準拠コースの入口へ戻る。このコースについて(次のレッスンは順次追加します)