关于 compact
Compact Language Skill 为使用 Midnight 智能合约语言 Compact 的开发者提供语言参考与实践指导。内容涵盖电路、公共账本与私有 witness、数据类型、安全模式及编译流程。
主要功能
- 01解释电路与 witness 模型
- 02说明账本状态和 disclose()
- 03介绍数据类型及标准库
- 04提供隐私与安全设计模式
- 05展示合约编译和常见错误
适用场景
- 01规划 Compact 合约的公共与私有数据
- 02实现基于承诺或 Merkle 树的隐私模式
- 03检查 circuit、witness 和类型用法
- 04编译合约并理解常见错误
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ kali-decoder/ midnight-skills --skill compact安装后在 Claude Code 中输入/compact确认已加载。
详细介绍
Compact 语言技能
Compact 是 Midnight 的智能合约语言。它看起来像 TypeScript,但会编译为零知识电路。编译器会处理所有密码学机制——你只需编写业务逻辑。
主要参考资料:
compact-book.vercel.app— 结构清晰、以示例为主的参考资料(最佳信息来源)docs.midnight.network/compact— 官方规范(截至 2026 年,测试和安全页面仍标注为“即将推出”)
1) 心智模型
编写任何内容前必须牢记的三条原则:
1. A circuit is a constraint system, not a function.
It declares relationships that MUST hold. The proof proves they held.
2. The ledger is public. Everything in `export ledger` is plaintext on-chain.
Private data lives in witnesses. It never touches the chain.
3. `disclose()` is a compile-time annotation, not encryption.
It tells the compiler "I am intentionally making this public."
The compiler prevents accidental disclosure. No runtime cost.
两个世界:
| 世界 | 位置 | 谁能看到 | 更新方式 |
|---|---|---|---|
export ledger | 每个网络节点 | 所有人 | ZK 证明 |
witness(私有) | 用户的本地存储 | 仅所有者 | 永不更新(始终保留在本地) |
2) 合约结构
每个 Compact 合约都包含四个必需部分和一个可选部分:
pragma language_version >= 0.22; // 1. Language version
import CompactStandardLibrary; // optional, needed for hashing, Merkle, etc.
export ledger counter: Uint<64>; // 2. Public state (on-chain, readable by all)
witness callerSecret(): Bytes<32>; // 3. Private input (body lives in TypeScript)
constructor() { // 5. OPTIONAL: runs once on deployment
counter = 0;
}
export circuit increment(): [] { // 4. Logic (compiles to ZK circuit)
counter = disclose((counter + 1) as Uint<64>);
}
| 部分 | 关键字 | 用途 |
|---|---|---|
| 公共状态 | export ledger | 在链上,所有人均可读取 |
| 私有输入 | witness | DApp 提供的回调(主体在 TypeScript 中) |
| 逻辑 | export circuit | 编译为 ZK 电路 |
| 初始化 | constructor | 部署时运行一次 |
| 组织结构 | module / import | 命名空间和文件管理 |
3) 数据类型
基本类型
| 类型 | 范围/大小 | 说明 |
|---|---|---|
Boolean | true / false | 不会进行真值/假值强制转换 |
Field | 0 到素域阶数 | 仅支持 == 和 !=——不支持 <、> |
Uint<n> | n 位无符号整数,溢出时回绕 | Uint<8> = 0..255 |
Uint<0..n> | 有界整数,范围为 0 到 n-1 | 支持 <、>。强制转换可能在运行时失败 |
Bytes<n> | 恰好 n 个字节 | 使用 pad(n, str) 将字符串填充为固定长度 |
Opaque<"string"> | 外部 JS 数据 | 电路只能看到哈希,看不到内容。不会在链上隐藏 |
**关键:**仅在进行相等性检查时使用 Field。需要排序(<、<=、>、>=)时,使用 Uint<0..n>。
复合类型
// Tuple
const pair: [Field, Boolean] = [42, true];
const first = pair[0];
// Vector (homogeneous, fixed-length)
const v: Vector<4, Uint<8>> = [1, 2, 3, 4];
// Struct (nominal typing — shape isn't enough, name matters)
struct Point { x: Uint<32>, y: Uint<32> }
const p = Point { x: 10, y: 20 };
// Enum (first variant = default)
enum State { VACANT, OCCUPIED }
类型别名
type Hash = Bytes<32>; // structural alias — fully interchangeable
new type UserId = Bytes<32>; // nominal alias — requires explicit cast
默认值
每种类型都有零默认值:false、0、n 个零字节、首个枚举变体。使用 default<T>() 访问。
4) 账本状态(公开)和 disclose()
账本修饰符
ledger val: Field; // private to contract, not exported
export ledger cnt: Counter; // readable from TypeScript/DApp
sealed ledger config: Uint<32>; // write-once (only in constructor)
export sealed ledger mapping: Map<Boolean, Field>;
disclose() 规则
如果数据从 witness 流向账本写入(或电路返回值),则必须用 disclose() 包裹。编译器会跟踪 witness 污染在每次操作中的传播;如果未经处理的披露值到达账本,就会报错。
// ❌ COMPILER ERROR — undisclosed witness data reaching ledger
export circuit record(): [] {
stored = getSecret();
}
// ✅ CORRECT — explicitly disclosed
export circuit record(): [] {
stored = disclose(getSecret());
}
disclose() 不是加密,而是编译时信号。披露的值会以明文形式保存在链上。
账本 ADT 选择指南
| 用例 | 类型 |
|---|---|
| 单个可变值 | ledger f: T(Cell) |
| 单调递增计数器 | Counter(使用 .increment(n)) |
| 成员跟踪(会揭示成员身份) | Set<T> |
| 按键存储 | Map<K, V> |
| 有序队列 | List<T> |
| 匿名成员证明(当前根) | MerkleTree<n, T> |
| 匿名成员证明(历史根) | HistoricMerkleTree<n, T> |
| 区块时间、代币、合约地址 | Kernel |
5) 电路和 Witness
电路
// Exported = callable from DApp
export circuit post(msg: Opaque<"string">): [] {
message = disclose(msg);
}
// Pure = no ledger access, no side effects
pure circuit publicKey(sk: Bytes<32>, seq: Bytes<32>): Bytes<32> {
return persistentHash<Vector<3, Bytes<32>>>([pad(32, "domain:"), seq, sk]);
}
// Internal (no export keyword) — not callable from DApp
circuit hashTokenData(owner: Bytes<32>, meta: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([owner, meta]);
}
- 返回类型为
[]表示无返回值(过程式) assert(condition, "message")= 运行时保护条件;若条件为 false,交易将失败- 始终使用
assert验证 witness 输出——witness 不可信
Witness
witness 声明一个私有输入回调。签名写在 Compact 中,主体写在 TypeScript 中。
// Compact side — declaration only
witness callerAddress(): Bytes<32>;
witness findMerklePath(leaf: Bytes<32>): MerkleTreePath<10, Bytes<32>>;
// TypeScript side — implementation
function callerAddress(context: WitnessContext): Uint8Array {
return context.coinPublicKey; // or wallet-derived address
}
function findMerklePath(
context: WitnessContext,
leaf: Uint8Array,
): MerkleTreePath<Uint8Array> {
return context.ledger.items.findPathForLeaf(leaf)!;
}
Witness 数据永远不会触及区块链。只有证明电路正确运行的 ZK 证明会上链。
6) 标准库——关键函数
import CompactStandardLibrary;
哈希
| 函数 | 输出 | 升级后保留? | 是否受 witness 影响? | 用途 |
|---|---|---|---|---|
transientHash<T>(v) | Field | 否 | 是 | 临时计算 |
transientCommit<T>(v, rand) | Field | 否 | 否 | 无需 disclose() 的临时计算 |
persistentHash<T>(v) | Bytes<32> | 是 | 是 | 链上密钥、ID |
persistentCommit<T>(v, rand) | Bytes<32> | 是 | 否 | 在链上隐藏敏感值 |
关键区别:
*Hash= 仅对值进行哈希。值空间较小时可通过暴力破解。*Commit= 对值和随机 nonce 进行哈希。即使值较小,也无法通过暴力破解。- 持久化 = 合约升级后仍保留。用于账本存储。
- 临时 = 仅供临时使用。升级后不会保留。
填充与转换
pad(32, "midnight:domain:separator") // Creates Bytes<32> from string
Merkle 树
// Check that a path validates to the current root
assert(
items.checkRoot(merkleTreePathRoot<10, Bytes<32>>(path)),
"invalid merkle path"
);
// TypeScript: get the path
const path = context.ledger.items.findPathForLeaf(leaf); // O(n) scan
const path = context.ledger.items.pathForLeaf(index); // O(1) with index
Maybe / Option
export ledger message: Maybe<Opaque<"string">>;
message = some<Opaque<"string">>(newMessage); // has value
message = none<Opaque<"string">>(); // empty
const val = message.value; // access inner value (only when present)
7) 安全模式
官方安全文档“即将推出”。请使用 compact-book 中的这些模式。
公开内容(假设一切都是公开的)
| 操作 | 会泄露的内容 |
|---|---|
ledger.insert(v) | 值 v |
ledger.lookup(k) | 键 k 和返回值 |
set.member(f(x)) | f(x),而不是 x |
merkleTree.insert(v) | 不会泄露 v |
| 电路参数 | 全部参数 |
witness 返回值 | 不会泄露任何内容(保留在本地) |
模式 1:承诺(存储哈希,而非值)
export ledger balanceCommitments: Map<Bytes<32>, Bytes<32>>;
export circuit commitBalance(value: Uint<64>): [] {
const nonce = freshNonce();
const commitment = persistentCommit<Uint<64>>(value, nonce);
balanceCommitments.insert(disclose(callerAddress()), disclose(commitment));
}
重复使用 nonce = 隐私灾难。 使用相同 nonce 和值的两个承诺在链上完全相同。务必使用全新的 nonce。
模式 2:基于哈希的身份验证(通过哈希实现 ZK 签名)
witness secretKey(): Bytes<32>;
export ledger organizer: Bytes<32>;
constructor() {
organizer = disclose(publicKey(secretKey()));
}
export circuit adminAction(): [] {
assert(organizer == publicKey(secretKey()), "not authorized");
// ... do the thing
}
pure circuit publicKey(sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>(
[pad(32, "myapp:admin:v1"), sk]
);
}
域分隔符至关重要。 同一个密钥在不同用途下会生成不同的公钥。切勿跨域复用公钥。
模式 3:用于匿名成员资格的 Merkle 树
Set<T> 会泄露正在检查的承诺。MerkleTree<n, T> 只会泄露已证明某个值存在。
export ledger allowlist: MerkleTree<16, Bytes<32>>;
witness findPath(leaf: Bytes<32>): MerkleTreePath<16, Bytes<32>>;
export circuit addToAllowlist(commitment: Bytes<32>): [] {
allowlist.insert(disclose(commitment)); // insert still requires disclose
}
export circuit proveAllowlisted(): [] {
const sk = secretKey();
const leaf = publicKey(sk);
const path = findPath(leaf);
assert(
allowlist.checkRoot(merkleTreePathRoot<16, Bytes<32>>(path)),
"not in allowlist"
);
// proof: caller is in the allowlist. Nothing else revealed.
}
深度指南:每增加一层,电路深度约增加 1。通常使用 16–20 层。采用满足需求的最小深度。
模式 4:承诺/空值标记(一次性匿名令牌)
export ledger commitments: HistoricMerkleTree<16, Bytes<32>>;
export ledger nullifiers: Set<Bytes<32>>;
export circuit spend(): [] {
const sk = secretKey();
const commitment = makeCommitment(sk);
const path = findCommitmentPath(commitment);
// 1. Prove membership
assert(
commitments.checkRoot(merkleTreePathRoot<16, Bytes<32>>(path)),
"not in commitments tree"
);
// 2. Prevent reuse
const nul = makeNullifier(sk);
assert(!nullifiers.member(nul), "already spent");
nullifiers.insert(disclose(nul));
}
// CRITICAL: commitment and nullifier MUST use different domain separators
pure circuit makeCommitment(sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "myapp:commit:v1"), sk]);
}
pure circuit makeNullifier(sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "myapp:nullifier:v1"), sk]);
}
仅当证明必须针对过去的根进行验证时(即证明计算后可能会添加项目),才使用 HistoricMerkleTree。否则使用普通的 MerkleTree。
8) 完整合约示例
最简公告板(无隐私)
pragma language_version >= 0.22;
export ledger message: Opaque<"string">;
export circuit post(msg: Opaque<"string">): [] {
message = disclose(msg);
}
带所有权的公告板(隐私模式)
pragma language_version >= 0.22;
import CompactStandardLibrary;
export enum State { VACANT, OCCUPIED }
export ledger state: State;
export ledger message: Maybe<Opaque<"string">>;
export ledger sequence: Counter;
export ledger owner: Bytes<32>;
witness localSecretKey(): Bytes<32>;
constructor() {
state = State.VACANT;
message = none<Opaque<"string">>();
sequence.increment(1);
}
export circuit post(newMessage: Opaque<"string">): [] {
assert(state == State.VACANT, "Board occupied");
owner = disclose(publicKey(localSecretKey(), sequence as Field as Bytes<32>));
message = disclose(some<Opaque<"string">>(newMessage));
state = State.OCCUPIED;
}
export circuit takeDown(): Opaque<"string"> {
assert(state == State.OCCUPIED, "Board empty");
assert(owner == publicKey(localSecretKey(), sequence as Field as Bytes<32>), "Not owner");
const msg = message.value;
state = State.VACANT;
sequence.increment(1);
message = none<Opaque<"string">>();
return msg;
}
pure circuit publicKey(sk: Bytes<32>, seq: Bytes<32>): Bytes<32> {
return persistentHash<Vector<3, Bytes<32>>>([pad(32, "bboard:pk:"), seq, sk]);
}
为什么要使用序列号? 防止重放攻击——同一个密钥每轮都会生成不同的公钥。
NFT 合约(基于承诺的所有权)
pragma language_version >= 0.22;
import CompactStandardLibrary;
export ledger totalSupply: Uint<64>;
export ledger nextTokenId: Uint<64>;
export ledger tokenCommitments: Map<Uint<64>, Bytes<32>>;
witness callerAddress(): Bytes<32>;
constructor() {
totalSupply = 0;
nextTokenId = 1;
}
circuit hashTokenData(owner: Bytes<32>, metaHash: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([owner, metaHash]);
}
export circuit mint(metaHash: Bytes<32>): [] {
const caller = disclose(callerAddress());
const tokenId = nextTokenId;
const commitment = hashTokenData(caller, metaHash);
tokenCommitments.insert(tokenId, disclose(commitment));
totalSupply = disclose((totalSupply + 1) as Uint<64>);
nextTokenId = disclose((tokenId + 1) as Uint<64>);
}
export circuit transfer(tokenId: Uint<64>, newOwner: Bytes<32>, metaHash: Bytes<32>): [] {
const caller = disclose(callerAddress());
const expected = hashTokenData(caller, metaHash);
const pubId = disclose(tokenId);
assert(tokenCommitments.member(pubId), "Token does not exist");
assert(tokenCommitments.lookup(pubId) == expected, "Not the owner");
const nextCommitment = hashTokenData(disclose(newOwner), metaHash);
tokenCommitments.insert(pubId, disclose(nextCommitment));
}
9) 编译与工具
# Compile a contract
compact compile contracts/your-contract.compact contracts/managed/your-contract
# Output structure
contracts/managed/your-contract/
keys/
circuitName.prover # 2–10 MB
circuitName.verifier # ~2 KB
zkir/
circuitName.bzkir # 1–3 KB
也会生成 TypeScript 绑定——从托管输出文件夹导入 Contract 对象。
预览版中的电路大小: ProofStation 要求电路大小至少为 k ≥ 6。错误:prove: no SRS params for k=6 表示电路太小。可添加额外的虚拟账本字段进行填充,以增大电路。
10)常见错误(完整列表)
披露相关错误:
- 未使用
disclose()就将见证数据写入账本 → 编译器报错(很好,能及早发现) - 未使用
disclose()就从export circuit返回见证数据 → 编译器报错 - 误以为
disclose()会加密数据 → 它不会,数据仍是明文
类型相关错误:
- 需要使用
<、>时却用了Field→ 改用Uint<0..n> - 在运行时使用
as Uint<0..n>强制转换 → 如果值超出范围,可能会失败(交易错误,而非编译错误) - 混淆
type Hash = Bytes<32>(结构类型,可互换)和new type UserId = Bytes<32>(名义类型,需要强制转换)
隐私相关错误:
- 对取值空间较小的值使用
persistentHash(可被暴力破解)→ 使用带新鲜 nonce 的persistentCommit - 重用 nonce → 两个值相同的承诺在链上完全相同
- 需要匿名性时使用
Set<T>→Set.member(v)会暴露v。请改用MerkleTree - 承诺和 nullifier 使用相同的域分隔符 → 可能发生碰撞
- 使用
transientHash存储到账本 → 瞬态值无法在合约升级后保留 - 在
export ledger中放入每个用户的数据 → 泄露隐私
账本相关错误:
- 忘记对见证输出使用
assert→ 见证不可信,必须验证 - 在
constructor中编写本该放在电路中的逻辑 → 构造函数只运行一次,之后便不再运行 - 不必要地使用
HistoricMerkleTree→ 会增加存储成本;除非需要历史根,否则请使用MerkleTree - 对内部辅助电路使用
export→ 不必要地扩大暴露面
11)关键字速查
| 关键字 | 用途 |
|---|---|
pragma language_version >= n | 声明最低语言版本 |
import ModuleName | 导入模块 |
export ledger f: T | 公开的链上状态字段 |
sealed ledger f: T | 仅可写入一次的状态(仅限构造函数) |
witness f(): T | 私有输入回调(函数体在 TypeScript 中) |
export circuit f(): T | 可调用的 ZK 电路 |
pure circuit f(): T | 纯函数(不访问账本) |
constructor() | 部署时运行一次 |
disclose(v) | 标记有意公开披露 |
assert(cond, msg) | 运行时保护条件(条件为 false 时交易失败) |
default<T>() | 类型 T 的零值/空值 |
pad(n, str) | 根据字符串创建 Bytes<n> |
some<T>(v) / none<T>() | Maybe/Option 构造函数 |
new type A = B | 名义类型别名 |
type A = B | 结构类型别名 |