compact

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Compact Language Skill

Compact语言技能

Compact is Midnight's smart contract language. It looks like TypeScript but compiles to zero-knowledge circuits. The compiler handles all cryptographic machinery — you write business logic.
Primary references:
  • compact-book.vercel.app
    — structured example-driven reference (your best source)
  • docs.midnight.network/compact
    — official spec (testing/security pages are "coming soon" as of 2026)

Compact是Midnight的智能合约语言。它外观类似TypeScript,但会编译为零知识(ZK)电路。编译器会处理所有密码学机制——你只需编写业务逻辑。
主要参考资料:
  • compact-book.vercel.app
    —— 结构化的示例驱动型参考资料(最佳来源)
  • docs.midnight.network/compact
    —— 官方规范(截至2026年,测试/安全页面“即将推出”)

1) The Mental Model

1) 心智模型

Three non-negotiables before writing anything:
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.
The two worlds:
WorldWhereWho sees itUpdated by
export ledger
Every network nodeEveryoneZK proof
witness
(private)
User's local storageOnly the ownerNever (stays local)

编写代码前必须明确的三点:
1. 电路是约束系统,而非函数。
   它声明必须成立的关系。证明用于验证这些关系确实成立。

2. 账本是公开的。`export ledger`中的所有内容都是链上明文。
   私有数据存储在witness中,永远不会上链。

3. `disclose()`是编译时注解,而非加密。
   它告知编译器“我有意将此内容公开”。
   编译器会防止意外披露,无运行时成本。
两个环境:
环境位置可见者更新方式
export ledger
每个网络节点所有人ZK证明
witness
(私有)
用户本地存储仅所有者永不更新(始终保存在本地)

2) Contract Structure

2) 合约结构

Every Compact contract has four mandatory pieces + one optional:
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>);
}
PieceKeywordPurpose
Public state
export ledger
On-chain, readable by all
Private inputs
witness
Callbacks the DApp provides (body in TypeScript)
Logic
export circuit
Compiles to ZK circuits
Initialization
constructor
Runs once on deploy
Organization
module
/
import
Namespace and file management

每个Compact合约包含四个必填部分 + 一个可选部分:
compact
pragma language_version >= 0.22;      // 1. 语言版本

import CompactStandardLibrary;         // 可选,哈希、Merkle树等功能需要

export ledger counter: Uint<64>;       // 2. 公开状态(链上,所有人可读)

witness callerSecret(): Bytes<32>;     // 3. 私有输入(实现体在TypeScript中)

constructor() {                        // 5. 可选:部署时运行一次
  counter = 0;
}

export circuit increment(): [] {       // 4. 逻辑(编译为ZK电路)
  counter = disclose((counter + 1) as Uint<64>);
}
部分关键字用途
公开状态
export ledger
链上存储,所有人可读
私有输入
witness
DApp提供的回调函数(实现体在TypeScript中)
逻辑
export circuit
编译为ZK电路
初始化
constructor
部署时运行一次
组织管理
module
/
import
命名空间和文件管理

3) Data Types

3) 数据类型

Primitives

基本类型

TypeRange/SizeNotes
Boolean
true
/
false
No truthy/falsy coercion
Field
0 to prime field orderOnly
==
and
!=
— no
<
,
>
Uint<n>
n-bit unsigned, wraps on overflow
Uint<8>
= 0..255
Uint<0..n>
Bounded integer, 0 to n-1Supports
<
,
>
. Cast can fail at runtime
Bytes<n>
Exactly n bytesUse
pad(n, str)
for fixed-length from strings
Opaque<"string">
Foreign JS dataCircuits see only hash, not contents. Not hidden on-chain
Critical: Use
Field
only for equality checks. Use
Uint<0..n>
whenever you need ordering (
<
,
<=
,
>
,
>=
).
类型范围/大小说明
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>

Composite Types

复合类型

compact
// 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 }
compact
// 元组
const pair: [Field, Boolean] = [42, true];
const first = pair[0];

// 向量(同类型,固定长度)
const v: Vector<4, Uint<8>> = [1, 2, 3, 4];

// 结构体(标称类型 —— 仅形状不够,名称也很重要)
struct Point { x: Uint<32>, y: Uint<32> }
const p = Point { x: 10, y: 20 };

// 枚举(第一个变体为默认值)
enum State { VACANT, OCCUPIED }

Type Aliases

类型别名

compact
type Hash = Bytes<32>;        // structural alias — fully interchangeable
new type UserId = Bytes<32>;  // nominal alias — requires explicit cast
compact
type Hash = Bytes<32>;        // 结构别名 —— 完全可互换
new type UserId = Bytes<32>;  // 标称别名 —— 需要显式转换

Default Values

默认值

Every type has a zero default:
false
,
0
,
n
zero bytes, first enum variant. Access with
default<T>()
.

每种类型都有零值默认值:
false
0
、n个零字节、枚举的第一个变体。使用
default<T>()
获取。

4) Ledger State (Public) and
disclose()

4) 账本状态(公开)与
disclose()

Ledger Modifiers

账本修饰符

compact
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>;
compact
ledger val: Field;                          // 合约私有,不对外导出
export ledger cnt: Counter;                 // 可从TypeScript/DApp读取
sealed ledger config: Uint<32>;             // 仅可写入一次(仅在constructor中)
export sealed ledger mapping: Map<Boolean, Field>;

The
disclose()
Rule

disclose()
规则

If data flows from a
witness
to a ledger write (or circuit return), it must be wrapped in
disclose()
. The compiler tracks witness taint through every operation and will error if untainted disclosure reaches the ledger.
compact
// ❌ COMPILER ERROR — undisclosed witness data reaching ledger
export circuit record(): [] {
  stored = getSecret();
}

// ✅ CORRECT — explicitly disclosed
export circuit record(): [] {
  stored = disclose(getSecret());
}
disclose()
is not encryption. It is a compile-time signal. The disclosed value is plaintext on-chain.
如果数据从
witness
流向账本写入(或电路返回值),则必须
disclose()
包裹。编译器会跟踪每个操作中的witness污染,若未污染的披露数据到达账本,会抛出错误。
compact
// ❌ 编译错误 —— 未披露的witness数据流入账本
export circuit record(): [] {
  stored = getSecret();
}

// ✅ 正确 —— 显式披露
export circuit record(): [] {
  stored = disclose(getSecret());
}
disclose()
不是加密,而是编译时信号。披露的值会以明文形式上链。

Ledger ADT Chooser

账本ADT选择器

Use caseType
Single mutable value
ledger f: T
(Cell)
Monotonically growing counter
Counter
(use
.increment(n)
)
Membership tracking (reveals which)
Set<T>
Per-key storage
Map<K, V>
Ordered queue
List<T>
Anonymous membership proofs (current root)
MerkleTree<n, T>
Anonymous membership proofs (past roots)
HistoricMerkleTree<n, T>
Block time, tokens, contract address
Kernel

使用场景类型
单个可变值
ledger f: T
(单元存储)
单调递增计数器
Counter
(使用
.increment(n)
成员跟踪(会暴露具体成员)
Set<T>
按键存储
Map<K, V>
有序队列
List<T>
匿名成员证明(当前根)
MerkleTree<n, T>
匿名成员证明(历史根)
HistoricMerkleTree<n, T>
区块时间、代币、合约地址
Kernel

5) Circuits and Witnesses

5) 电路与Witness

Circuits

电路

compact
// 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]);
}
  • []
    as return type = no return value (procedure-style)
  • assert(condition, "message")
    = runtime guard, transaction fails if condition is false
  • Always validate witness outputs with
    assert
    — witnesses are untrusted
compact
// 导出的电路 = 可从DApp调用
export circuit post(msg: Opaque<"string">): [] {
  message = disclose(msg);
}

// 纯电路 = 不访问账本,无副作用
pure circuit publicKey(sk: Bytes<32>, seq: Bytes<32>): Bytes<32> {
  return persistentHash<Vector<3, Bytes<32>>>([pad(32, "domain:"), seq, sk]);
}

// 内部电路(无export关键字)—— 不可从DApp调用
circuit hashTokenData(owner: Bytes<32>, meta: Bytes<32>): Bytes<32> {
  return persistentHash<Vector<2, Bytes<32>>>([owner, meta]);
}
  • []
    作为返回类型 = 无返回值(过程式)
  • assert(condition, "message")
    = 运行时守卫,若条件为假则交易失败
  • 务必用
    assert
    验证witness输出 —— witness不可信

Witnesses

Witness

A
witness
declares a private input callback. The signature is in Compact, the body is in TypeScript.
compact
// Compact side — declaration only
witness callerAddress(): Bytes<32>;
witness findMerklePath(leaf: Bytes<32>): MerkleTreePath<10, Bytes<32>>;
typescript
// 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 data never touches the chain. Only the ZK proof that the circuit ran correctly goes on-chain.

witness
声明一个私有输入回调函数。签名在Compact中定义,实现体在TypeScript中编写。
compact
// Compact侧 —— 仅声明
witness callerAddress(): Bytes<32>;
witness findMerklePath(leaf: Bytes<32>): MerkleTreePath<10, Bytes<32>>;
typescript
// TypeScript侧 —— 实现
function callerAddress(context: WitnessContext): Uint8Array {
  return context.coinPublicKey;  // 或钱包派生地址
}

function findMerklePath(
  context: WitnessContext,
  leaf: Uint8Array,
): MerkleTreePath<Uint8Array> {
  return context.ledger.items.findPathForLeaf(leaf)!;
}
Witness数据永远不会上链。只有电路正确运行的ZK证明会上链。

6) Standard Library — Key Functions

6) 标准库 —— 核心函数

compact
import CompactStandardLibrary;
compact
import CompactStandardLibrary;

Hashing

哈希

FunctionOutputSurvives upgrade?Witness-tainted?Use for
transientHash<T>(v)
Field
NoYesTemporary computations
transientCommit<T>(v, rand)
Field
NoNoTemp computation without
disclose()
persistentHash<T>(v)
Bytes<32>
YesYesKeys, IDs on-chain
persistentCommit<T>(v, rand)
Bytes<32>
YesNoHiding sensitive values on-chain
Key distinction:
  • *Hash
    = hash of value only. Brute-forcible for small value spaces.
  • *Commit
    = hash of value + random nonce. Infeasible to brute-force even for small values.
  • Persistent = survives contract upgrades. Use for ledger storage.
  • Transient = temporary. Does NOT survive upgrades.
函数输出合约升级后是否保留?是否受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
    = 对值+随机一次性数进行哈希。即使是小值域也无法暴力破解。
  • Persistent = 合约升级后仍保留。用于账本存储。
  • Transient = 临时数据。合约升级后不会保留。

Padding and Conversion

填充与转换

compact
pad(32, "midnight:domain:separator")   // Creates Bytes<32> from string
compact
pad(32, "midnight:domain:separator")   // 从字符串创建Bytes<32>

Merkle Trees

Merkle树

compact
// 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
compact
// 验证路径是否匹配当前根
assert(
  items.checkRoot(merkleTreePathRoot<10, Bytes<32>>(path)),
  "invalid merkle path"
);

// TypeScript:获取路径
const path = context.ledger.items.findPathForLeaf(leaf);  // O(n)扫描
const path = context.ledger.items.pathForLeaf(index);      // 已知索引时O(1)

Maybe / Option

Maybe / 可选类型

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

compact
export ledger message: Maybe<Opaque<"string">>;

message = some<Opaque<"string">>(newMessage);   // 有值
message = none<Opaque<"string">>();              // 空值

const val = message.value;  // 访问内部值(仅当存在时)

7) Security Patterns

7) 安全模式

The official security docs are "coming soon." Use these patterns from the compact-book.
官方安全文档“即将推出”。使用compact-book中的以下模式。

What's Public (assume everything is)

哪些内容是公开的(假设所有内容都是)

OperationWhat it reveals
ledger.insert(v)
The value
v
ledger.lookup(k)
Key
k
and the returned value
set.member(f(x))
f(x)
, not
x
merkleTree.insert(v)
Does NOT reveal
v
Circuit argumentsAll of them
witness
return values
Nothing (stays local)
操作会暴露什么
ledger.insert(v)
v
ledger.lookup(k)
k
和返回值
set.member(f(x))
f(x)
,而非
x
merkleTree.insert(v)
不会暴露
v
电路参数所有参数
witness
返回值
无(始终保存在本地)

Pattern 1: Commitment (Store Hash, Not Value)

模式1:承诺(存储哈希,而非值)

compact
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 reuse = privacy catastrophe. Two commitments with same nonce + value are identical on-chain. Always fresh nonce.
compact
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));
}
重复使用一次性数 = 隐私灾难。 相同一次性数+值的两个承诺在链上完全相同。务必使用全新的一次性数。

Pattern 2: Hash-Based Auth (ZK Signatures via Hashing)

模式2:基于哈希的认证(通过哈希实现ZK签名)

compact
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]
  );
}
Domain separator is critical. Same secret key → different public keys for different purposes. Never reuse a public key across domains.
compact
witness secretKey(): Bytes<32>;
export ledger organizer: Bytes<32>;

constructor() {
  organizer = disclose(publicKey(secretKey()));
}

export circuit adminAction(): [] {
  assert(organizer == publicKey(secretKey()), "not authorized");
  // ... 执行操作
}

pure circuit publicKey(sk: Bytes<32>): Bytes<32> {
  return persistentHash<Vector<2, Bytes<32>>>(
    [pad(32, "myapp:admin:v1"), sk]
  );
}
域分隔符至关重要。 相同私钥在不同用途下会生成不同公钥。切勿跨域重用公钥。

Pattern 3: Merkle Tree for Anonymous Membership

模式3:用于匿名成员证明的Merkle树

Set<T>
reveals which commitment was checked.
MerkleTree<n, T>
only reveals that some value was proven.
compact
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.
}
Depth guide: each level adds ~1 to circuit depth. 16–20 is typical. Use minimum depth needed.
Set<T>
会暴露被检查的承诺。
MerkleTree<n, T>
仅能证明存在某个值。
compact
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));  // 插入仍需使用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"
  );
  // 证明:调用者在白名单中。无其他信息暴露。
}
深度指南:每增加一层,电路深度约增加1。通常使用16–20层。使用满足需求的最小深度即可。

Pattern 4: Commitment/Nullifier (Single-Use Anonymous Tokens)

模式4:承诺/Nullifier(一次性匿名代币)

compact
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]);
}
Use
HistoricMerkleTree
only when proofs must verify against past roots (i.e., items may be added after the proof was computed). Otherwise use plain
MerkleTree
.

compact
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. 证明成员身份
  assert(
    commitments.checkRoot(merkleTreePathRoot<16, Bytes<32>>(path)),
    "not in commitments tree"
  );
  
  // 2. 防止重复使用
  const nul = makeNullifier(sk);
  assert(!nullifiers.member(nul), "already spent");
  nullifiers.insert(disclose(nul));
}

// 关键:承诺和Nullifier必须使用不同的域分隔符
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) Complete Contract Examples

8) 完整合约示例

Minimal Bulletin Board (No Privacy)

极简公告板(无隐私保护)

compact
pragma language_version >= 0.22;

export ledger message: Opaque<"string">;

export circuit post(msg: Opaque<"string">): [] {
  message = disclose(msg);
}
compact
pragma language_version >= 0.22;

export ledger message: Opaque<"string">;

export circuit post(msg: Opaque<"string">): [] {
  message = disclose(msg);
}

Bulletin Board with Ownership (Privacy Pattern)

带所有权的公告板(隐私保护模式)

compact
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]);
}
Why sequence? Prevents replay attacks — same secret key produces different public key each round.
compact
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]);
}
为什么需要sequence? 防止重放攻击 —— 相同私钥每次循环生成不同的公钥。

NFT Contract (Commitment-Based Ownership)

NFT合约(基于承诺的所有权)

compact
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));
}

compact
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) Compilation and Tooling

9) 编译与工具

bash
undefined
bash
undefined

Compile a contract

编译合约

compact compile contracts/your-contract.compact contracts/managed/your-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 bindings are also generated — import the `Contract` object from the managed output folder.

**Circuit size on preview:** ProofStation requires minimum circuit size (`k ≥ 6`). Error: `prove: no SRS params for k=6` means the circuit is too small. Pad with additional dummy ledger fields to increase size.

---
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) Common Mistakes (The Full List)

10) 常见错误(完整列表)

Disclosure mistakes:
  • Writing witness data to the ledger without
    disclose()
    → compiler error (good, caught early)
  • Returning witness data from
    export circuit
    without
    disclose()
    → compiler error
  • Thinking
    disclose()
    encrypts data → it doesn't, it's plaintext
Type mistakes:
  • Using
    Field
    when you need
    <
    ,
    >
    → use
    Uint<0..n>
    instead
  • as Uint<0..n>
    cast at runtime → can fail if value is out of range (transaction error, not compile error)
  • Confusing
    type Hash = Bytes<32>
    (structural, interchangeable) with
    new type UserId = Bytes<32>
    (nominal, requires cast)
Privacy mistakes:
  • Using
    persistentHash
    for small value spaces (brute-forcible) → use
    persistentCommit
    with fresh nonce
  • Reusing nonces → two same-value commitments are identical on-chain
  • Using
    Set<T>
    when you need anonymity →
    Set.member(v)
    reveals
    v
    . Use
    MerkleTree
    instead
  • Same domain separator for commitment and nullifier → potential collision
  • Using
    transientHash
    for ledger storage → transient values don't survive contract upgrades
  • Putting per-user data in
    export ledger
    → privacy leak
Ledger mistakes:
  • Forgetting
    assert
    on witness outputs → witnesses are untrusted, validate them
  • Writing logic in
    constructor
    that should be in circuits → constructor runs once and is done
  • Using
    HistoricMerkleTree
    unnecessarily → adds storage cost; use
    MerkleTree
    unless past roots are needed
  • export
    on internal helper circuits → exposes surface area unnecessarily

披露错误:
  • 将witness数据写入账本时未使用
    disclose()
    → 编译错误(很好,提前发现)
  • export circuit
    返回witness数据时未使用
    disclose()
    → 编译错误
  • 认为
    disclose()
    会加密数据 → 不会,数据是明文
类型错误:
  • 需要
    <
    >
    时使用
    Field
    → 改用
    Uint<0..n>
  • 运行时使用
    as Uint<0..n>
    转换 → 若值超出范围会失败(交易错误,非编译错误)
  • 混淆
    type Hash = Bytes<32>
    (结构别名,可互换)与
    new type UserId = Bytes<32>
    (标称别名,需要显式转换)
隐私错误:
  • 对小值域使用
    persistentHash
    (可被暴力破解)→ 使用带全新一次性数的
    persistentCommit
  • 重复使用一次性数 → 相同值的两个承诺在链上完全相同
  • 需要匿名性时使用
    Set<T>
    Set.member(v)
    会暴露
    v
    。改用
    MerkleTree
  • 承诺和Nullifier使用相同的域分隔符 → 可能发生碰撞
  • 对账本存储使用
    transientHash
    → 临时数据无法在合约升级后保留
  • 将用户专属数据放入
    export ledger
    → 隐私泄露
账本错误:
  • 忘记用
    assert
    验证witness输出 → witness不可信,必须验证
  • 应在电路中编写的逻辑却放在
    constructor
    中 →
    constructor
    仅运行一次
  • 不必要地使用
    HistoricMerkleTree
    → 增加存储成本;除非需要历史根,否则使用
    MerkleTree
  • 对内部辅助电路使用
    export
    → 不必要地暴露调用接口

11) Keyword Quick Reference

11) 关键字速查

KeywordPurpose
pragma language_version >= n
Declare minimum language version
import ModuleName
Import a module
export ledger f: T
Public on-chain state field
sealed ledger f: T
Write-once state (constructor only)
witness f(): T
Private input callback (body in TypeScript)
export circuit f(): T
Callable ZK circuit
pure circuit f(): T
Pure function (no ledger access)
constructor()
Runs once on deployment
disclose(v)
Mark intentional public disclosure
assert(cond, msg)
Runtime guard (tx fails if false)
default<T>()
Zero/empty value for type T
pad(n, str)
Create
Bytes<n>
from string
some<T>(v)
/
none<T>()
Maybe/Option constructors
new type A = B
Nominal type alias
type A = B
Structural type alias
关键字用途
pragma language_version >= n
声明最低语言版本
import ModuleName
导入模块
export ledger f: T
链上公开状态字段
sealed ledger f: T
仅可写入一次的状态(仅在constructor中)
witness f(): T
私有输入回调函数(实现体在TypeScript中)
export circuit f(): T
可调用的ZK电路
pure circuit f(): T
纯函数(不访问账本)
constructor()
部署时运行一次
disclose(v)
标记有意公开的内容
assert(cond, msg)
运行时守卫(条件为假则交易失败)
default<T>()
类型T的零值/空值
pad(n, str)
从字符串创建
Bytes<n>
some<T>(v)
/
none<T>()
Maybe/可选类型构造函数
new type A = B
标称类型别名
type A = B
结构类型别名