mcpField
E
example-zk-loan-application

example-zk-loan-application

github.com/kali-decoder開発18122 回インストール1 日前に更新
リポジトリ

example-zk-loan-application について

Compact と TypeScript を使って Midnight Network 上にプライバシー保護型ローン dApp を構築するための開発スキルです。ゼロ知識による適格性確認、オフチェーンの信用情報署名、CLI または任意の Web UI の実装を扱います。

主な機能

  1. 01ZK 回路でローン適格性を検証
  2. 02Schnorr による信用情報のオフチェーン署名
  3. 03デプロイとローン操作の CLI フロー
  4. 04任意の Next.js ローン画面
  5. 05公開ローン状態の照会

活用シーン

  1. 01収入や信用スコアを公開しないローンアプリの構築
  2. 02オフチェーン署名とオンチェーン検証の実装
  3. 03ローン申請、応答、状態照会フローの開発

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/kali-decoder/midnight-skills --skill example-zk-loan-application

インストール後、Claude Code で/example-zk-loan-applicationを入力して読み込まれたか確認します。

詳細

Midnight Network ZK Loan Application

クレジットスコア、収入、勤続年数をオンチェーンで公開することなく、申請者が ZK 回路内で適格性を証明する、プライバシーを保護するローン dApp です。信頼できる証明プロバイダーがクレジットデータにオフチェーンで署名し、コントラクトは回路内で Schnorr 署名を検証して、ローンの結果(ステータス+承認額)のみを台帳に書き込みます。

このスキルで生成されるもの:

  • contract/ — schnorr.compact と zkloan-credit-scorer.compact、ウィットネス、コンパイルスクリプト
  • zkloan-credit-scorer-attestation-api/ — クレジットプロフィールに Schnorr 署名する REST サーバー
  • zkloan-credit-scorer-cli/ — ローンのデプロイ、プロバイダーの登録、リクエストと応答を行う CLI(ヘッドレスウォレット)
  • app/loan/ (オプション) — Next.js + 1AM ウォレット UI(templates/leaderboard-dapp/ のパターンをコピー)
  • lib/zkloan.ts — デプロイ、requestLoan、respondToLoan、管理者呼び出し、台帳のデコード
  • lib/midnight.ts — ウォレットセッション+パッチ適用済みインデクサープロバイダー(references/midnight-session.md からコピー)
  • public/zk/zkloan/ — コントラクトのビルドから同期された ZK 証明アセット

共通リファレンス(プロバイダーの正式な実装+トラブルシューティング。プロンプト内で重複させないでください):

  • references/midnight-session.md — createConnectedSession、インデクサーのパッチ、デプロイ/呼び出しヘルパー
  • references/gotchas.md — preprod でのデプロイのハング、GraphQL の offset: null、ZK アセットのパス
  • references/versions.json — 固定された @midnight-ntwrk/* のバージョン

主なリファレンス:

  • example-leaderboard-dapp/ / templates/leaderboard-dapp/ — Next.js + 1AM、低レベルのデプロイ/呼び出し、インデクサーからの読み取り
  • example-locker-dapp/ — ウィットネスのプライベート状態、createUnprovenDeployTx + submitTxAsync
  • example-counter/ — ヘッドレス CLI ウォレット、モノレポのワークスペース、vitest によるコントラクトテスト
  • compact/ — disclose()、ウィットネス、persistentHash、Map、Set、new type、純粋回路
  • security/ — 呼び出し元の識別に ownPublicKey() を決して信頼しないこと。ウィットネスからキーを導出します
  • indexer/ — ウォレットを使わずに公開ローン台帳を読み取ります

アーキテクチャ上の重要な注意点:

  • クレジットスコア、収入、勤続年数、証明署名はプライベートのままです(ウィットネスのみ)
  • オンチェーンに記録されるのは、ローンのステータス+承認額+導出されたユーザー公開鍵のバイト列のみです
  • 認証に決して ownPublicKey() を使わないでください。これは証明者が指定できるため、回避可能です。getUserSecret() ウィットネス+PIN から deriveUserPublicKey/deriveAdminPublicKey を使って本人確認情報を導出してください
  • evaluateApplicant 内で transientHash(userPubKeyBytes) を使って証明を紐付けます
  • 適格性の段階:≥700 + ≥$2k + ≥24mo → $10k、≥600 + ≥$1.5k → $7k、≥580 → $3k、それ以外は却下
  • 金額が段階ごとの上限を超える場合、requestLoan は Proposed を返すことがあります。ユーザーは respondToLoan を呼び出して承認または辞退します
  • PIN の変更時は、changePin + onGoingPinMigration 台帳マップを使って、5 件ずつローンを移行します
  • createUnprovenDeployTx + submitTxAsync を使ってください。deployContract() は使わないでください(preprod でハングします)
  • @midnight-ntwrk/wallet-sdk-shielded の使用には Node.js ≥ 22 が必要です(Map.values() の Iterator ヘルパーを使用するため)

ワークフロー

ユーザーを支援する際は、次の手順に従ってください。

  1. モノレポ — ルートの package.json ワークスペース:contract、attestation-api、cli
  2. コントラクト — Compact(schnorr.compact + zkloan-credit-scorer.compact)とウィットネスをコンパイル
  3. 証明 API — プロバイダーの Jubjub キーペアを生成/読み込み、クレジットプロフィールに署名し、REST エンドポイントを公開
  4. デプロイ — CLI またはブラウザーから低レベルのデプロイを行い、userSecretKey + コントラクトアドレスをプライベート状態に保存
  5. プロバイダーを登録 — 管理者がオンチェーンで registerProvider(providerId, providerPk) を呼び出す
  6. ローンをリクエスト — 証明を取得 → プライベート状態に入力 → requestLoan(amount, secretPin)
  7. 応答 — ステータスが Proposed の場合、respondToLoan(loanId, secretPin, accept) を呼び出す
  8. 状態を読み取る — インデクサーの GraphQL → loans のネストしたマップをデコード(公開)
  9. フロントエンド (オプション) — 1AM に接続し、デプロイ/参加、ローンフォーム、ローンテーブル、管理者パネルを用意

1) モノレポ構成

zkloan-credit-scorer/
├── package.json                    # workspaces + pinned midnight-js 4.0.4 / ledger 8.0.3
├── .nvmrc                          # 22
├── contract/
│   ├── package.json
│   └── src/
│       ├── schnorr.compact
│       ├── zkloan-credit-scorer.compact
│       ├── witnesses.ts
│       ├── index.ts
│       └── managed/zkloan-credit-scorer/   # compiler output (gitignored)
├── zkloan-credit-scorer-attestation-api/
│   └── src/                        # Express/Fastify REST — sign credit profiles
├── zkloan-credit-scorer-cli/
│   └── src/                        # deploy, register-provider, request-loan, admin
└── zkloan-dapp/                    # optional Next.js frontend
    ├── lib/midnight.ts             # copy from templates/leaderboard-dapp
    ├── lib/zkloan.ts
    ├── app/loan/LoanClient.tsx
    ├── contract/                   # symlink or workspace import from ../contract
    ├── scripts/sync-zk-assets.mjs
    └── public/zk/zkloan/

ルートの package.json のエンジン要件:"node": ">=22.0.0"。ワークスペースでは @midnight-ntwrk/compact-runtime: ^0.16.0、@midnight-ntwrk/midnight-js-*: 4.0.4、@midnight-ntwrk/ledger-v8: 8.0.3 にバージョンを固定しています。


2) Compact コントラクト

Schnorr モジュール(contract/src/schnorr.compact)

回路内で Jubjub Schnorr 署名を検証します。Compact Standard Library に jubjubSchnorrVerify が追加されるまでの一時的なポリフィルです。

module schnorr {
  import CompactStandardLibrary;
  export struct SchnorrSignature { announcement: JubjubPoint; response: Field; }
  witness getSchnorrReduction(challengeHash: Field): [Field, Uint<248>];
  export circuit schnorrVerify<#n>(msg: Vector<n, Field>, signature: SchnorrSignature, pk: JubjubPoint): [] { /* ... */ }
  export pure circuit schnorrChallenge(...): Field { /* transientHash challenge */ }
}

JubjubPoint の等価性: jubjubPointX() / jubjubPointY() を比較してください。構造体の == は参照の等価性を比較するため、新しいポイントでは常に失敗します。

メインコントラクト(contract/src/zkloan-credit-scorer.compact)
pragma language_version >= 0.22 && <= 0.23;
import CompactStandardLibrary;
import "schnorr" prefix Schnorr_;

export enum LoanStatus { Approved, Rejected, Proposed, NotAccepted }
export struct LoanApplication { authorizedAmount: Uint<16>; status: LoanStatus; }

export new type UserSecretKey = Bytes<32>;
export new type UserPublicKey = Bytes<32>;
export new type AdminPublicKey = Bytes<32>;

export ledger blacklist: Set<UserPublicKey>;
export ledger loans: Map<Bytes<32>, Map<Uint<16>, LoanApplication>>;
export ledger onGoingPinMigration: Map<Bytes<32>, Uint<16>>;
export ledger contractAdmin: AdminPublicKey;
export ledger providers: Map<Uint<16>, JubjubPoint>;

witness getAttestedScoringWitness(): [Applicant, Schnorr_SchnorrSignature, Uint<16>];
witness getUserSecret(): UserSecretKey;

export pure circuit deriveUserPublicKey(sk: UserSecretKey, pin: Uint<16>): UserPublicKey { /* persistentHash domain "zkloan:user:pk:v1" */ }
export pure circuit deriveAdminPublicKey(sk: UserSecretKey): AdminPublicKey { /* "zkloan:admin:pk:v1" */ }

export circuit requestLoan(amountRequested: Uint<16>, secretPin: Uint<16>): [] { /* ... */ }
export circuit respondToLoan(loanId: Uint<16>, secretPin: Uint<16>, accept: Boolean): [] { /* ... */ }
export circuit changePin(oldPin: Uint<16>, newPin: Uint<16>): [] { /* batched migration, 5 per tx */ }

// Admin (all guard with deriveAdminPublicKey(getUserSecret()) == contractAdmin)
export circuit blacklistUser(account: UserPublicKey): [] { /* ... */ }
export circuit registerProvider(providerId: Uint<16>, providerPk: JubjubPoint): [] { /* ... */ }
export circuit rotateAdmin(newAdmin: AdminPublicKey): [] { /* ... */ }

プライバシー境界:

データ可視性
クレジットスコア、収入、勤続期間非公開(witness)
アテステーション署名非公開(ZK 入力)
ユーザーシークレット + PIN非公開(witness / 回路入力)
派生した UserPublicKey(ローンマップのキー)公開(台帳)
ローンのステータス + 承認額公開(台帳)
contractAdmin、providers、blacklist公開(台帳)

コンパイル:

cd contract
npm run compact    # → src/managed/zkloan-credit-scorer/
npm run build
cd ..

ブラウザー UI では、npm run sync:assets も実行してください(keys/zkir を public/zk/zkloan/ にコピーします)。


3) Witness

contract/src/witnesses.ts:

export type ZKLoanCreditScorerPrivateState = {
  creditScore: bigint;
  monthlyIncome: bigint;
  monthsAsCustomer: bigint;
  attestationSignature: SchnorrSignature;
  attestationProviderId: bigint;
  userSecretKey: Uint8Array;   // 32 bytes — authoritative caller identity
};

export const witnesses = {
  getAttestedScoringWitness: (ctx) => [ctx.privateState, [profile, sig, providerId]],
  getSchnorrReduction: (ctx, challengeHash) => [ctx.privateState, [q, r]],  // challenge / 2^248
  getUserSecret: (ctx) => [ctx.privateState, ctx.privateState.userSecretKey],
};

requestLoan の前に、TypeScript で以下を行う必要があります。

  1. 信用プロファイルと、ユーザーの派生 pubkey ハッシュを使ってアテステーション API を呼び出す
  2. 返された署名とプロバイダー ID を、信用情報とともにプライベート状態に保存する
  3. userSecretKey が 32 バイトであることを確認する(localStorage または CLI キーストアに保存)

4) アテステーション API

信頼できるオフチェーン署名者です。フロー:

sequenceDiagram
  participant User
  participant API as Attestation API
  participant Wallet as 1AM / CLI
  participant Chain as Midnight Network

  User->>API: POST /attest { creditScore, income, tenure, userPubKeyHash }
  API->>API: Schnorr-sign msg = [score, income, tenure, userPubKeyHash]
  API-->>User: { signature, providerId }
  User->>Wallet: requestLoan(amount, pin) + private state with attestation
  Wallet->>Chain: ZK tx — evaluateApplicant verifies signature in-circuit
  Chain-->>User: Loan Proposed / Approved / Rejected on ledger

実装チェックリスト:

  • 起動時に Jubjub プロバイダーのキーペアを生成する(または環境変数から読み込む)
  • コントラクトのエクスポートされた pureCircuits.schnorrChallenge を使ってチャレンジを計算する(回路内と同じハッシュ)
  • プロバイダーのシークレットキーで署名し、{ announcement, response } と providerId を返す
  • ローンが成功する前に、管理者が registerProvider(providerId, providerPk) をオンチェーンで実行する必要がある
  • アテステーションを transientHash(deriveUserPublicKey(secret, pin)) にバインドする(回路が使うものと同じハッシュ)

エンドポイントの形状の例:

// POST /attest
// body: { creditScore, monthlyIncome, monthsAsCustomer, userPubKeyHash: string }
// response: { providerId, signature: { announcement: { x, y }, response } }

ローカルで実行する(デフォルトのポートは例として 3001):

cd zkloan-credit-scorer-attestation-api
npm run dev

5) CLI(ヘッドレスウォレット)

example-counter/ の CLI パターンを踏襲します。

コマンド目的
deploycreateUnprovenDeployTx → コンストラクターを通じてデプロイヤーを管理者として保存
register-provider <id>管理者:アテステーション API からオンチェーンのプロバイダー公開鍵を登録
request-loan <amount> <pin>アテステーションを取得 → プライベート状態を設定 → requestLoan
respond-loan <id> <pin> accept|declineProposed ローンを承認または辞退
list-loans派生ユーザーキーの公開 loans マップをインデクサーから読み取る
change-pin <old> <new>onGoingPinMigration がクリアされるまで繰り返す
blacklist-user <userPk>管理者によるガバナンス

@midnight-ntwrk/wallet-sdk-facade と level プライベート状態プロバイダーを使用します。ウォレット設定または .env で、ローカルの証明サーバーと preprod インデクサーを指定してください。


6) TypeScript 統合(ブラウザー)

lib/zkloan.ts — templates/leaderboard-dapp/lib/leaderboard.ts と同じ低レベルパターンです。

関数目的
getOrCreateUserSecret()32 バイトのシークレットを localStorage に保存
deriveUserPublicKey(session, pin)エクスポートされた純粋回路をオフチェーンで呼び出す
fetchAttestation(apiUrl, profile, userPubKeyHash)アテステーション API に POST する
deployZkLoan(session)デプロイし、シークレットでプライベート状態を初期化する
registerProvider(session, addr, id, pk)管理者による回路呼び出し
requestLoan(session, addr, amount, pin, profile)アテステーション → プライベート状態を更新 → requestLoan
respondToLoan(session, addr, loanId, pin, accept)提案された金額を承認/辞退する
fetchLoanState(queryUrl, addr, userPkBytes)インデクサーをポーリングし、ネストされた loans マップをデコードする
import { createUnprovenDeployTx, submitCallTxAsync, submitTxAsync } from '@midnight-ntwrk/midnight-js-contracts';
import { ContractState } from '@midnight-ntwrk/compact-runtime';
import { CompiledZKLoanContract, Contract, ledger } from '../contract/src/index';

export const ZK_PATH = '/zk/zkloan';
const PRIVATE_STATE_ID = 'zkloanPrivateState';

export async function requestLoan(session, contractAddress, amount, pin, attestation) {
  await session.providers.privateStateProvider.set(PRIVATE_STATE_ID, {
    ...attestation,
    userSecretKey: getOrCreateUserSecret(),
  });
  await submitCallTxAsync(session.providers, {
    compiledContract: CompiledZKLoanContract,
    contractAddress,
    circuitId: 'requestLoan',
    args: [BigInt(amount), BigInt(pin)],
    privateStateId: PRIVATE_STATE_ID,
  });
}

ZK アセットのパス:/zk/zkloan(public/zk/zkloan/ に同期されます)。


7) ブラウザー UI

templates/leaderboard-dapp/app/leaderboard/LeaderboardClient.tsx から構成をコピーします。

  1. 接続 — detectWallet() → wallet.connect('preprod') → createConnectedSession(api, ZK_PATH)
  2. デプロイ/参加 — 新しいコントラクトをデプロイするか、既存のアドレスを貼り付けます。localStorage に保存します
  3. 管理者パネル (デプロイしたユーザーのみ) — API からアテステーションプロバイダー ID と公開鍵を登録します
  4. ローンを申し込む — フォーム:金額、秘密の PIN、信用情報のフィールド → アテステーション API を呼び出す → requestLoan
  5. 自分のローン — ユーザーの派生公開鍵に対応するインデクサーの状態をデコードし、ステータスバッジ(Approved/Proposed/Rejected)を表示します
  6. 回答する — Proposed の場合、適格な上限額を受け入れるボタン、または辞退するボタンを表示します
  7. オプション — PIN 変更ウィザード(移行が完了するまで changePin を繰り返し呼び出します)

公開ローン記録のインデクサー読み取りはウォレットなしで動作します。書き込みには接続とアテステーションのみが必要です。


8) エンドツーエンドのローカル実行

# Terminal 1 — proof server
docker run -d -p 6300:6300 midnightntwrk/proof-server:8.0.3 -- \
  midnight-proof-server --network preprod

# Terminal 2 — attestation API
cd zkloan-credit-scorer-attestation-api && npm run dev

# Terminal 3 — CLI or Next.js UI
cd zkloan-credit-scorer-cli   # or zkloan-dapp/
npm install
cd ../contract && npm run compact && npm run build && cd ..
npm run sync:assets           # UI only
npm run deploy                # CLI
npm run register-provider 1
npm run request-loan 5000 1234

ウォレット:Preprod 上の 1AM 拡張機能、プルーフサーバー http://localhost:6300、フォーセットから入手した tNIGHT と tDUST。


9) 前提条件

コンポーネントバージョン
Node.js≥ 22(シールドウォレット SDK に必須)
Compact コンパイラー0.22–0.23(pragma language_version)
Compact ランタイム0.16.0
Ledger8.0.3
midnight-js4.0.4
プルーフサーバー8.0.3
Dockerローカルのプルーフサーバー用
node --version          # v22+
compact compile --version
compact update
docker run -d -p 6300:6300 midnightntwrk/proof-server:8.0.3 -- \
  midnight-proof-server --network preprod

10) セキュリティチェックリスト

  • ブラックリスト、管理者、ユーザー ID の確認に ownPublicKey() を使用しないでください
  • アテステーションプロバイダーの秘密鍵はアテステーションサーバーのみに保管し、ブラウザーのプライベート状態には決して含めないでください
  • disclose() は、証人値をレジャーの読み取りに渡しても安全であることを示します。信用情報のフィールドは決して公開されません
  • ブラックリストにはウォレットアドレスではなく、派生した UserPublicKey を保存します
  • ドメイン区切り文字列を変更した場合は再デプロイしてください。persistentHash のプリイメージは派生キーに影響します
  • アテステーション API にレート制限を設け、署名する前に入力範囲を検証してください

11) 本番環境での注意事項

  • アテステーション API を HTTPS の背後にデプロイします。rotateAdmin でプロバイダーキーをローテーションし、プロバイダーを再登録してください
  • フロントエンド:Vercel/Netlify のビルドステップで npm run compact && npm run sync:assets を実行します
  • デフォルトのコントラクトアドレスを環境変数または localStorage に保存します
  • メインネットでは、ウォレットの getConfiguration() を通じてネットワーク ID、インデクサー URL、プルーフサーバーを更新します
  • references/versions.json のバージョンを固定し、サポートマトリックスと照合してください

クイックコマンド

# Contract only
cd contract && npm install && npm run compact && npm run build

# Full monorepo (from root)
npm install
npm run validate:registry   # if added to workspace scripts
cd contract && npm run test:compile

# Optional browser UI (after adding zkloan-dapp from leaderboard template)
cd zkloan-dapp
npm install && npm run compact && npm run sync:assets && npm run dev

http://localhost:3000 を開く → 1AM に接続 → デプロイ → プロバイダーを登録 → ローンを申し込む → オンチェーンで結果を確認。

コメント