关于 example-zk-loan-application
为构建 Midnight Network 隐私贷款 dApp 提供的开发技能,面向使用 Compact 和 TypeScript 的开发者。它涵盖零知识资格验证、链下信用签名服务及 CLI 或可选网页界面的实现流程。
主要功能
- 01零知识电路验证贷款资格
- 02链下信用资料 Schnorr 签名
- 03提供部署与贷款 CLI 流程
- 04可选 Next.js 贷款界面
- 05支持公开贷款状态查询
适用场景
- 01构建不公开收入和信用分的贷款应用
- 02实现链下信用资料签名与链上验证
- 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 部署卡住、GraphQLoffset: null、ZK 资源路径references/versions.json— 固定的@midnight-ntwrk/*版本
主要参考资料:
example-leaderboard-dapp//templates/leaderboard-dapp/— Next.js + 1AM、底层部署/调用、索引器读取example-locker-dapp/— 见证私有状态、createUnprovenDeployTx+submitTxAsyncexample-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 辅助方法)
工作流
协助用户时,请按以下顺序操作:
- Monorepo — 根目录
package.json工作区:contract、attestation-api、cli - 合约 — 编译 Compact(
schnorr.compact+zkloan-credit-scorer.compact)+ 见证 - 证明 API — 生成/加载提供方 Jubjub 密钥对;对信用档案签名;提供 REST 端点
- 部署 — CLI 或浏览器:底层部署,将
userSecretKey+ 合约地址保存到私有状态 - 注册提供方 — 管理员在链上调用
registerProvider(providerId, providerPk) - 申请贷款 — 获取证明 → 填充私有状态 →
requestLoan(amount, secretPin) - 响应 — 如果状态为
Proposed,调用respondToLoan(loanId, secretPin, accept) - 读取状态 — 使用索引器 GraphQL → 解码
loans嵌套映射(公开) - 前端 (可选) — 连接 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 必须:
- 使用信用资料 + 用户派生公钥哈希调用证明 API
- 将返回的签名 + 提供方 ID 与信用字段一同存储在私有状态中
- 确保
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 模式:
| 命令 | 用途 |
|---|---|
deploy | createUnprovenDeployTx → 通过构造函数将部署者设为管理员 |
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 的结构:
- 连接 —
detectWallet()→wallet.connect('preprod')→createConnectedSession(api, ZK_PATH) - 部署 / 加入 — 部署新合约或粘贴现有地址;保存到
localStorage - 管理面板 (仅部署者可见) — 注册来自 API 的证明提供方 ID + 公钥
- 申请贷款 — 表单:金额、秘密 PIN、信用字段 → 调用证明 API →
requestLoan - 我的贷款 — 解码用户派生公钥对应的索引器状态;显示状态徽章(已批准 / 已提议 / 已拒绝)
- 响应 — 如果状态为
Proposed,则显示按钮,可接受最高符合条件的金额或拒绝 - 可选 — 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 |
| Ledger | 8.0.3 |
| midnight-js | 4.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 → 部署 → 注册提供方 → 申请贷款 → 查看链上结果。