mcpField
C
compact

compact

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

关于 compact

Compact Language Skill 为使用 Midnight 智能合约语言 Compact 的开发者提供语言参考与实践指导。内容涵盖电路、公共账本与私有 witness、数据类型、安全模式及编译流程。

主要功能

  1. 01解释电路与 witness 模型
  2. 02说明账本状态和 disclose()
  3. 03介绍数据类型及标准库
  4. 04提供隐私与安全设计模式
  5. 05展示合约编译和常见错误

适用场景

  1. 01规划 Compact 合约的公共与私有数据
  2. 02实现基于承诺或 Merkle 树的隐私模式
  3. 03检查 circuit、witness 和类型用法
  4. 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在链上,所有人均可读取
私有输入witnessDApp 提供的回调(主体在 TypeScript 中)
逻辑export circuit编译为 ZK 电路
初始化constructor部署时运行一次
组织结构module / import命名空间和文件管理

3) 数据类型

基本类型
类型范围/大小说明
Booleantrue / false不会进行真值/假值强制转换
Field0 到素域阶数仅支持 == 和 !=——不支持 <、>
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结构类型别名

评论

支持 Markdown