mcpField
E
example-zk-loan-application

example-zk-loan-application

github.com/kali-decoder开发18122 次安装1 天前更新
代码仓库

关于 example-zk-loan-application

为构建 Midnight Network 隐私贷款 dApp 提供的开发技能,面向使用 Compact 和 TypeScript 的开发者。它涵盖零知识资格验证、链下信用签名服务及 CLI 或可选网页界面的实现流程。

主要功能

  1. 01零知识电路验证贷款资格
  2. 02链下信用资料 Schnorr 签名
  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 贷款申请

一个保护隐私的贷款 dApp,申请人可在 ZK 电路中证明自己符合资格,而无需在链上透露信用评分、收入或任职时长。可信的证明提供方在链下对信用数据签名;合约在电路内验证 Schnorr 签名,并仅将贷款结果(状态 + 授权金额)写入账本。

此技能生成的内容:

  • contract/ — schnorr.compact + zkloan-credit-scorer.compact、见证、编译脚本
  • zkloan-credit-scorer-attestation-api/ — 对信用档案进行 Schnorr 签名的 REST 服务
  • zkloan-credit-scorer-cli/ — 部署、注册提供方、申请/响应贷款(无头钱包)
  • app/loan/ (可选) — Next.js + 1AM 钱包界面(从 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 钱包、monorepo 工作区、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. Monorepo — 根目录 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) Monorepo 结构

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 的 engines:"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 之前,暂时使用此 polyfill。

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) Witnesses

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. 使用信用资料 + 用户派生公钥哈希调用证明 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 提供方密钥对(或从 env 加载)
  • 通过合约导出的 pureCircuits.schnorrChallenge 计算 challenge(与电路内使用的哈希相同)
  • 使用提供方私钥签名;返回 { 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|decline接受或拒绝 Proposed 状态的贷款
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()在 localStorage 中持久化 32 字节密钥
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. 我的贷款 — 解码用户派生公钥对应的索引器状态;显示状态徽章(已批准 / 已提议 / 已拒绝)
  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)安全检查清单

  • 不要在黑名单、管理员或用户身份检查中使用 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 → 部署 → 注册提供方 → 申请贷款 → 查看链上结果。

评论

支持 Markdown